diff --git a/package.json b/package.json index 6a366ba..71ade15 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pixi.js", - "version": "4.3.4", + "version": "4.3.5", "description": "Pixi.js is a fast lightweight 2D library that works across all devices.", "author": "Mat Groves", "contributors": [ @@ -61,7 +61,7 @@ "ismobilejs": "^0.4.0", "object-assign": "^4.0.1", "pixi-gl-core": "^1.0.3", - "resource-loader": "^2.0.4" + "resource-loader": "^2.0.6" }, "devDependencies": { "babel-cli": "^6.18.0", @@ -70,9 +70,9 @@ "babel-preset-es2015": "^6.14.0", "babelify": "^7.3.0", "del": "^2.2.0", - "electron-prebuilt": "^1.3.2", + "electron": "^1.4.15", "eslint": "^3.5.0", - "floss": "^1.2.0", + "floss": "^2.0.1", "gh-pages": "^0.11.0", "jaguarjs-jsdoc": "^1.0.1", "js-md5": "^0.4.1", diff --git a/package.json b/package.json index 6a366ba..71ade15 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pixi.js", - "version": "4.3.4", + "version": "4.3.5", "description": "Pixi.js is a fast lightweight 2D library that works across all devices.", "author": "Mat Groves", "contributors": [ @@ -61,7 +61,7 @@ "ismobilejs": "^0.4.0", "object-assign": "^4.0.1", "pixi-gl-core": "^1.0.3", - "resource-loader": "^2.0.4" + "resource-loader": "^2.0.6" }, "devDependencies": { "babel-cli": "^6.18.0", @@ -70,9 +70,9 @@ "babel-preset-es2015": "^6.14.0", "babelify": "^7.3.0", "del": "^2.2.0", - "electron-prebuilt": "^1.3.2", + "electron": "^1.4.15", "eslint": "^3.5.0", - "floss": "^1.2.0", + "floss": "^2.0.1", "gh-pages": "^0.11.0", "jaguarjs-jsdoc": "^1.0.1", "js-md5": "^0.4.1", diff --git a/src/accessibility/AccessibilityManager.js b/src/accessibility/AccessibilityManager.js index df2716a..f16bcb1 100644 --- a/src/accessibility/AccessibilityManager.js +++ b/src/accessibility/AccessibilityManager.js @@ -3,7 +3,7 @@ import accessibleTarget from './accessibleTarget'; // add some extra variables to the container.. -Object.assign( +core.utils.mixins.delayMixin( core.DisplayObject.prototype, accessibleTarget ); diff --git a/package.json b/package.json index 6a366ba..71ade15 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pixi.js", - "version": "4.3.4", + "version": "4.3.5", "description": "Pixi.js is a fast lightweight 2D library that works across all devices.", "author": "Mat Groves", "contributors": [ @@ -61,7 +61,7 @@ "ismobilejs": "^0.4.0", "object-assign": "^4.0.1", "pixi-gl-core": "^1.0.3", - "resource-loader": "^2.0.4" + "resource-loader": "^2.0.6" }, "devDependencies": { "babel-cli": "^6.18.0", @@ -70,9 +70,9 @@ "babel-preset-es2015": "^6.14.0", "babelify": "^7.3.0", "del": "^2.2.0", - "electron-prebuilt": "^1.3.2", + "electron": "^1.4.15", "eslint": "^3.5.0", - "floss": "^1.2.0", + "floss": "^2.0.1", "gh-pages": "^0.11.0", "jaguarjs-jsdoc": "^1.0.1", "js-md5": "^0.4.1", diff --git a/src/accessibility/AccessibilityManager.js b/src/accessibility/AccessibilityManager.js index df2716a..f16bcb1 100644 --- a/src/accessibility/AccessibilityManager.js +++ b/src/accessibility/AccessibilityManager.js @@ -3,7 +3,7 @@ import accessibleTarget from './accessibleTarget'; // add some extra variables to the container.. -Object.assign( +core.utils.mixins.delayMixin( core.DisplayObject.prototype, accessibleTarget ); diff --git a/src/core/Application.js b/src/core/Application.js index ea0f058..5340578 100644 --- a/src/core/Application.js +++ b/src/core/Application.js @@ -1,6 +1,6 @@ import { autoDetectRenderer } from './autoDetectRenderer'; import Container from './display/Container'; -import Ticker from './ticker/Ticker'; +import { shared, Ticker } from './ticker'; /** * Convenience class to create a new PIXI application. @@ -35,9 +35,9 @@ * @param {boolean} [noWebGL=false] - prevents selection of WebGL renderer, even if such is present * @param {boolean} [options.legacy=false] - If true Pixi will aim to ensure compatibility * with older / less advanced devices. If you experience unexplained flickering try setting this to true. - + * @param {boolean} [useSharedTicker=false] - `true` to use PIXI.ticker.shared, `false` to create new ticker. */ - constructor(width, height, options, noWebGL) + constructor(width, height, options, noWebGL, useSharedTicker = false) { /** * WebGL renderer if available, otherwise CanvasRenderer @@ -46,23 +46,46 @@ this.renderer = autoDetectRenderer(width, height, options, noWebGL); /** - * The root display container that's renderered. + * The root display container that's rendered. * @member {PIXI.Container} */ this.stage = new Container(); /** + * Internal reference to the ticker + * @member {PIXI.ticker.Ticker} + * @private + */ + this._ticker = null; + + /** * Ticker for doing render updates. * @member {PIXI.ticker.Ticker} + * @default PIXI.ticker.shared */ - this.ticker = new Ticker(); - - this.ticker.add(this.render, this); + this.ticker = useSharedTicker ? shared : new Ticker(); // Start the rendering this.start(); } + set ticker(ticker) // eslint-disable-line require-jsdoc + { + if (this._ticker) + { + this._ticker.remove(this.render, this); + } + this._ticker = ticker; + if (ticker) + { + ticker.add(this.render, this); + } + } + get ticker() // eslint-disable-line require-jsdoc + { + return this._ticker; + } + /** * Render the current stage. */ @@ -76,7 +99,7 @@ */ stop() { - this.ticker.stop(); + this._ticker.stop(); } /** @@ -84,7 +107,7 @@ */ start() { - this.ticker.start(); + this._ticker.start(); } /** @@ -98,13 +121,22 @@ } /** + * Reference to the renderer's screen rectangle. Its safe to use as filterArea or hitArea for whole screen + * @member {PIXI.Rectangle} + * @readonly + */ + get screen() + { + return this.renderer.screen; + } + + /** * Destroy and don't use after this. * @param {Boolean} [removeView=false] Automatically remove canvas from DOM. */ destroy(removeView) { this.stop(); - this.ticker.remove(this.render, this); this.ticker = null; this.stage.destroy(); diff --git a/package.json b/package.json index 6a366ba..71ade15 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pixi.js", - "version": "4.3.4", + "version": "4.3.5", "description": "Pixi.js is a fast lightweight 2D library that works across all devices.", "author": "Mat Groves", "contributors": [ @@ -61,7 +61,7 @@ "ismobilejs": "^0.4.0", "object-assign": "^4.0.1", "pixi-gl-core": "^1.0.3", - "resource-loader": "^2.0.4" + "resource-loader": "^2.0.6" }, "devDependencies": { "babel-cli": "^6.18.0", @@ -70,9 +70,9 @@ "babel-preset-es2015": "^6.14.0", "babelify": "^7.3.0", "del": "^2.2.0", - "electron-prebuilt": "^1.3.2", + "electron": "^1.4.15", "eslint": "^3.5.0", - "floss": "^1.2.0", + "floss": "^2.0.1", "gh-pages": "^0.11.0", "jaguarjs-jsdoc": "^1.0.1", "js-md5": "^0.4.1", diff --git a/src/accessibility/AccessibilityManager.js b/src/accessibility/AccessibilityManager.js index df2716a..f16bcb1 100644 --- a/src/accessibility/AccessibilityManager.js +++ b/src/accessibility/AccessibilityManager.js @@ -3,7 +3,7 @@ import accessibleTarget from './accessibleTarget'; // add some extra variables to the container.. -Object.assign( +core.utils.mixins.delayMixin( core.DisplayObject.prototype, accessibleTarget ); diff --git a/src/core/Application.js b/src/core/Application.js index ea0f058..5340578 100644 --- a/src/core/Application.js +++ b/src/core/Application.js @@ -1,6 +1,6 @@ import { autoDetectRenderer } from './autoDetectRenderer'; import Container from './display/Container'; -import Ticker from './ticker/Ticker'; +import { shared, Ticker } from './ticker'; /** * Convenience class to create a new PIXI application. @@ -35,9 +35,9 @@ * @param {boolean} [noWebGL=false] - prevents selection of WebGL renderer, even if such is present * @param {boolean} [options.legacy=false] - If true Pixi will aim to ensure compatibility * with older / less advanced devices. If you experience unexplained flickering try setting this to true. - + * @param {boolean} [useSharedTicker=false] - `true` to use PIXI.ticker.shared, `false` to create new ticker. */ - constructor(width, height, options, noWebGL) + constructor(width, height, options, noWebGL, useSharedTicker = false) { /** * WebGL renderer if available, otherwise CanvasRenderer @@ -46,23 +46,46 @@ this.renderer = autoDetectRenderer(width, height, options, noWebGL); /** - * The root display container that's renderered. + * The root display container that's rendered. * @member {PIXI.Container} */ this.stage = new Container(); /** + * Internal reference to the ticker + * @member {PIXI.ticker.Ticker} + * @private + */ + this._ticker = null; + + /** * Ticker for doing render updates. * @member {PIXI.ticker.Ticker} + * @default PIXI.ticker.shared */ - this.ticker = new Ticker(); - - this.ticker.add(this.render, this); + this.ticker = useSharedTicker ? shared : new Ticker(); // Start the rendering this.start(); } + set ticker(ticker) // eslint-disable-line require-jsdoc + { + if (this._ticker) + { + this._ticker.remove(this.render, this); + } + this._ticker = ticker; + if (ticker) + { + ticker.add(this.render, this); + } + } + get ticker() // eslint-disable-line require-jsdoc + { + return this._ticker; + } + /** * Render the current stage. */ @@ -76,7 +99,7 @@ */ stop() { - this.ticker.stop(); + this._ticker.stop(); } /** @@ -84,7 +107,7 @@ */ start() { - this.ticker.start(); + this._ticker.start(); } /** @@ -98,13 +121,22 @@ } /** + * Reference to the renderer's screen rectangle. Its safe to use as filterArea or hitArea for whole screen + * @member {PIXI.Rectangle} + * @readonly + */ + get screen() + { + return this.renderer.screen; + } + + /** * Destroy and don't use after this. * @param {Boolean} [removeView=false] Automatically remove canvas from DOM. */ destroy(removeView) { this.stop(); - this.ticker.remove(this.render, this); this.ticker = null; this.stage.destroy(); diff --git a/src/core/Shader.js b/src/core/Shader.js new file mode 100644 index 0000000..78b17e6 --- /dev/null +++ b/src/core/Shader.js @@ -0,0 +1,46 @@ +import { GLShader } from 'pixi-gl-core'; +import settings from './settings'; + +function checkPrecision(src, def) +{ + if (src instanceof Array) + { + if (src[0].substring(0, 9) !== 'precision') + { + const copy = src.slice(0); + + copy.unshift(`precision ${def} float;`); + + return copy; + } + } + else if (src.substring(0, 9) !== 'precision') + { + return `precision ${def} float;\n${src}`; + } + + return src; +} + +/** + * Wrapper class, webGL Shader for Pixi. + * Adds precision string if vertexSrc or fragmentSrc have no mention of it. + * + * @class + * @extends GLShader + * @memberof PIXI + */ +export default class Shader extends GLShader +{ + /** + * + * @param {WebGLRenderingContext} gl - The current WebGL rendering context + * @param {string|string[]} vertexSrc - The vertex shader source as an array of strings. + * @param {string|string[]} fragmentSrc - The fragment shader source as an array of strings. + */ + constructor(gl, vertexSrc, fragmentSrc) + { + super(gl, checkPrecision(vertexSrc, settings.PRECISION_VERTEX), + checkPrecision(fragmentSrc, settings.PRECISION_FRAGMENT)); + } +} diff --git a/package.json b/package.json index 6a366ba..71ade15 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pixi.js", - "version": "4.3.4", + "version": "4.3.5", "description": "Pixi.js is a fast lightweight 2D library that works across all devices.", "author": "Mat Groves", "contributors": [ @@ -61,7 +61,7 @@ "ismobilejs": "^0.4.0", "object-assign": "^4.0.1", "pixi-gl-core": "^1.0.3", - "resource-loader": "^2.0.4" + "resource-loader": "^2.0.6" }, "devDependencies": { "babel-cli": "^6.18.0", @@ -70,9 +70,9 @@ "babel-preset-es2015": "^6.14.0", "babelify": "^7.3.0", "del": "^2.2.0", - "electron-prebuilt": "^1.3.2", + "electron": "^1.4.15", "eslint": "^3.5.0", - "floss": "^1.2.0", + "floss": "^2.0.1", "gh-pages": "^0.11.0", "jaguarjs-jsdoc": "^1.0.1", "js-md5": "^0.4.1", diff --git a/src/accessibility/AccessibilityManager.js b/src/accessibility/AccessibilityManager.js index df2716a..f16bcb1 100644 --- a/src/accessibility/AccessibilityManager.js +++ b/src/accessibility/AccessibilityManager.js @@ -3,7 +3,7 @@ import accessibleTarget from './accessibleTarget'; // add some extra variables to the container.. -Object.assign( +core.utils.mixins.delayMixin( core.DisplayObject.prototype, accessibleTarget ); diff --git a/src/core/Application.js b/src/core/Application.js index ea0f058..5340578 100644 --- a/src/core/Application.js +++ b/src/core/Application.js @@ -1,6 +1,6 @@ import { autoDetectRenderer } from './autoDetectRenderer'; import Container from './display/Container'; -import Ticker from './ticker/Ticker'; +import { shared, Ticker } from './ticker'; /** * Convenience class to create a new PIXI application. @@ -35,9 +35,9 @@ * @param {boolean} [noWebGL=false] - prevents selection of WebGL renderer, even if such is present * @param {boolean} [options.legacy=false] - If true Pixi will aim to ensure compatibility * with older / less advanced devices. If you experience unexplained flickering try setting this to true. - + * @param {boolean} [useSharedTicker=false] - `true` to use PIXI.ticker.shared, `false` to create new ticker. */ - constructor(width, height, options, noWebGL) + constructor(width, height, options, noWebGL, useSharedTicker = false) { /** * WebGL renderer if available, otherwise CanvasRenderer @@ -46,23 +46,46 @@ this.renderer = autoDetectRenderer(width, height, options, noWebGL); /** - * The root display container that's renderered. + * The root display container that's rendered. * @member {PIXI.Container} */ this.stage = new Container(); /** + * Internal reference to the ticker + * @member {PIXI.ticker.Ticker} + * @private + */ + this._ticker = null; + + /** * Ticker for doing render updates. * @member {PIXI.ticker.Ticker} + * @default PIXI.ticker.shared */ - this.ticker = new Ticker(); - - this.ticker.add(this.render, this); + this.ticker = useSharedTicker ? shared : new Ticker(); // Start the rendering this.start(); } + set ticker(ticker) // eslint-disable-line require-jsdoc + { + if (this._ticker) + { + this._ticker.remove(this.render, this); + } + this._ticker = ticker; + if (ticker) + { + ticker.add(this.render, this); + } + } + get ticker() // eslint-disable-line require-jsdoc + { + return this._ticker; + } + /** * Render the current stage. */ @@ -76,7 +99,7 @@ */ stop() { - this.ticker.stop(); + this._ticker.stop(); } /** @@ -84,7 +107,7 @@ */ start() { - this.ticker.start(); + this._ticker.start(); } /** @@ -98,13 +121,22 @@ } /** + * Reference to the renderer's screen rectangle. Its safe to use as filterArea or hitArea for whole screen + * @member {PIXI.Rectangle} + * @readonly + */ + get screen() + { + return this.renderer.screen; + } + + /** * Destroy and don't use after this. * @param {Boolean} [removeView=false] Automatically remove canvas from DOM. */ destroy(removeView) { this.stop(); - this.ticker.remove(this.render, this); this.ticker = null; this.stage.destroy(); diff --git a/src/core/Shader.js b/src/core/Shader.js new file mode 100644 index 0000000..78b17e6 --- /dev/null +++ b/src/core/Shader.js @@ -0,0 +1,46 @@ +import { GLShader } from 'pixi-gl-core'; +import settings from './settings'; + +function checkPrecision(src, def) +{ + if (src instanceof Array) + { + if (src[0].substring(0, 9) !== 'precision') + { + const copy = src.slice(0); + + copy.unshift(`precision ${def} float;`); + + return copy; + } + } + else if (src.substring(0, 9) !== 'precision') + { + return `precision ${def} float;\n${src}`; + } + + return src; +} + +/** + * Wrapper class, webGL Shader for Pixi. + * Adds precision string if vertexSrc or fragmentSrc have no mention of it. + * + * @class + * @extends GLShader + * @memberof PIXI + */ +export default class Shader extends GLShader +{ + /** + * + * @param {WebGLRenderingContext} gl - The current WebGL rendering context + * @param {string|string[]} vertexSrc - The vertex shader source as an array of strings. + * @param {string|string[]} fragmentSrc - The fragment shader source as an array of strings. + */ + constructor(gl, vertexSrc, fragmentSrc) + { + super(gl, checkPrecision(vertexSrc, settings.PRECISION_VERTEX), + checkPrecision(fragmentSrc, settings.PRECISION_FRAGMENT)); + } +} diff --git a/src/core/const.js b/src/core/const.js index ef6f662..3de0efe 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -188,7 +188,7 @@ * The gc modes that are supported by pixi. * * The {@link PIXI.settings.GC_MODE} Garbage Collection mode for pixi textures is AUTO - * If set to GC_MODE, the renderer will occasianally check textures usage. If they are not + * If set to GC_MODE, the renderer will occasionally check textures usage. If they are not * used for a specified period of time they will be removed from the GPU. They will of course * be uploaded again when they are required. This is a silent behind the scenes process that * should ensure that the GPU does not get filled up. diff --git a/package.json b/package.json index 6a366ba..71ade15 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pixi.js", - "version": "4.3.4", + "version": "4.3.5", "description": "Pixi.js is a fast lightweight 2D library that works across all devices.", "author": "Mat Groves", "contributors": [ @@ -61,7 +61,7 @@ "ismobilejs": "^0.4.0", "object-assign": "^4.0.1", "pixi-gl-core": "^1.0.3", - "resource-loader": "^2.0.4" + "resource-loader": "^2.0.6" }, "devDependencies": { "babel-cli": "^6.18.0", @@ -70,9 +70,9 @@ "babel-preset-es2015": "^6.14.0", "babelify": "^7.3.0", "del": "^2.2.0", - "electron-prebuilt": "^1.3.2", + "electron": "^1.4.15", "eslint": "^3.5.0", - "floss": "^1.2.0", + "floss": "^2.0.1", "gh-pages": "^0.11.0", "jaguarjs-jsdoc": "^1.0.1", "js-md5": "^0.4.1", diff --git a/src/accessibility/AccessibilityManager.js b/src/accessibility/AccessibilityManager.js index df2716a..f16bcb1 100644 --- a/src/accessibility/AccessibilityManager.js +++ b/src/accessibility/AccessibilityManager.js @@ -3,7 +3,7 @@ import accessibleTarget from './accessibleTarget'; // add some extra variables to the container.. -Object.assign( +core.utils.mixins.delayMixin( core.DisplayObject.prototype, accessibleTarget ); diff --git a/src/core/Application.js b/src/core/Application.js index ea0f058..5340578 100644 --- a/src/core/Application.js +++ b/src/core/Application.js @@ -1,6 +1,6 @@ import { autoDetectRenderer } from './autoDetectRenderer'; import Container from './display/Container'; -import Ticker from './ticker/Ticker'; +import { shared, Ticker } from './ticker'; /** * Convenience class to create a new PIXI application. @@ -35,9 +35,9 @@ * @param {boolean} [noWebGL=false] - prevents selection of WebGL renderer, even if such is present * @param {boolean} [options.legacy=false] - If true Pixi will aim to ensure compatibility * with older / less advanced devices. If you experience unexplained flickering try setting this to true. - + * @param {boolean} [useSharedTicker=false] - `true` to use PIXI.ticker.shared, `false` to create new ticker. */ - constructor(width, height, options, noWebGL) + constructor(width, height, options, noWebGL, useSharedTicker = false) { /** * WebGL renderer if available, otherwise CanvasRenderer @@ -46,23 +46,46 @@ this.renderer = autoDetectRenderer(width, height, options, noWebGL); /** - * The root display container that's renderered. + * The root display container that's rendered. * @member {PIXI.Container} */ this.stage = new Container(); /** + * Internal reference to the ticker + * @member {PIXI.ticker.Ticker} + * @private + */ + this._ticker = null; + + /** * Ticker for doing render updates. * @member {PIXI.ticker.Ticker} + * @default PIXI.ticker.shared */ - this.ticker = new Ticker(); - - this.ticker.add(this.render, this); + this.ticker = useSharedTicker ? shared : new Ticker(); // Start the rendering this.start(); } + set ticker(ticker) // eslint-disable-line require-jsdoc + { + if (this._ticker) + { + this._ticker.remove(this.render, this); + } + this._ticker = ticker; + if (ticker) + { + ticker.add(this.render, this); + } + } + get ticker() // eslint-disable-line require-jsdoc + { + return this._ticker; + } + /** * Render the current stage. */ @@ -76,7 +99,7 @@ */ stop() { - this.ticker.stop(); + this._ticker.stop(); } /** @@ -84,7 +107,7 @@ */ start() { - this.ticker.start(); + this._ticker.start(); } /** @@ -98,13 +121,22 @@ } /** + * Reference to the renderer's screen rectangle. Its safe to use as filterArea or hitArea for whole screen + * @member {PIXI.Rectangle} + * @readonly + */ + get screen() + { + return this.renderer.screen; + } + + /** * Destroy and don't use after this. * @param {Boolean} [removeView=false] Automatically remove canvas from DOM. */ destroy(removeView) { this.stop(); - this.ticker.remove(this.render, this); this.ticker = null; this.stage.destroy(); diff --git a/src/core/Shader.js b/src/core/Shader.js new file mode 100644 index 0000000..78b17e6 --- /dev/null +++ b/src/core/Shader.js @@ -0,0 +1,46 @@ +import { GLShader } from 'pixi-gl-core'; +import settings from './settings'; + +function checkPrecision(src, def) +{ + if (src instanceof Array) + { + if (src[0].substring(0, 9) !== 'precision') + { + const copy = src.slice(0); + + copy.unshift(`precision ${def} float;`); + + return copy; + } + } + else if (src.substring(0, 9) !== 'precision') + { + return `precision ${def} float;\n${src}`; + } + + return src; +} + +/** + * Wrapper class, webGL Shader for Pixi. + * Adds precision string if vertexSrc or fragmentSrc have no mention of it. + * + * @class + * @extends GLShader + * @memberof PIXI + */ +export default class Shader extends GLShader +{ + /** + * + * @param {WebGLRenderingContext} gl - The current WebGL rendering context + * @param {string|string[]} vertexSrc - The vertex shader source as an array of strings. + * @param {string|string[]} fragmentSrc - The fragment shader source as an array of strings. + */ + constructor(gl, vertexSrc, fragmentSrc) + { + super(gl, checkPrecision(vertexSrc, settings.PRECISION_VERTEX), + checkPrecision(fragmentSrc, settings.PRECISION_FRAGMENT)); + } +} diff --git a/src/core/const.js b/src/core/const.js index ef6f662..3de0efe 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -188,7 +188,7 @@ * The gc modes that are supported by pixi. * * The {@link PIXI.settings.GC_MODE} Garbage Collection mode for pixi textures is AUTO - * If set to GC_MODE, the renderer will occasianally check textures usage. If they are not + * If set to GC_MODE, the renderer will occasionally check textures usage. If they are not * used for a specified period of time they will be removed from the GPU. They will of course * be uploaded again when they are required. This is a silent behind the scenes process that * should ensure that the GPU does not get filled up. diff --git a/src/core/display/Container.js b/src/core/display/Container.js index ed8cd5f..17fde8d 100644 --- a/src/core/display/Container.js +++ b/src/core/display/Container.js @@ -73,11 +73,12 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.push(child); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -108,11 +109,12 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.splice(index, 0, child); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -225,10 +227,11 @@ if (index === -1) return null; child.parent = null; + // ensure child transform will be recalculated + child.transform._parentID = -1; removeItems(this.children, index, 1); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -249,11 +252,12 @@ { const child = this.getChildAt(index); + // ensure child transform will be recalculated.. child.parent = null; + child.transform._parentID = -1; removeItems(this.children, index, 1); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -284,13 +288,12 @@ for (let i = 0; i < removed.length; ++i) { removed[i].parent = null; + if (removed[i].transform) + { + removed[i].transform._parentID = -1; + } } - // ensure a transform will be recalculated.. - if (this.transform) - { - this.transform._parentID = -1; - } this._boundsID++; this.onChildrenChange(beginIndex); diff --git a/package.json b/package.json index 6a366ba..71ade15 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pixi.js", - "version": "4.3.4", + "version": "4.3.5", "description": "Pixi.js is a fast lightweight 2D library that works across all devices.", "author": "Mat Groves", "contributors": [ @@ -61,7 +61,7 @@ "ismobilejs": "^0.4.0", "object-assign": "^4.0.1", "pixi-gl-core": "^1.0.3", - "resource-loader": "^2.0.4" + "resource-loader": "^2.0.6" }, "devDependencies": { "babel-cli": "^6.18.0", @@ -70,9 +70,9 @@ "babel-preset-es2015": "^6.14.0", "babelify": "^7.3.0", "del": "^2.2.0", - "electron-prebuilt": "^1.3.2", + "electron": "^1.4.15", "eslint": "^3.5.0", - "floss": "^1.2.0", + "floss": "^2.0.1", "gh-pages": "^0.11.0", "jaguarjs-jsdoc": "^1.0.1", "js-md5": "^0.4.1", diff --git a/src/accessibility/AccessibilityManager.js b/src/accessibility/AccessibilityManager.js index df2716a..f16bcb1 100644 --- a/src/accessibility/AccessibilityManager.js +++ b/src/accessibility/AccessibilityManager.js @@ -3,7 +3,7 @@ import accessibleTarget from './accessibleTarget'; // add some extra variables to the container.. -Object.assign( +core.utils.mixins.delayMixin( core.DisplayObject.prototype, accessibleTarget ); diff --git a/src/core/Application.js b/src/core/Application.js index ea0f058..5340578 100644 --- a/src/core/Application.js +++ b/src/core/Application.js @@ -1,6 +1,6 @@ import { autoDetectRenderer } from './autoDetectRenderer'; import Container from './display/Container'; -import Ticker from './ticker/Ticker'; +import { shared, Ticker } from './ticker'; /** * Convenience class to create a new PIXI application. @@ -35,9 +35,9 @@ * @param {boolean} [noWebGL=false] - prevents selection of WebGL renderer, even if such is present * @param {boolean} [options.legacy=false] - If true Pixi will aim to ensure compatibility * with older / less advanced devices. If you experience unexplained flickering try setting this to true. - + * @param {boolean} [useSharedTicker=false] - `true` to use PIXI.ticker.shared, `false` to create new ticker. */ - constructor(width, height, options, noWebGL) + constructor(width, height, options, noWebGL, useSharedTicker = false) { /** * WebGL renderer if available, otherwise CanvasRenderer @@ -46,23 +46,46 @@ this.renderer = autoDetectRenderer(width, height, options, noWebGL); /** - * The root display container that's renderered. + * The root display container that's rendered. * @member {PIXI.Container} */ this.stage = new Container(); /** + * Internal reference to the ticker + * @member {PIXI.ticker.Ticker} + * @private + */ + this._ticker = null; + + /** * Ticker for doing render updates. * @member {PIXI.ticker.Ticker} + * @default PIXI.ticker.shared */ - this.ticker = new Ticker(); - - this.ticker.add(this.render, this); + this.ticker = useSharedTicker ? shared : new Ticker(); // Start the rendering this.start(); } + set ticker(ticker) // eslint-disable-line require-jsdoc + { + if (this._ticker) + { + this._ticker.remove(this.render, this); + } + this._ticker = ticker; + if (ticker) + { + ticker.add(this.render, this); + } + } + get ticker() // eslint-disable-line require-jsdoc + { + return this._ticker; + } + /** * Render the current stage. */ @@ -76,7 +99,7 @@ */ stop() { - this.ticker.stop(); + this._ticker.stop(); } /** @@ -84,7 +107,7 @@ */ start() { - this.ticker.start(); + this._ticker.start(); } /** @@ -98,13 +121,22 @@ } /** + * Reference to the renderer's screen rectangle. Its safe to use as filterArea or hitArea for whole screen + * @member {PIXI.Rectangle} + * @readonly + */ + get screen() + { + return this.renderer.screen; + } + + /** * Destroy and don't use after this. * @param {Boolean} [removeView=false] Automatically remove canvas from DOM. */ destroy(removeView) { this.stop(); - this.ticker.remove(this.render, this); this.ticker = null; this.stage.destroy(); diff --git a/src/core/Shader.js b/src/core/Shader.js new file mode 100644 index 0000000..78b17e6 --- /dev/null +++ b/src/core/Shader.js @@ -0,0 +1,46 @@ +import { GLShader } from 'pixi-gl-core'; +import settings from './settings'; + +function checkPrecision(src, def) +{ + if (src instanceof Array) + { + if (src[0].substring(0, 9) !== 'precision') + { + const copy = src.slice(0); + + copy.unshift(`precision ${def} float;`); + + return copy; + } + } + else if (src.substring(0, 9) !== 'precision') + { + return `precision ${def} float;\n${src}`; + } + + return src; +} + +/** + * Wrapper class, webGL Shader for Pixi. + * Adds precision string if vertexSrc or fragmentSrc have no mention of it. + * + * @class + * @extends GLShader + * @memberof PIXI + */ +export default class Shader extends GLShader +{ + /** + * + * @param {WebGLRenderingContext} gl - The current WebGL rendering context + * @param {string|string[]} vertexSrc - The vertex shader source as an array of strings. + * @param {string|string[]} fragmentSrc - The fragment shader source as an array of strings. + */ + constructor(gl, vertexSrc, fragmentSrc) + { + super(gl, checkPrecision(vertexSrc, settings.PRECISION_VERTEX), + checkPrecision(fragmentSrc, settings.PRECISION_FRAGMENT)); + } +} diff --git a/src/core/const.js b/src/core/const.js index ef6f662..3de0efe 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -188,7 +188,7 @@ * The gc modes that are supported by pixi. * * The {@link PIXI.settings.GC_MODE} Garbage Collection mode for pixi textures is AUTO - * If set to GC_MODE, the renderer will occasianally check textures usage. If they are not + * If set to GC_MODE, the renderer will occasionally check textures usage. If they are not * used for a specified period of time they will be removed from the GPU. They will of course * be uploaded again when they are required. This is a silent behind the scenes process that * should ensure that the GPU does not get filled up. diff --git a/src/core/display/Container.js b/src/core/display/Container.js index ed8cd5f..17fde8d 100644 --- a/src/core/display/Container.js +++ b/src/core/display/Container.js @@ -73,11 +73,12 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.push(child); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -108,11 +109,12 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.splice(index, 0, child); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -225,10 +227,11 @@ if (index === -1) return null; child.parent = null; + // ensure child transform will be recalculated + child.transform._parentID = -1; removeItems(this.children, index, 1); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -249,11 +252,12 @@ { const child = this.getChildAt(index); + // ensure child transform will be recalculated.. child.parent = null; + child.transform._parentID = -1; removeItems(this.children, index, 1); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -284,13 +288,12 @@ for (let i = 0; i < removed.length; ++i) { removed[i].parent = null; + if (removed[i].transform) + { + removed[i].transform._parentID = -1; + } } - // ensure a transform will be recalculated.. - if (this.transform) - { - this.transform._parentID = -1; - } this._boundsID++; this.onChildrenChange(beginIndex); diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index f37f2f5..be4c4fb 100644 --- a/src/core/graphics/Graphics.js +++ b/src/core/graphics/Graphics.js @@ -28,8 +28,9 @@ { /** * + * @param {boolean} [nativeLines=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP */ - constructor() + constructor(nativeLines = false) { super(); @@ -50,6 +51,13 @@ this.lineWidth = 0; /** + * If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * + * @member {boolean} + */ + this.nativeLines = nativeLines; + + /** * The color of any lines drawn. * * @member {string} @@ -757,28 +765,14 @@ if (!this._spriteRect) { - if (!Graphics._SPRITE_TEXTURE) - { - Graphics._SPRITE_TEXTURE = RenderTexture.create(10, 10); - - const canvas = document.createElement('canvas'); - - canvas.width = 10; - canvas.height = 10; - - const context = canvas.getContext('2d'); - - context.fillStyle = 'white'; - context.fillRect(0, 0, 10, 10); - - Graphics._SPRITE_TEXTURE = Texture.fromCanvas(canvas); - } - - this._spriteRect = new Sprite(Graphics._SPRITE_TEXTURE); + this._spriteRect = new Sprite(new Texture(Texture.WHITE)); } + + const sprite = this._spriteRect; + if (this.tint === 0xffffff) { - this._spriteRect.tint = this.graphicsData[0].fillColor; + sprite.tint = this.graphicsData[0].fillColor; } else { @@ -792,20 +786,21 @@ t1[1] *= t2[1]; t1[2] *= t2[2]; - this._spriteRect.tint = rgb2hex(t1); + sprite.tint = rgb2hex(t1); } - this._spriteRect.alpha = this.graphicsData[0].fillAlpha; - this._spriteRect.worldAlpha = this.worldAlpha * this._spriteRect.alpha; + sprite.alpha = this.graphicsData[0].fillAlpha; + sprite.worldAlpha = this.worldAlpha * sprite.alpha; + sprite.blendMode = this.blendMode; - Graphics._SPRITE_TEXTURE._frame.width = rect.width; - Graphics._SPRITE_TEXTURE._frame.height = rect.height; + sprite.texture._frame.width = rect.width; + sprite.texture._frame.height = rect.height; - this._spriteRect.transform.worldTransform = this.transform.worldTransform; + sprite.transform.worldTransform = this.transform.worldTransform; - this._spriteRect.anchor.set(-rect.x / rect.width, -rect.y / rect.height); - this._spriteRect._onAnchorUpdate(); + sprite.anchor.set(-rect.x / rect.width, -rect.y / rect.height); + sprite._onAnchorUpdate(); - this._spriteRect._renderWebGL(renderer); + sprite._renderWebGL(renderer); } /** @@ -1031,6 +1026,7 @@ this.fillColor, this.fillAlpha, this.filling, + this.nativeLines, shape ); diff --git a/package.json b/package.json index 6a366ba..71ade15 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pixi.js", - "version": "4.3.4", + "version": "4.3.5", "description": "Pixi.js is a fast lightweight 2D library that works across all devices.", "author": "Mat Groves", "contributors": [ @@ -61,7 +61,7 @@ "ismobilejs": "^0.4.0", "object-assign": "^4.0.1", "pixi-gl-core": "^1.0.3", - "resource-loader": "^2.0.4" + "resource-loader": "^2.0.6" }, "devDependencies": { "babel-cli": "^6.18.0", @@ -70,9 +70,9 @@ "babel-preset-es2015": "^6.14.0", "babelify": "^7.3.0", "del": "^2.2.0", - "electron-prebuilt": "^1.3.2", + "electron": "^1.4.15", "eslint": "^3.5.0", - "floss": "^1.2.0", + "floss": "^2.0.1", "gh-pages": "^0.11.0", "jaguarjs-jsdoc": "^1.0.1", "js-md5": "^0.4.1", diff --git a/src/accessibility/AccessibilityManager.js b/src/accessibility/AccessibilityManager.js index df2716a..f16bcb1 100644 --- a/src/accessibility/AccessibilityManager.js +++ b/src/accessibility/AccessibilityManager.js @@ -3,7 +3,7 @@ import accessibleTarget from './accessibleTarget'; // add some extra variables to the container.. -Object.assign( +core.utils.mixins.delayMixin( core.DisplayObject.prototype, accessibleTarget ); diff --git a/src/core/Application.js b/src/core/Application.js index ea0f058..5340578 100644 --- a/src/core/Application.js +++ b/src/core/Application.js @@ -1,6 +1,6 @@ import { autoDetectRenderer } from './autoDetectRenderer'; import Container from './display/Container'; -import Ticker from './ticker/Ticker'; +import { shared, Ticker } from './ticker'; /** * Convenience class to create a new PIXI application. @@ -35,9 +35,9 @@ * @param {boolean} [noWebGL=false] - prevents selection of WebGL renderer, even if such is present * @param {boolean} [options.legacy=false] - If true Pixi will aim to ensure compatibility * with older / less advanced devices. If you experience unexplained flickering try setting this to true. - + * @param {boolean} [useSharedTicker=false] - `true` to use PIXI.ticker.shared, `false` to create new ticker. */ - constructor(width, height, options, noWebGL) + constructor(width, height, options, noWebGL, useSharedTicker = false) { /** * WebGL renderer if available, otherwise CanvasRenderer @@ -46,23 +46,46 @@ this.renderer = autoDetectRenderer(width, height, options, noWebGL); /** - * The root display container that's renderered. + * The root display container that's rendered. * @member {PIXI.Container} */ this.stage = new Container(); /** + * Internal reference to the ticker + * @member {PIXI.ticker.Ticker} + * @private + */ + this._ticker = null; + + /** * Ticker for doing render updates. * @member {PIXI.ticker.Ticker} + * @default PIXI.ticker.shared */ - this.ticker = new Ticker(); - - this.ticker.add(this.render, this); + this.ticker = useSharedTicker ? shared : new Ticker(); // Start the rendering this.start(); } + set ticker(ticker) // eslint-disable-line require-jsdoc + { + if (this._ticker) + { + this._ticker.remove(this.render, this); + } + this._ticker = ticker; + if (ticker) + { + ticker.add(this.render, this); + } + } + get ticker() // eslint-disable-line require-jsdoc + { + return this._ticker; + } + /** * Render the current stage. */ @@ -76,7 +99,7 @@ */ stop() { - this.ticker.stop(); + this._ticker.stop(); } /** @@ -84,7 +107,7 @@ */ start() { - this.ticker.start(); + this._ticker.start(); } /** @@ -98,13 +121,22 @@ } /** + * Reference to the renderer's screen rectangle. Its safe to use as filterArea or hitArea for whole screen + * @member {PIXI.Rectangle} + * @readonly + */ + get screen() + { + return this.renderer.screen; + } + + /** * Destroy and don't use after this. * @param {Boolean} [removeView=false] Automatically remove canvas from DOM. */ destroy(removeView) { this.stop(); - this.ticker.remove(this.render, this); this.ticker = null; this.stage.destroy(); diff --git a/src/core/Shader.js b/src/core/Shader.js new file mode 100644 index 0000000..78b17e6 --- /dev/null +++ b/src/core/Shader.js @@ -0,0 +1,46 @@ +import { GLShader } from 'pixi-gl-core'; +import settings from './settings'; + +function checkPrecision(src, def) +{ + if (src instanceof Array) + { + if (src[0].substring(0, 9) !== 'precision') + { + const copy = src.slice(0); + + copy.unshift(`precision ${def} float;`); + + return copy; + } + } + else if (src.substring(0, 9) !== 'precision') + { + return `precision ${def} float;\n${src}`; + } + + return src; +} + +/** + * Wrapper class, webGL Shader for Pixi. + * Adds precision string if vertexSrc or fragmentSrc have no mention of it. + * + * @class + * @extends GLShader + * @memberof PIXI + */ +export default class Shader extends GLShader +{ + /** + * + * @param {WebGLRenderingContext} gl - The current WebGL rendering context + * @param {string|string[]} vertexSrc - The vertex shader source as an array of strings. + * @param {string|string[]} fragmentSrc - The fragment shader source as an array of strings. + */ + constructor(gl, vertexSrc, fragmentSrc) + { + super(gl, checkPrecision(vertexSrc, settings.PRECISION_VERTEX), + checkPrecision(fragmentSrc, settings.PRECISION_FRAGMENT)); + } +} diff --git a/src/core/const.js b/src/core/const.js index ef6f662..3de0efe 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -188,7 +188,7 @@ * The gc modes that are supported by pixi. * * The {@link PIXI.settings.GC_MODE} Garbage Collection mode for pixi textures is AUTO - * If set to GC_MODE, the renderer will occasianally check textures usage. If they are not + * If set to GC_MODE, the renderer will occasionally check textures usage. If they are not * used for a specified period of time they will be removed from the GPU. They will of course * be uploaded again when they are required. This is a silent behind the scenes process that * should ensure that the GPU does not get filled up. diff --git a/src/core/display/Container.js b/src/core/display/Container.js index ed8cd5f..17fde8d 100644 --- a/src/core/display/Container.js +++ b/src/core/display/Container.js @@ -73,11 +73,12 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.push(child); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -108,11 +109,12 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.splice(index, 0, child); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -225,10 +227,11 @@ if (index === -1) return null; child.parent = null; + // ensure child transform will be recalculated + child.transform._parentID = -1; removeItems(this.children, index, 1); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -249,11 +252,12 @@ { const child = this.getChildAt(index); + // ensure child transform will be recalculated.. child.parent = null; + child.transform._parentID = -1; removeItems(this.children, index, 1); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -284,13 +288,12 @@ for (let i = 0; i < removed.length; ++i) { removed[i].parent = null; + if (removed[i].transform) + { + removed[i].transform._parentID = -1; + } } - // ensure a transform will be recalculated.. - if (this.transform) - { - this.transform._parentID = -1; - } this._boundsID++; this.onChildrenChange(beginIndex); diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index f37f2f5..be4c4fb 100644 --- a/src/core/graphics/Graphics.js +++ b/src/core/graphics/Graphics.js @@ -28,8 +28,9 @@ { /** * + * @param {boolean} [nativeLines=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP */ - constructor() + constructor(nativeLines = false) { super(); @@ -50,6 +51,13 @@ this.lineWidth = 0; /** + * If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * + * @member {boolean} + */ + this.nativeLines = nativeLines; + + /** * The color of any lines drawn. * * @member {string} @@ -757,28 +765,14 @@ if (!this._spriteRect) { - if (!Graphics._SPRITE_TEXTURE) - { - Graphics._SPRITE_TEXTURE = RenderTexture.create(10, 10); - - const canvas = document.createElement('canvas'); - - canvas.width = 10; - canvas.height = 10; - - const context = canvas.getContext('2d'); - - context.fillStyle = 'white'; - context.fillRect(0, 0, 10, 10); - - Graphics._SPRITE_TEXTURE = Texture.fromCanvas(canvas); - } - - this._spriteRect = new Sprite(Graphics._SPRITE_TEXTURE); + this._spriteRect = new Sprite(new Texture(Texture.WHITE)); } + + const sprite = this._spriteRect; + if (this.tint === 0xffffff) { - this._spriteRect.tint = this.graphicsData[0].fillColor; + sprite.tint = this.graphicsData[0].fillColor; } else { @@ -792,20 +786,21 @@ t1[1] *= t2[1]; t1[2] *= t2[2]; - this._spriteRect.tint = rgb2hex(t1); + sprite.tint = rgb2hex(t1); } - this._spriteRect.alpha = this.graphicsData[0].fillAlpha; - this._spriteRect.worldAlpha = this.worldAlpha * this._spriteRect.alpha; + sprite.alpha = this.graphicsData[0].fillAlpha; + sprite.worldAlpha = this.worldAlpha * sprite.alpha; + sprite.blendMode = this.blendMode; - Graphics._SPRITE_TEXTURE._frame.width = rect.width; - Graphics._SPRITE_TEXTURE._frame.height = rect.height; + sprite.texture._frame.width = rect.width; + sprite.texture._frame.height = rect.height; - this._spriteRect.transform.worldTransform = this.transform.worldTransform; + sprite.transform.worldTransform = this.transform.worldTransform; - this._spriteRect.anchor.set(-rect.x / rect.width, -rect.y / rect.height); - this._spriteRect._onAnchorUpdate(); + sprite.anchor.set(-rect.x / rect.width, -rect.y / rect.height); + sprite._onAnchorUpdate(); - this._spriteRect._renderWebGL(renderer); + sprite._renderWebGL(renderer); } /** @@ -1031,6 +1026,7 @@ this.fillColor, this.fillAlpha, this.filling, + this.nativeLines, shape ); diff --git a/src/core/graphics/GraphicsData.js b/src/core/graphics/GraphicsData.js index f5a87ae..b6f03c0 100644 --- a/src/core/graphics/GraphicsData.js +++ b/src/core/graphics/GraphicsData.js @@ -14,14 +14,19 @@ * @param {number} fillColor - the color of the fill * @param {number} fillAlpha - the alpha of the fill * @param {boolean} fill - whether or not the shape is filled with a colour + * @param {boolean} nativeLines - the method for drawing lines * @param {PIXI.Circle|PIXI.Rectangle|PIXI.Ellipse|PIXI.Polygon} shape - The shape object to draw. */ - constructor(lineWidth, lineColor, lineAlpha, fillColor, fillAlpha, fill, shape) + constructor(lineWidth, lineColor, lineAlpha, fillColor, fillAlpha, fill, nativeLines, shape) { /** * @member {number} the width of the line to draw */ this.lineWidth = lineWidth; + /** + * @member {boolean} if true the liens will be draw using LINES instead of TRIANGLE_STRIP + */ + this.nativeLines = nativeLines; /** * @member {number} the color of the line to draw @@ -85,6 +90,7 @@ this.fillColor, this.fillAlpha, this.fill, + this.nativeLines, this.shape ); } diff --git a/package.json b/package.json index 6a366ba..71ade15 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pixi.js", - "version": "4.3.4", + "version": "4.3.5", "description": "Pixi.js is a fast lightweight 2D library that works across all devices.", "author": "Mat Groves", "contributors": [ @@ -61,7 +61,7 @@ "ismobilejs": "^0.4.0", "object-assign": "^4.0.1", "pixi-gl-core": "^1.0.3", - "resource-loader": "^2.0.4" + "resource-loader": "^2.0.6" }, "devDependencies": { "babel-cli": "^6.18.0", @@ -70,9 +70,9 @@ "babel-preset-es2015": "^6.14.0", "babelify": "^7.3.0", "del": "^2.2.0", - "electron-prebuilt": "^1.3.2", + "electron": "^1.4.15", "eslint": "^3.5.0", - "floss": "^1.2.0", + "floss": "^2.0.1", "gh-pages": "^0.11.0", "jaguarjs-jsdoc": "^1.0.1", "js-md5": "^0.4.1", diff --git a/src/accessibility/AccessibilityManager.js b/src/accessibility/AccessibilityManager.js index df2716a..f16bcb1 100644 --- a/src/accessibility/AccessibilityManager.js +++ b/src/accessibility/AccessibilityManager.js @@ -3,7 +3,7 @@ import accessibleTarget from './accessibleTarget'; // add some extra variables to the container.. -Object.assign( +core.utils.mixins.delayMixin( core.DisplayObject.prototype, accessibleTarget ); diff --git a/src/core/Application.js b/src/core/Application.js index ea0f058..5340578 100644 --- a/src/core/Application.js +++ b/src/core/Application.js @@ -1,6 +1,6 @@ import { autoDetectRenderer } from './autoDetectRenderer'; import Container from './display/Container'; -import Ticker from './ticker/Ticker'; +import { shared, Ticker } from './ticker'; /** * Convenience class to create a new PIXI application. @@ -35,9 +35,9 @@ * @param {boolean} [noWebGL=false] - prevents selection of WebGL renderer, even if such is present * @param {boolean} [options.legacy=false] - If true Pixi will aim to ensure compatibility * with older / less advanced devices. If you experience unexplained flickering try setting this to true. - + * @param {boolean} [useSharedTicker=false] - `true` to use PIXI.ticker.shared, `false` to create new ticker. */ - constructor(width, height, options, noWebGL) + constructor(width, height, options, noWebGL, useSharedTicker = false) { /** * WebGL renderer if available, otherwise CanvasRenderer @@ -46,23 +46,46 @@ this.renderer = autoDetectRenderer(width, height, options, noWebGL); /** - * The root display container that's renderered. + * The root display container that's rendered. * @member {PIXI.Container} */ this.stage = new Container(); /** + * Internal reference to the ticker + * @member {PIXI.ticker.Ticker} + * @private + */ + this._ticker = null; + + /** * Ticker for doing render updates. * @member {PIXI.ticker.Ticker} + * @default PIXI.ticker.shared */ - this.ticker = new Ticker(); - - this.ticker.add(this.render, this); + this.ticker = useSharedTicker ? shared : new Ticker(); // Start the rendering this.start(); } + set ticker(ticker) // eslint-disable-line require-jsdoc + { + if (this._ticker) + { + this._ticker.remove(this.render, this); + } + this._ticker = ticker; + if (ticker) + { + ticker.add(this.render, this); + } + } + get ticker() // eslint-disable-line require-jsdoc + { + return this._ticker; + } + /** * Render the current stage. */ @@ -76,7 +99,7 @@ */ stop() { - this.ticker.stop(); + this._ticker.stop(); } /** @@ -84,7 +107,7 @@ */ start() { - this.ticker.start(); + this._ticker.start(); } /** @@ -98,13 +121,22 @@ } /** + * Reference to the renderer's screen rectangle. Its safe to use as filterArea or hitArea for whole screen + * @member {PIXI.Rectangle} + * @readonly + */ + get screen() + { + return this.renderer.screen; + } + + /** * Destroy and don't use after this. * @param {Boolean} [removeView=false] Automatically remove canvas from DOM. */ destroy(removeView) { this.stop(); - this.ticker.remove(this.render, this); this.ticker = null; this.stage.destroy(); diff --git a/src/core/Shader.js b/src/core/Shader.js new file mode 100644 index 0000000..78b17e6 --- /dev/null +++ b/src/core/Shader.js @@ -0,0 +1,46 @@ +import { GLShader } from 'pixi-gl-core'; +import settings from './settings'; + +function checkPrecision(src, def) +{ + if (src instanceof Array) + { + if (src[0].substring(0, 9) !== 'precision') + { + const copy = src.slice(0); + + copy.unshift(`precision ${def} float;`); + + return copy; + } + } + else if (src.substring(0, 9) !== 'precision') + { + return `precision ${def} float;\n${src}`; + } + + return src; +} + +/** + * Wrapper class, webGL Shader for Pixi. + * Adds precision string if vertexSrc or fragmentSrc have no mention of it. + * + * @class + * @extends GLShader + * @memberof PIXI + */ +export default class Shader extends GLShader +{ + /** + * + * @param {WebGLRenderingContext} gl - The current WebGL rendering context + * @param {string|string[]} vertexSrc - The vertex shader source as an array of strings. + * @param {string|string[]} fragmentSrc - The fragment shader source as an array of strings. + */ + constructor(gl, vertexSrc, fragmentSrc) + { + super(gl, checkPrecision(vertexSrc, settings.PRECISION_VERTEX), + checkPrecision(fragmentSrc, settings.PRECISION_FRAGMENT)); + } +} diff --git a/src/core/const.js b/src/core/const.js index ef6f662..3de0efe 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -188,7 +188,7 @@ * The gc modes that are supported by pixi. * * The {@link PIXI.settings.GC_MODE} Garbage Collection mode for pixi textures is AUTO - * If set to GC_MODE, the renderer will occasianally check textures usage. If they are not + * If set to GC_MODE, the renderer will occasionally check textures usage. If they are not * used for a specified period of time they will be removed from the GPU. They will of course * be uploaded again when they are required. This is a silent behind the scenes process that * should ensure that the GPU does not get filled up. diff --git a/src/core/display/Container.js b/src/core/display/Container.js index ed8cd5f..17fde8d 100644 --- a/src/core/display/Container.js +++ b/src/core/display/Container.js @@ -73,11 +73,12 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.push(child); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -108,11 +109,12 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.splice(index, 0, child); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -225,10 +227,11 @@ if (index === -1) return null; child.parent = null; + // ensure child transform will be recalculated + child.transform._parentID = -1; removeItems(this.children, index, 1); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -249,11 +252,12 @@ { const child = this.getChildAt(index); + // ensure child transform will be recalculated.. child.parent = null; + child.transform._parentID = -1; removeItems(this.children, index, 1); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -284,13 +288,12 @@ for (let i = 0; i < removed.length; ++i) { removed[i].parent = null; + if (removed[i].transform) + { + removed[i].transform._parentID = -1; + } } - // ensure a transform will be recalculated.. - if (this.transform) - { - this.transform._parentID = -1; - } this._boundsID++; this.onChildrenChange(beginIndex); diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index f37f2f5..be4c4fb 100644 --- a/src/core/graphics/Graphics.js +++ b/src/core/graphics/Graphics.js @@ -28,8 +28,9 @@ { /** * + * @param {boolean} [nativeLines=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP */ - constructor() + constructor(nativeLines = false) { super(); @@ -50,6 +51,13 @@ this.lineWidth = 0; /** + * If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * + * @member {boolean} + */ + this.nativeLines = nativeLines; + + /** * The color of any lines drawn. * * @member {string} @@ -757,28 +765,14 @@ if (!this._spriteRect) { - if (!Graphics._SPRITE_TEXTURE) - { - Graphics._SPRITE_TEXTURE = RenderTexture.create(10, 10); - - const canvas = document.createElement('canvas'); - - canvas.width = 10; - canvas.height = 10; - - const context = canvas.getContext('2d'); - - context.fillStyle = 'white'; - context.fillRect(0, 0, 10, 10); - - Graphics._SPRITE_TEXTURE = Texture.fromCanvas(canvas); - } - - this._spriteRect = new Sprite(Graphics._SPRITE_TEXTURE); + this._spriteRect = new Sprite(new Texture(Texture.WHITE)); } + + const sprite = this._spriteRect; + if (this.tint === 0xffffff) { - this._spriteRect.tint = this.graphicsData[0].fillColor; + sprite.tint = this.graphicsData[0].fillColor; } else { @@ -792,20 +786,21 @@ t1[1] *= t2[1]; t1[2] *= t2[2]; - this._spriteRect.tint = rgb2hex(t1); + sprite.tint = rgb2hex(t1); } - this._spriteRect.alpha = this.graphicsData[0].fillAlpha; - this._spriteRect.worldAlpha = this.worldAlpha * this._spriteRect.alpha; + sprite.alpha = this.graphicsData[0].fillAlpha; + sprite.worldAlpha = this.worldAlpha * sprite.alpha; + sprite.blendMode = this.blendMode; - Graphics._SPRITE_TEXTURE._frame.width = rect.width; - Graphics._SPRITE_TEXTURE._frame.height = rect.height; + sprite.texture._frame.width = rect.width; + sprite.texture._frame.height = rect.height; - this._spriteRect.transform.worldTransform = this.transform.worldTransform; + sprite.transform.worldTransform = this.transform.worldTransform; - this._spriteRect.anchor.set(-rect.x / rect.width, -rect.y / rect.height); - this._spriteRect._onAnchorUpdate(); + sprite.anchor.set(-rect.x / rect.width, -rect.y / rect.height); + sprite._onAnchorUpdate(); - this._spriteRect._renderWebGL(renderer); + sprite._renderWebGL(renderer); } /** @@ -1031,6 +1026,7 @@ this.fillColor, this.fillAlpha, this.filling, + this.nativeLines, shape ); diff --git a/src/core/graphics/GraphicsData.js b/src/core/graphics/GraphicsData.js index f5a87ae..b6f03c0 100644 --- a/src/core/graphics/GraphicsData.js +++ b/src/core/graphics/GraphicsData.js @@ -14,14 +14,19 @@ * @param {number} fillColor - the color of the fill * @param {number} fillAlpha - the alpha of the fill * @param {boolean} fill - whether or not the shape is filled with a colour + * @param {boolean} nativeLines - the method for drawing lines * @param {PIXI.Circle|PIXI.Rectangle|PIXI.Ellipse|PIXI.Polygon} shape - The shape object to draw. */ - constructor(lineWidth, lineColor, lineAlpha, fillColor, fillAlpha, fill, shape) + constructor(lineWidth, lineColor, lineAlpha, fillColor, fillAlpha, fill, nativeLines, shape) { /** * @member {number} the width of the line to draw */ this.lineWidth = lineWidth; + /** + * @member {boolean} if true the liens will be draw using LINES instead of TRIANGLE_STRIP + */ + this.nativeLines = nativeLines; /** * @member {number} the color of the line to draw @@ -85,6 +90,7 @@ this.fillColor, this.fillAlpha, this.fill, + this.nativeLines, this.shape ); } diff --git a/src/core/graphics/webgl/GraphicsRenderer.js b/src/core/graphics/webgl/GraphicsRenderer.js index 14083fe..67f5e92 100644 --- a/src/core/graphics/webgl/GraphicsRenderer.js +++ b/src/core/graphics/webgl/GraphicsRenderer.js @@ -102,7 +102,15 @@ shaderTemp.uniforms.alpha = graphics.worldAlpha; renderer.bindVao(webGLData.vao); - webGLData.vao.draw(gl.TRIANGLE_STRIP, webGLData.indices.length); + + if (graphics.nativeLines) + { + gl.drawArrays(gl.LINES, 0, webGLData.points.length / 6); + } + else + { + webGLData.vao.draw(gl.TRIANGLE_STRIP, webGLData.indices.length); + } } } diff --git a/package.json b/package.json index 6a366ba..71ade15 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pixi.js", - "version": "4.3.4", + "version": "4.3.5", "description": "Pixi.js is a fast lightweight 2D library that works across all devices.", "author": "Mat Groves", "contributors": [ @@ -61,7 +61,7 @@ "ismobilejs": "^0.4.0", "object-assign": "^4.0.1", "pixi-gl-core": "^1.0.3", - "resource-loader": "^2.0.4" + "resource-loader": "^2.0.6" }, "devDependencies": { "babel-cli": "^6.18.0", @@ -70,9 +70,9 @@ "babel-preset-es2015": "^6.14.0", "babelify": "^7.3.0", "del": "^2.2.0", - "electron-prebuilt": "^1.3.2", + "electron": "^1.4.15", "eslint": "^3.5.0", - "floss": "^1.2.0", + "floss": "^2.0.1", "gh-pages": "^0.11.0", "jaguarjs-jsdoc": "^1.0.1", "js-md5": "^0.4.1", diff --git a/src/accessibility/AccessibilityManager.js b/src/accessibility/AccessibilityManager.js index df2716a..f16bcb1 100644 --- a/src/accessibility/AccessibilityManager.js +++ b/src/accessibility/AccessibilityManager.js @@ -3,7 +3,7 @@ import accessibleTarget from './accessibleTarget'; // add some extra variables to the container.. -Object.assign( +core.utils.mixins.delayMixin( core.DisplayObject.prototype, accessibleTarget ); diff --git a/src/core/Application.js b/src/core/Application.js index ea0f058..5340578 100644 --- a/src/core/Application.js +++ b/src/core/Application.js @@ -1,6 +1,6 @@ import { autoDetectRenderer } from './autoDetectRenderer'; import Container from './display/Container'; -import Ticker from './ticker/Ticker'; +import { shared, Ticker } from './ticker'; /** * Convenience class to create a new PIXI application. @@ -35,9 +35,9 @@ * @param {boolean} [noWebGL=false] - prevents selection of WebGL renderer, even if such is present * @param {boolean} [options.legacy=false] - If true Pixi will aim to ensure compatibility * with older / less advanced devices. If you experience unexplained flickering try setting this to true. - + * @param {boolean} [useSharedTicker=false] - `true` to use PIXI.ticker.shared, `false` to create new ticker. */ - constructor(width, height, options, noWebGL) + constructor(width, height, options, noWebGL, useSharedTicker = false) { /** * WebGL renderer if available, otherwise CanvasRenderer @@ -46,23 +46,46 @@ this.renderer = autoDetectRenderer(width, height, options, noWebGL); /** - * The root display container that's renderered. + * The root display container that's rendered. * @member {PIXI.Container} */ this.stage = new Container(); /** + * Internal reference to the ticker + * @member {PIXI.ticker.Ticker} + * @private + */ + this._ticker = null; + + /** * Ticker for doing render updates. * @member {PIXI.ticker.Ticker} + * @default PIXI.ticker.shared */ - this.ticker = new Ticker(); - - this.ticker.add(this.render, this); + this.ticker = useSharedTicker ? shared : new Ticker(); // Start the rendering this.start(); } + set ticker(ticker) // eslint-disable-line require-jsdoc + { + if (this._ticker) + { + this._ticker.remove(this.render, this); + } + this._ticker = ticker; + if (ticker) + { + ticker.add(this.render, this); + } + } + get ticker() // eslint-disable-line require-jsdoc + { + return this._ticker; + } + /** * Render the current stage. */ @@ -76,7 +99,7 @@ */ stop() { - this.ticker.stop(); + this._ticker.stop(); } /** @@ -84,7 +107,7 @@ */ start() { - this.ticker.start(); + this._ticker.start(); } /** @@ -98,13 +121,22 @@ } /** + * Reference to the renderer's screen rectangle. Its safe to use as filterArea or hitArea for whole screen + * @member {PIXI.Rectangle} + * @readonly + */ + get screen() + { + return this.renderer.screen; + } + + /** * Destroy and don't use after this. * @param {Boolean} [removeView=false] Automatically remove canvas from DOM. */ destroy(removeView) { this.stop(); - this.ticker.remove(this.render, this); this.ticker = null; this.stage.destroy(); diff --git a/src/core/Shader.js b/src/core/Shader.js new file mode 100644 index 0000000..78b17e6 --- /dev/null +++ b/src/core/Shader.js @@ -0,0 +1,46 @@ +import { GLShader } from 'pixi-gl-core'; +import settings from './settings'; + +function checkPrecision(src, def) +{ + if (src instanceof Array) + { + if (src[0].substring(0, 9) !== 'precision') + { + const copy = src.slice(0); + + copy.unshift(`precision ${def} float;`); + + return copy; + } + } + else if (src.substring(0, 9) !== 'precision') + { + return `precision ${def} float;\n${src}`; + } + + return src; +} + +/** + * Wrapper class, webGL Shader for Pixi. + * Adds precision string if vertexSrc or fragmentSrc have no mention of it. + * + * @class + * @extends GLShader + * @memberof PIXI + */ +export default class Shader extends GLShader +{ + /** + * + * @param {WebGLRenderingContext} gl - The current WebGL rendering context + * @param {string|string[]} vertexSrc - The vertex shader source as an array of strings. + * @param {string|string[]} fragmentSrc - The fragment shader source as an array of strings. + */ + constructor(gl, vertexSrc, fragmentSrc) + { + super(gl, checkPrecision(vertexSrc, settings.PRECISION_VERTEX), + checkPrecision(fragmentSrc, settings.PRECISION_FRAGMENT)); + } +} diff --git a/src/core/const.js b/src/core/const.js index ef6f662..3de0efe 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -188,7 +188,7 @@ * The gc modes that are supported by pixi. * * The {@link PIXI.settings.GC_MODE} Garbage Collection mode for pixi textures is AUTO - * If set to GC_MODE, the renderer will occasianally check textures usage. If they are not + * If set to GC_MODE, the renderer will occasionally check textures usage. If they are not * used for a specified period of time they will be removed from the GPU. They will of course * be uploaded again when they are required. This is a silent behind the scenes process that * should ensure that the GPU does not get filled up. diff --git a/src/core/display/Container.js b/src/core/display/Container.js index ed8cd5f..17fde8d 100644 --- a/src/core/display/Container.js +++ b/src/core/display/Container.js @@ -73,11 +73,12 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.push(child); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -108,11 +109,12 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.splice(index, 0, child); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -225,10 +227,11 @@ if (index === -1) return null; child.parent = null; + // ensure child transform will be recalculated + child.transform._parentID = -1; removeItems(this.children, index, 1); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -249,11 +252,12 @@ { const child = this.getChildAt(index); + // ensure child transform will be recalculated.. child.parent = null; + child.transform._parentID = -1; removeItems(this.children, index, 1); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -284,13 +288,12 @@ for (let i = 0; i < removed.length; ++i) { removed[i].parent = null; + if (removed[i].transform) + { + removed[i].transform._parentID = -1; + } } - // ensure a transform will be recalculated.. - if (this.transform) - { - this.transform._parentID = -1; - } this._boundsID++; this.onChildrenChange(beginIndex); diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index f37f2f5..be4c4fb 100644 --- a/src/core/graphics/Graphics.js +++ b/src/core/graphics/Graphics.js @@ -28,8 +28,9 @@ { /** * + * @param {boolean} [nativeLines=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP */ - constructor() + constructor(nativeLines = false) { super(); @@ -50,6 +51,13 @@ this.lineWidth = 0; /** + * If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * + * @member {boolean} + */ + this.nativeLines = nativeLines; + + /** * The color of any lines drawn. * * @member {string} @@ -757,28 +765,14 @@ if (!this._spriteRect) { - if (!Graphics._SPRITE_TEXTURE) - { - Graphics._SPRITE_TEXTURE = RenderTexture.create(10, 10); - - const canvas = document.createElement('canvas'); - - canvas.width = 10; - canvas.height = 10; - - const context = canvas.getContext('2d'); - - context.fillStyle = 'white'; - context.fillRect(0, 0, 10, 10); - - Graphics._SPRITE_TEXTURE = Texture.fromCanvas(canvas); - } - - this._spriteRect = new Sprite(Graphics._SPRITE_TEXTURE); + this._spriteRect = new Sprite(new Texture(Texture.WHITE)); } + + const sprite = this._spriteRect; + if (this.tint === 0xffffff) { - this._spriteRect.tint = this.graphicsData[0].fillColor; + sprite.tint = this.graphicsData[0].fillColor; } else { @@ -792,20 +786,21 @@ t1[1] *= t2[1]; t1[2] *= t2[2]; - this._spriteRect.tint = rgb2hex(t1); + sprite.tint = rgb2hex(t1); } - this._spriteRect.alpha = this.graphicsData[0].fillAlpha; - this._spriteRect.worldAlpha = this.worldAlpha * this._spriteRect.alpha; + sprite.alpha = this.graphicsData[0].fillAlpha; + sprite.worldAlpha = this.worldAlpha * sprite.alpha; + sprite.blendMode = this.blendMode; - Graphics._SPRITE_TEXTURE._frame.width = rect.width; - Graphics._SPRITE_TEXTURE._frame.height = rect.height; + sprite.texture._frame.width = rect.width; + sprite.texture._frame.height = rect.height; - this._spriteRect.transform.worldTransform = this.transform.worldTransform; + sprite.transform.worldTransform = this.transform.worldTransform; - this._spriteRect.anchor.set(-rect.x / rect.width, -rect.y / rect.height); - this._spriteRect._onAnchorUpdate(); + sprite.anchor.set(-rect.x / rect.width, -rect.y / rect.height); + sprite._onAnchorUpdate(); - this._spriteRect._renderWebGL(renderer); + sprite._renderWebGL(renderer); } /** @@ -1031,6 +1026,7 @@ this.fillColor, this.fillAlpha, this.filling, + this.nativeLines, shape ); diff --git a/src/core/graphics/GraphicsData.js b/src/core/graphics/GraphicsData.js index f5a87ae..b6f03c0 100644 --- a/src/core/graphics/GraphicsData.js +++ b/src/core/graphics/GraphicsData.js @@ -14,14 +14,19 @@ * @param {number} fillColor - the color of the fill * @param {number} fillAlpha - the alpha of the fill * @param {boolean} fill - whether or not the shape is filled with a colour + * @param {boolean} nativeLines - the method for drawing lines * @param {PIXI.Circle|PIXI.Rectangle|PIXI.Ellipse|PIXI.Polygon} shape - The shape object to draw. */ - constructor(lineWidth, lineColor, lineAlpha, fillColor, fillAlpha, fill, shape) + constructor(lineWidth, lineColor, lineAlpha, fillColor, fillAlpha, fill, nativeLines, shape) { /** * @member {number} the width of the line to draw */ this.lineWidth = lineWidth; + /** + * @member {boolean} if true the liens will be draw using LINES instead of TRIANGLE_STRIP + */ + this.nativeLines = nativeLines; /** * @member {number} the color of the line to draw @@ -85,6 +90,7 @@ this.fillColor, this.fillAlpha, this.fill, + this.nativeLines, this.shape ); } diff --git a/src/core/graphics/webgl/GraphicsRenderer.js b/src/core/graphics/webgl/GraphicsRenderer.js index 14083fe..67f5e92 100644 --- a/src/core/graphics/webgl/GraphicsRenderer.js +++ b/src/core/graphics/webgl/GraphicsRenderer.js @@ -102,7 +102,15 @@ shaderTemp.uniforms.alpha = graphics.worldAlpha; renderer.bindVao(webGLData.vao); - webGLData.vao.draw(gl.TRIANGLE_STRIP, webGLData.indices.length); + + if (graphics.nativeLines) + { + gl.drawArrays(gl.LINES, 0, webGLData.points.length / 6); + } + else + { + webGLData.vao.draw(gl.TRIANGLE_STRIP, webGLData.indices.length); + } } } diff --git a/src/core/graphics/webgl/utils/buildLine.js b/src/core/graphics/webgl/utils/buildLine.js index ecf43f7..4d6862e 100644 --- a/src/core/graphics/webgl/utils/buildLine.js +++ b/src/core/graphics/webgl/utils/buildLine.js @@ -11,7 +11,29 @@ * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties * @param {object} webGLData - an object containing all the webGL-specific information to create this shape */ -export default function buildLine(graphicsData, webGLData) +export default function (graphicsData, webGLData) +{ + if (graphicsData.nativeLines) + { + buildNativeLine(graphicsData, webGLData); + } + else + { + buildLine(graphicsData, webGLData); + } +} + +/** + * Builds a line to draw using the poligon method. + * + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @private + * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties + * @param {object} webGLData - an object containing all the webGL-specific information to create this shape + */ +function buildLine(graphicsData, webGLData) { // TODO OPTIMISE! let points = graphicsData.points; @@ -224,3 +246,46 @@ indices.push(indexStart - 1); } + +/** + * Builds a line to draw using the gl.drawArrays(gl.LINES) method + * + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @private + * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties + * @param {object} webGLData - an object containing all the webGL-specific information to create this shape + */ +function buildNativeLine(graphicsData, webGLData) +{ + let i = 0; + const points = graphicsData.points; + + if (points.length === 0) return; + + const verts = webGLData.points; + const length = points.length / 2; + + // sort color + const color = hex2rgb(graphicsData.lineColor); + const alpha = graphicsData.lineAlpha; + const r = color[0] * alpha; + const g = color[1] * alpha; + const b = color[2] * alpha; + + for (i = 1; i < length; i++) + { + const p1x = points[(i - 1) * 2]; + const p1y = points[((i - 1) * 2) + 1]; + + const p2x = points[i * 2]; + const p2y = points[(i * 2) + 1]; + + verts.push(p1x, p1y); + verts.push(r, g, b, alpha); + + verts.push(p2x, p2y); + verts.push(r, g, b, alpha); + } +} diff --git a/package.json b/package.json index 6a366ba..71ade15 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pixi.js", - "version": "4.3.4", + "version": "4.3.5", "description": "Pixi.js is a fast lightweight 2D library that works across all devices.", "author": "Mat Groves", "contributors": [ @@ -61,7 +61,7 @@ "ismobilejs": "^0.4.0", "object-assign": "^4.0.1", "pixi-gl-core": "^1.0.3", - "resource-loader": "^2.0.4" + "resource-loader": "^2.0.6" }, "devDependencies": { "babel-cli": "^6.18.0", @@ -70,9 +70,9 @@ "babel-preset-es2015": "^6.14.0", "babelify": "^7.3.0", "del": "^2.2.0", - "electron-prebuilt": "^1.3.2", + "electron": "^1.4.15", "eslint": "^3.5.0", - "floss": "^1.2.0", + "floss": "^2.0.1", "gh-pages": "^0.11.0", "jaguarjs-jsdoc": "^1.0.1", "js-md5": "^0.4.1", diff --git a/src/accessibility/AccessibilityManager.js b/src/accessibility/AccessibilityManager.js index df2716a..f16bcb1 100644 --- a/src/accessibility/AccessibilityManager.js +++ b/src/accessibility/AccessibilityManager.js @@ -3,7 +3,7 @@ import accessibleTarget from './accessibleTarget'; // add some extra variables to the container.. -Object.assign( +core.utils.mixins.delayMixin( core.DisplayObject.prototype, accessibleTarget ); diff --git a/src/core/Application.js b/src/core/Application.js index ea0f058..5340578 100644 --- a/src/core/Application.js +++ b/src/core/Application.js @@ -1,6 +1,6 @@ import { autoDetectRenderer } from './autoDetectRenderer'; import Container from './display/Container'; -import Ticker from './ticker/Ticker'; +import { shared, Ticker } from './ticker'; /** * Convenience class to create a new PIXI application. @@ -35,9 +35,9 @@ * @param {boolean} [noWebGL=false] - prevents selection of WebGL renderer, even if such is present * @param {boolean} [options.legacy=false] - If true Pixi will aim to ensure compatibility * with older / less advanced devices. If you experience unexplained flickering try setting this to true. - + * @param {boolean} [useSharedTicker=false] - `true` to use PIXI.ticker.shared, `false` to create new ticker. */ - constructor(width, height, options, noWebGL) + constructor(width, height, options, noWebGL, useSharedTicker = false) { /** * WebGL renderer if available, otherwise CanvasRenderer @@ -46,23 +46,46 @@ this.renderer = autoDetectRenderer(width, height, options, noWebGL); /** - * The root display container that's renderered. + * The root display container that's rendered. * @member {PIXI.Container} */ this.stage = new Container(); /** + * Internal reference to the ticker + * @member {PIXI.ticker.Ticker} + * @private + */ + this._ticker = null; + + /** * Ticker for doing render updates. * @member {PIXI.ticker.Ticker} + * @default PIXI.ticker.shared */ - this.ticker = new Ticker(); - - this.ticker.add(this.render, this); + this.ticker = useSharedTicker ? shared : new Ticker(); // Start the rendering this.start(); } + set ticker(ticker) // eslint-disable-line require-jsdoc + { + if (this._ticker) + { + this._ticker.remove(this.render, this); + } + this._ticker = ticker; + if (ticker) + { + ticker.add(this.render, this); + } + } + get ticker() // eslint-disable-line require-jsdoc + { + return this._ticker; + } + /** * Render the current stage. */ @@ -76,7 +99,7 @@ */ stop() { - this.ticker.stop(); + this._ticker.stop(); } /** @@ -84,7 +107,7 @@ */ start() { - this.ticker.start(); + this._ticker.start(); } /** @@ -98,13 +121,22 @@ } /** + * Reference to the renderer's screen rectangle. Its safe to use as filterArea or hitArea for whole screen + * @member {PIXI.Rectangle} + * @readonly + */ + get screen() + { + return this.renderer.screen; + } + + /** * Destroy and don't use after this. * @param {Boolean} [removeView=false] Automatically remove canvas from DOM. */ destroy(removeView) { this.stop(); - this.ticker.remove(this.render, this); this.ticker = null; this.stage.destroy(); diff --git a/src/core/Shader.js b/src/core/Shader.js new file mode 100644 index 0000000..78b17e6 --- /dev/null +++ b/src/core/Shader.js @@ -0,0 +1,46 @@ +import { GLShader } from 'pixi-gl-core'; +import settings from './settings'; + +function checkPrecision(src, def) +{ + if (src instanceof Array) + { + if (src[0].substring(0, 9) !== 'precision') + { + const copy = src.slice(0); + + copy.unshift(`precision ${def} float;`); + + return copy; + } + } + else if (src.substring(0, 9) !== 'precision') + { + return `precision ${def} float;\n${src}`; + } + + return src; +} + +/** + * Wrapper class, webGL Shader for Pixi. + * Adds precision string if vertexSrc or fragmentSrc have no mention of it. + * + * @class + * @extends GLShader + * @memberof PIXI + */ +export default class Shader extends GLShader +{ + /** + * + * @param {WebGLRenderingContext} gl - The current WebGL rendering context + * @param {string|string[]} vertexSrc - The vertex shader source as an array of strings. + * @param {string|string[]} fragmentSrc - The fragment shader source as an array of strings. + */ + constructor(gl, vertexSrc, fragmentSrc) + { + super(gl, checkPrecision(vertexSrc, settings.PRECISION_VERTEX), + checkPrecision(fragmentSrc, settings.PRECISION_FRAGMENT)); + } +} diff --git a/src/core/const.js b/src/core/const.js index ef6f662..3de0efe 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -188,7 +188,7 @@ * The gc modes that are supported by pixi. * * The {@link PIXI.settings.GC_MODE} Garbage Collection mode for pixi textures is AUTO - * If set to GC_MODE, the renderer will occasianally check textures usage. If they are not + * If set to GC_MODE, the renderer will occasionally check textures usage. If they are not * used for a specified period of time they will be removed from the GPU. They will of course * be uploaded again when they are required. This is a silent behind the scenes process that * should ensure that the GPU does not get filled up. diff --git a/src/core/display/Container.js b/src/core/display/Container.js index ed8cd5f..17fde8d 100644 --- a/src/core/display/Container.js +++ b/src/core/display/Container.js @@ -73,11 +73,12 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.push(child); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -108,11 +109,12 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.splice(index, 0, child); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -225,10 +227,11 @@ if (index === -1) return null; child.parent = null; + // ensure child transform will be recalculated + child.transform._parentID = -1; removeItems(this.children, index, 1); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -249,11 +252,12 @@ { const child = this.getChildAt(index); + // ensure child transform will be recalculated.. child.parent = null; + child.transform._parentID = -1; removeItems(this.children, index, 1); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -284,13 +288,12 @@ for (let i = 0; i < removed.length; ++i) { removed[i].parent = null; + if (removed[i].transform) + { + removed[i].transform._parentID = -1; + } } - // ensure a transform will be recalculated.. - if (this.transform) - { - this.transform._parentID = -1; - } this._boundsID++; this.onChildrenChange(beginIndex); diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index f37f2f5..be4c4fb 100644 --- a/src/core/graphics/Graphics.js +++ b/src/core/graphics/Graphics.js @@ -28,8 +28,9 @@ { /** * + * @param {boolean} [nativeLines=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP */ - constructor() + constructor(nativeLines = false) { super(); @@ -50,6 +51,13 @@ this.lineWidth = 0; /** + * If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * + * @member {boolean} + */ + this.nativeLines = nativeLines; + + /** * The color of any lines drawn. * * @member {string} @@ -757,28 +765,14 @@ if (!this._spriteRect) { - if (!Graphics._SPRITE_TEXTURE) - { - Graphics._SPRITE_TEXTURE = RenderTexture.create(10, 10); - - const canvas = document.createElement('canvas'); - - canvas.width = 10; - canvas.height = 10; - - const context = canvas.getContext('2d'); - - context.fillStyle = 'white'; - context.fillRect(0, 0, 10, 10); - - Graphics._SPRITE_TEXTURE = Texture.fromCanvas(canvas); - } - - this._spriteRect = new Sprite(Graphics._SPRITE_TEXTURE); + this._spriteRect = new Sprite(new Texture(Texture.WHITE)); } + + const sprite = this._spriteRect; + if (this.tint === 0xffffff) { - this._spriteRect.tint = this.graphicsData[0].fillColor; + sprite.tint = this.graphicsData[0].fillColor; } else { @@ -792,20 +786,21 @@ t1[1] *= t2[1]; t1[2] *= t2[2]; - this._spriteRect.tint = rgb2hex(t1); + sprite.tint = rgb2hex(t1); } - this._spriteRect.alpha = this.graphicsData[0].fillAlpha; - this._spriteRect.worldAlpha = this.worldAlpha * this._spriteRect.alpha; + sprite.alpha = this.graphicsData[0].fillAlpha; + sprite.worldAlpha = this.worldAlpha * sprite.alpha; + sprite.blendMode = this.blendMode; - Graphics._SPRITE_TEXTURE._frame.width = rect.width; - Graphics._SPRITE_TEXTURE._frame.height = rect.height; + sprite.texture._frame.width = rect.width; + sprite.texture._frame.height = rect.height; - this._spriteRect.transform.worldTransform = this.transform.worldTransform; + sprite.transform.worldTransform = this.transform.worldTransform; - this._spriteRect.anchor.set(-rect.x / rect.width, -rect.y / rect.height); - this._spriteRect._onAnchorUpdate(); + sprite.anchor.set(-rect.x / rect.width, -rect.y / rect.height); + sprite._onAnchorUpdate(); - this._spriteRect._renderWebGL(renderer); + sprite._renderWebGL(renderer); } /** @@ -1031,6 +1026,7 @@ this.fillColor, this.fillAlpha, this.filling, + this.nativeLines, shape ); diff --git a/src/core/graphics/GraphicsData.js b/src/core/graphics/GraphicsData.js index f5a87ae..b6f03c0 100644 --- a/src/core/graphics/GraphicsData.js +++ b/src/core/graphics/GraphicsData.js @@ -14,14 +14,19 @@ * @param {number} fillColor - the color of the fill * @param {number} fillAlpha - the alpha of the fill * @param {boolean} fill - whether or not the shape is filled with a colour + * @param {boolean} nativeLines - the method for drawing lines * @param {PIXI.Circle|PIXI.Rectangle|PIXI.Ellipse|PIXI.Polygon} shape - The shape object to draw. */ - constructor(lineWidth, lineColor, lineAlpha, fillColor, fillAlpha, fill, shape) + constructor(lineWidth, lineColor, lineAlpha, fillColor, fillAlpha, fill, nativeLines, shape) { /** * @member {number} the width of the line to draw */ this.lineWidth = lineWidth; + /** + * @member {boolean} if true the liens will be draw using LINES instead of TRIANGLE_STRIP + */ + this.nativeLines = nativeLines; /** * @member {number} the color of the line to draw @@ -85,6 +90,7 @@ this.fillColor, this.fillAlpha, this.fill, + this.nativeLines, this.shape ); } diff --git a/src/core/graphics/webgl/GraphicsRenderer.js b/src/core/graphics/webgl/GraphicsRenderer.js index 14083fe..67f5e92 100644 --- a/src/core/graphics/webgl/GraphicsRenderer.js +++ b/src/core/graphics/webgl/GraphicsRenderer.js @@ -102,7 +102,15 @@ shaderTemp.uniforms.alpha = graphics.worldAlpha; renderer.bindVao(webGLData.vao); - webGLData.vao.draw(gl.TRIANGLE_STRIP, webGLData.indices.length); + + if (graphics.nativeLines) + { + gl.drawArrays(gl.LINES, 0, webGLData.points.length / 6); + } + else + { + webGLData.vao.draw(gl.TRIANGLE_STRIP, webGLData.indices.length); + } } } diff --git a/src/core/graphics/webgl/utils/buildLine.js b/src/core/graphics/webgl/utils/buildLine.js index ecf43f7..4d6862e 100644 --- a/src/core/graphics/webgl/utils/buildLine.js +++ b/src/core/graphics/webgl/utils/buildLine.js @@ -11,7 +11,29 @@ * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties * @param {object} webGLData - an object containing all the webGL-specific information to create this shape */ -export default function buildLine(graphicsData, webGLData) +export default function (graphicsData, webGLData) +{ + if (graphicsData.nativeLines) + { + buildNativeLine(graphicsData, webGLData); + } + else + { + buildLine(graphicsData, webGLData); + } +} + +/** + * Builds a line to draw using the poligon method. + * + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @private + * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties + * @param {object} webGLData - an object containing all the webGL-specific information to create this shape + */ +function buildLine(graphicsData, webGLData) { // TODO OPTIMISE! let points = graphicsData.points; @@ -224,3 +246,46 @@ indices.push(indexStart - 1); } + +/** + * Builds a line to draw using the gl.drawArrays(gl.LINES) method + * + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @private + * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties + * @param {object} webGLData - an object containing all the webGL-specific information to create this shape + */ +function buildNativeLine(graphicsData, webGLData) +{ + let i = 0; + const points = graphicsData.points; + + if (points.length === 0) return; + + const verts = webGLData.points; + const length = points.length / 2; + + // sort color + const color = hex2rgb(graphicsData.lineColor); + const alpha = graphicsData.lineAlpha; + const r = color[0] * alpha; + const g = color[1] * alpha; + const b = color[2] * alpha; + + for (i = 1; i < length; i++) + { + const p1x = points[(i - 1) * 2]; + const p1y = points[((i - 1) * 2) + 1]; + + const p2x = points[i * 2]; + const p2y = points[(i * 2) + 1]; + + verts.push(p1x, p1y); + verts.push(r, g, b, alpha); + + verts.push(p2x, p2y); + verts.push(r, g, b, alpha); + } +} diff --git a/src/core/graphics/webgl/utils/buildRoundedRectangle.js b/src/core/graphics/webgl/utils/buildRoundedRectangle.js index 9abc807..a6c8461 100644 --- a/src/core/graphics/webgl/utils/buildRoundedRectangle.js +++ b/src/core/graphics/webgl/utils/buildRoundedRectangle.js @@ -77,6 +77,26 @@ } /** + * Calculate a single point for a quadratic bezier curve. + * Utility function used by quadraticBezierCurve. + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @private + * @param {number} n1 - first number + * @param {number} n2 - second number + * @param {number} perc - percentage + * @return {number} the result + * + */ +function getPt(n1, n2, perc) +{ + const diff = n2 - n1; + + return n1 + (diff * perc); +} + +/** * Calculate the points for a quadratic bezier curve. (helper function..) * Based on: https://stackoverflow.com/questions/785097/how-do-i-implement-a-bezier-curve-in-c * @@ -105,13 +125,6 @@ let x = 0; let y = 0; - function getPt(n1, n2, perc) - { - const diff = n2 - n1; - - return n1 + (diff * perc); - } - for (let i = 0, j = 0; i <= n; ++i) { j = i / n; diff --git a/package.json b/package.json index 6a366ba..71ade15 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pixi.js", - "version": "4.3.4", + "version": "4.3.5", "description": "Pixi.js is a fast lightweight 2D library that works across all devices.", "author": "Mat Groves", "contributors": [ @@ -61,7 +61,7 @@ "ismobilejs": "^0.4.0", "object-assign": "^4.0.1", "pixi-gl-core": "^1.0.3", - "resource-loader": "^2.0.4" + "resource-loader": "^2.0.6" }, "devDependencies": { "babel-cli": "^6.18.0", @@ -70,9 +70,9 @@ "babel-preset-es2015": "^6.14.0", "babelify": "^7.3.0", "del": "^2.2.0", - "electron-prebuilt": "^1.3.2", + "electron": "^1.4.15", "eslint": "^3.5.0", - "floss": "^1.2.0", + "floss": "^2.0.1", "gh-pages": "^0.11.0", "jaguarjs-jsdoc": "^1.0.1", "js-md5": "^0.4.1", diff --git a/src/accessibility/AccessibilityManager.js b/src/accessibility/AccessibilityManager.js index df2716a..f16bcb1 100644 --- a/src/accessibility/AccessibilityManager.js +++ b/src/accessibility/AccessibilityManager.js @@ -3,7 +3,7 @@ import accessibleTarget from './accessibleTarget'; // add some extra variables to the container.. -Object.assign( +core.utils.mixins.delayMixin( core.DisplayObject.prototype, accessibleTarget ); diff --git a/src/core/Application.js b/src/core/Application.js index ea0f058..5340578 100644 --- a/src/core/Application.js +++ b/src/core/Application.js @@ -1,6 +1,6 @@ import { autoDetectRenderer } from './autoDetectRenderer'; import Container from './display/Container'; -import Ticker from './ticker/Ticker'; +import { shared, Ticker } from './ticker'; /** * Convenience class to create a new PIXI application. @@ -35,9 +35,9 @@ * @param {boolean} [noWebGL=false] - prevents selection of WebGL renderer, even if such is present * @param {boolean} [options.legacy=false] - If true Pixi will aim to ensure compatibility * with older / less advanced devices. If you experience unexplained flickering try setting this to true. - + * @param {boolean} [useSharedTicker=false] - `true` to use PIXI.ticker.shared, `false` to create new ticker. */ - constructor(width, height, options, noWebGL) + constructor(width, height, options, noWebGL, useSharedTicker = false) { /** * WebGL renderer if available, otherwise CanvasRenderer @@ -46,23 +46,46 @@ this.renderer = autoDetectRenderer(width, height, options, noWebGL); /** - * The root display container that's renderered. + * The root display container that's rendered. * @member {PIXI.Container} */ this.stage = new Container(); /** + * Internal reference to the ticker + * @member {PIXI.ticker.Ticker} + * @private + */ + this._ticker = null; + + /** * Ticker for doing render updates. * @member {PIXI.ticker.Ticker} + * @default PIXI.ticker.shared */ - this.ticker = new Ticker(); - - this.ticker.add(this.render, this); + this.ticker = useSharedTicker ? shared : new Ticker(); // Start the rendering this.start(); } + set ticker(ticker) // eslint-disable-line require-jsdoc + { + if (this._ticker) + { + this._ticker.remove(this.render, this); + } + this._ticker = ticker; + if (ticker) + { + ticker.add(this.render, this); + } + } + get ticker() // eslint-disable-line require-jsdoc + { + return this._ticker; + } + /** * Render the current stage. */ @@ -76,7 +99,7 @@ */ stop() { - this.ticker.stop(); + this._ticker.stop(); } /** @@ -84,7 +107,7 @@ */ start() { - this.ticker.start(); + this._ticker.start(); } /** @@ -98,13 +121,22 @@ } /** + * Reference to the renderer's screen rectangle. Its safe to use as filterArea or hitArea for whole screen + * @member {PIXI.Rectangle} + * @readonly + */ + get screen() + { + return this.renderer.screen; + } + + /** * Destroy and don't use after this. * @param {Boolean} [removeView=false] Automatically remove canvas from DOM. */ destroy(removeView) { this.stop(); - this.ticker.remove(this.render, this); this.ticker = null; this.stage.destroy(); diff --git a/src/core/Shader.js b/src/core/Shader.js new file mode 100644 index 0000000..78b17e6 --- /dev/null +++ b/src/core/Shader.js @@ -0,0 +1,46 @@ +import { GLShader } from 'pixi-gl-core'; +import settings from './settings'; + +function checkPrecision(src, def) +{ + if (src instanceof Array) + { + if (src[0].substring(0, 9) !== 'precision') + { + const copy = src.slice(0); + + copy.unshift(`precision ${def} float;`); + + return copy; + } + } + else if (src.substring(0, 9) !== 'precision') + { + return `precision ${def} float;\n${src}`; + } + + return src; +} + +/** + * Wrapper class, webGL Shader for Pixi. + * Adds precision string if vertexSrc or fragmentSrc have no mention of it. + * + * @class + * @extends GLShader + * @memberof PIXI + */ +export default class Shader extends GLShader +{ + /** + * + * @param {WebGLRenderingContext} gl - The current WebGL rendering context + * @param {string|string[]} vertexSrc - The vertex shader source as an array of strings. + * @param {string|string[]} fragmentSrc - The fragment shader source as an array of strings. + */ + constructor(gl, vertexSrc, fragmentSrc) + { + super(gl, checkPrecision(vertexSrc, settings.PRECISION_VERTEX), + checkPrecision(fragmentSrc, settings.PRECISION_FRAGMENT)); + } +} diff --git a/src/core/const.js b/src/core/const.js index ef6f662..3de0efe 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -188,7 +188,7 @@ * The gc modes that are supported by pixi. * * The {@link PIXI.settings.GC_MODE} Garbage Collection mode for pixi textures is AUTO - * If set to GC_MODE, the renderer will occasianally check textures usage. If they are not + * If set to GC_MODE, the renderer will occasionally check textures usage. If they are not * used for a specified period of time they will be removed from the GPU. They will of course * be uploaded again when they are required. This is a silent behind the scenes process that * should ensure that the GPU does not get filled up. diff --git a/src/core/display/Container.js b/src/core/display/Container.js index ed8cd5f..17fde8d 100644 --- a/src/core/display/Container.js +++ b/src/core/display/Container.js @@ -73,11 +73,12 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.push(child); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -108,11 +109,12 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.splice(index, 0, child); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -225,10 +227,11 @@ if (index === -1) return null; child.parent = null; + // ensure child transform will be recalculated + child.transform._parentID = -1; removeItems(this.children, index, 1); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -249,11 +252,12 @@ { const child = this.getChildAt(index); + // ensure child transform will be recalculated.. child.parent = null; + child.transform._parentID = -1; removeItems(this.children, index, 1); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -284,13 +288,12 @@ for (let i = 0; i < removed.length; ++i) { removed[i].parent = null; + if (removed[i].transform) + { + removed[i].transform._parentID = -1; + } } - // ensure a transform will be recalculated.. - if (this.transform) - { - this.transform._parentID = -1; - } this._boundsID++; this.onChildrenChange(beginIndex); diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index f37f2f5..be4c4fb 100644 --- a/src/core/graphics/Graphics.js +++ b/src/core/graphics/Graphics.js @@ -28,8 +28,9 @@ { /** * + * @param {boolean} [nativeLines=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP */ - constructor() + constructor(nativeLines = false) { super(); @@ -50,6 +51,13 @@ this.lineWidth = 0; /** + * If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * + * @member {boolean} + */ + this.nativeLines = nativeLines; + + /** * The color of any lines drawn. * * @member {string} @@ -757,28 +765,14 @@ if (!this._spriteRect) { - if (!Graphics._SPRITE_TEXTURE) - { - Graphics._SPRITE_TEXTURE = RenderTexture.create(10, 10); - - const canvas = document.createElement('canvas'); - - canvas.width = 10; - canvas.height = 10; - - const context = canvas.getContext('2d'); - - context.fillStyle = 'white'; - context.fillRect(0, 0, 10, 10); - - Graphics._SPRITE_TEXTURE = Texture.fromCanvas(canvas); - } - - this._spriteRect = new Sprite(Graphics._SPRITE_TEXTURE); + this._spriteRect = new Sprite(new Texture(Texture.WHITE)); } + + const sprite = this._spriteRect; + if (this.tint === 0xffffff) { - this._spriteRect.tint = this.graphicsData[0].fillColor; + sprite.tint = this.graphicsData[0].fillColor; } else { @@ -792,20 +786,21 @@ t1[1] *= t2[1]; t1[2] *= t2[2]; - this._spriteRect.tint = rgb2hex(t1); + sprite.tint = rgb2hex(t1); } - this._spriteRect.alpha = this.graphicsData[0].fillAlpha; - this._spriteRect.worldAlpha = this.worldAlpha * this._spriteRect.alpha; + sprite.alpha = this.graphicsData[0].fillAlpha; + sprite.worldAlpha = this.worldAlpha * sprite.alpha; + sprite.blendMode = this.blendMode; - Graphics._SPRITE_TEXTURE._frame.width = rect.width; - Graphics._SPRITE_TEXTURE._frame.height = rect.height; + sprite.texture._frame.width = rect.width; + sprite.texture._frame.height = rect.height; - this._spriteRect.transform.worldTransform = this.transform.worldTransform; + sprite.transform.worldTransform = this.transform.worldTransform; - this._spriteRect.anchor.set(-rect.x / rect.width, -rect.y / rect.height); - this._spriteRect._onAnchorUpdate(); + sprite.anchor.set(-rect.x / rect.width, -rect.y / rect.height); + sprite._onAnchorUpdate(); - this._spriteRect._renderWebGL(renderer); + sprite._renderWebGL(renderer); } /** @@ -1031,6 +1026,7 @@ this.fillColor, this.fillAlpha, this.filling, + this.nativeLines, shape ); diff --git a/src/core/graphics/GraphicsData.js b/src/core/graphics/GraphicsData.js index f5a87ae..b6f03c0 100644 --- a/src/core/graphics/GraphicsData.js +++ b/src/core/graphics/GraphicsData.js @@ -14,14 +14,19 @@ * @param {number} fillColor - the color of the fill * @param {number} fillAlpha - the alpha of the fill * @param {boolean} fill - whether or not the shape is filled with a colour + * @param {boolean} nativeLines - the method for drawing lines * @param {PIXI.Circle|PIXI.Rectangle|PIXI.Ellipse|PIXI.Polygon} shape - The shape object to draw. */ - constructor(lineWidth, lineColor, lineAlpha, fillColor, fillAlpha, fill, shape) + constructor(lineWidth, lineColor, lineAlpha, fillColor, fillAlpha, fill, nativeLines, shape) { /** * @member {number} the width of the line to draw */ this.lineWidth = lineWidth; + /** + * @member {boolean} if true the liens will be draw using LINES instead of TRIANGLE_STRIP + */ + this.nativeLines = nativeLines; /** * @member {number} the color of the line to draw @@ -85,6 +90,7 @@ this.fillColor, this.fillAlpha, this.fill, + this.nativeLines, this.shape ); } diff --git a/src/core/graphics/webgl/GraphicsRenderer.js b/src/core/graphics/webgl/GraphicsRenderer.js index 14083fe..67f5e92 100644 --- a/src/core/graphics/webgl/GraphicsRenderer.js +++ b/src/core/graphics/webgl/GraphicsRenderer.js @@ -102,7 +102,15 @@ shaderTemp.uniforms.alpha = graphics.worldAlpha; renderer.bindVao(webGLData.vao); - webGLData.vao.draw(gl.TRIANGLE_STRIP, webGLData.indices.length); + + if (graphics.nativeLines) + { + gl.drawArrays(gl.LINES, 0, webGLData.points.length / 6); + } + else + { + webGLData.vao.draw(gl.TRIANGLE_STRIP, webGLData.indices.length); + } } } diff --git a/src/core/graphics/webgl/utils/buildLine.js b/src/core/graphics/webgl/utils/buildLine.js index ecf43f7..4d6862e 100644 --- a/src/core/graphics/webgl/utils/buildLine.js +++ b/src/core/graphics/webgl/utils/buildLine.js @@ -11,7 +11,29 @@ * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties * @param {object} webGLData - an object containing all the webGL-specific information to create this shape */ -export default function buildLine(graphicsData, webGLData) +export default function (graphicsData, webGLData) +{ + if (graphicsData.nativeLines) + { + buildNativeLine(graphicsData, webGLData); + } + else + { + buildLine(graphicsData, webGLData); + } +} + +/** + * Builds a line to draw using the poligon method. + * + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @private + * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties + * @param {object} webGLData - an object containing all the webGL-specific information to create this shape + */ +function buildLine(graphicsData, webGLData) { // TODO OPTIMISE! let points = graphicsData.points; @@ -224,3 +246,46 @@ indices.push(indexStart - 1); } + +/** + * Builds a line to draw using the gl.drawArrays(gl.LINES) method + * + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @private + * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties + * @param {object} webGLData - an object containing all the webGL-specific information to create this shape + */ +function buildNativeLine(graphicsData, webGLData) +{ + let i = 0; + const points = graphicsData.points; + + if (points.length === 0) return; + + const verts = webGLData.points; + const length = points.length / 2; + + // sort color + const color = hex2rgb(graphicsData.lineColor); + const alpha = graphicsData.lineAlpha; + const r = color[0] * alpha; + const g = color[1] * alpha; + const b = color[2] * alpha; + + for (i = 1; i < length; i++) + { + const p1x = points[(i - 1) * 2]; + const p1y = points[((i - 1) * 2) + 1]; + + const p2x = points[i * 2]; + const p2y = points[(i * 2) + 1]; + + verts.push(p1x, p1y); + verts.push(r, g, b, alpha); + + verts.push(p2x, p2y); + verts.push(r, g, b, alpha); + } +} diff --git a/src/core/graphics/webgl/utils/buildRoundedRectangle.js b/src/core/graphics/webgl/utils/buildRoundedRectangle.js index 9abc807..a6c8461 100644 --- a/src/core/graphics/webgl/utils/buildRoundedRectangle.js +++ b/src/core/graphics/webgl/utils/buildRoundedRectangle.js @@ -77,6 +77,26 @@ } /** + * Calculate a single point for a quadratic bezier curve. + * Utility function used by quadraticBezierCurve. + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @private + * @param {number} n1 - first number + * @param {number} n2 - second number + * @param {number} perc - percentage + * @return {number} the result + * + */ +function getPt(n1, n2, perc) +{ + const diff = n2 - n1; + + return n1 + (diff * perc); +} + +/** * Calculate the points for a quadratic bezier curve. (helper function..) * Based on: https://stackoverflow.com/questions/785097/how-do-i-implement-a-bezier-curve-in-c * @@ -105,13 +125,6 @@ let x = 0; let y = 0; - function getPt(n1, n2, perc) - { - const diff = n2 - n1; - - return n1 + (diff * perc); - } - for (let i = 0, j = 0; i <= n; ++i) { j = i / n; diff --git a/src/core/index.js b/src/core/index.js index f7a817c..4f414e8 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -30,6 +30,7 @@ export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; export { default as CanvasGraphicsRenderer } from './graphics/canvas/CanvasGraphicsRenderer'; +export { default as Spritesheet } from './textures/Spritesheet'; export { default as Texture } from './textures/Texture'; export { default as BaseTexture } from './textures/BaseTexture'; export { default as RenderTexture } from './textures/RenderTexture'; diff --git a/package.json b/package.json index 6a366ba..71ade15 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pixi.js", - "version": "4.3.4", + "version": "4.3.5", "description": "Pixi.js is a fast lightweight 2D library that works across all devices.", "author": "Mat Groves", "contributors": [ @@ -61,7 +61,7 @@ "ismobilejs": "^0.4.0", "object-assign": "^4.0.1", "pixi-gl-core": "^1.0.3", - "resource-loader": "^2.0.4" + "resource-loader": "^2.0.6" }, "devDependencies": { "babel-cli": "^6.18.0", @@ -70,9 +70,9 @@ "babel-preset-es2015": "^6.14.0", "babelify": "^7.3.0", "del": "^2.2.0", - "electron-prebuilt": "^1.3.2", + "electron": "^1.4.15", "eslint": "^3.5.0", - "floss": "^1.2.0", + "floss": "^2.0.1", "gh-pages": "^0.11.0", "jaguarjs-jsdoc": "^1.0.1", "js-md5": "^0.4.1", diff --git a/src/accessibility/AccessibilityManager.js b/src/accessibility/AccessibilityManager.js index df2716a..f16bcb1 100644 --- a/src/accessibility/AccessibilityManager.js +++ b/src/accessibility/AccessibilityManager.js @@ -3,7 +3,7 @@ import accessibleTarget from './accessibleTarget'; // add some extra variables to the container.. -Object.assign( +core.utils.mixins.delayMixin( core.DisplayObject.prototype, accessibleTarget ); diff --git a/src/core/Application.js b/src/core/Application.js index ea0f058..5340578 100644 --- a/src/core/Application.js +++ b/src/core/Application.js @@ -1,6 +1,6 @@ import { autoDetectRenderer } from './autoDetectRenderer'; import Container from './display/Container'; -import Ticker from './ticker/Ticker'; +import { shared, Ticker } from './ticker'; /** * Convenience class to create a new PIXI application. @@ -35,9 +35,9 @@ * @param {boolean} [noWebGL=false] - prevents selection of WebGL renderer, even if such is present * @param {boolean} [options.legacy=false] - If true Pixi will aim to ensure compatibility * with older / less advanced devices. If you experience unexplained flickering try setting this to true. - + * @param {boolean} [useSharedTicker=false] - `true` to use PIXI.ticker.shared, `false` to create new ticker. */ - constructor(width, height, options, noWebGL) + constructor(width, height, options, noWebGL, useSharedTicker = false) { /** * WebGL renderer if available, otherwise CanvasRenderer @@ -46,23 +46,46 @@ this.renderer = autoDetectRenderer(width, height, options, noWebGL); /** - * The root display container that's renderered. + * The root display container that's rendered. * @member {PIXI.Container} */ this.stage = new Container(); /** + * Internal reference to the ticker + * @member {PIXI.ticker.Ticker} + * @private + */ + this._ticker = null; + + /** * Ticker for doing render updates. * @member {PIXI.ticker.Ticker} + * @default PIXI.ticker.shared */ - this.ticker = new Ticker(); - - this.ticker.add(this.render, this); + this.ticker = useSharedTicker ? shared : new Ticker(); // Start the rendering this.start(); } + set ticker(ticker) // eslint-disable-line require-jsdoc + { + if (this._ticker) + { + this._ticker.remove(this.render, this); + } + this._ticker = ticker; + if (ticker) + { + ticker.add(this.render, this); + } + } + get ticker() // eslint-disable-line require-jsdoc + { + return this._ticker; + } + /** * Render the current stage. */ @@ -76,7 +99,7 @@ */ stop() { - this.ticker.stop(); + this._ticker.stop(); } /** @@ -84,7 +107,7 @@ */ start() { - this.ticker.start(); + this._ticker.start(); } /** @@ -98,13 +121,22 @@ } /** + * Reference to the renderer's screen rectangle. Its safe to use as filterArea or hitArea for whole screen + * @member {PIXI.Rectangle} + * @readonly + */ + get screen() + { + return this.renderer.screen; + } + + /** * Destroy and don't use after this. * @param {Boolean} [removeView=false] Automatically remove canvas from DOM. */ destroy(removeView) { this.stop(); - this.ticker.remove(this.render, this); this.ticker = null; this.stage.destroy(); diff --git a/src/core/Shader.js b/src/core/Shader.js new file mode 100644 index 0000000..78b17e6 --- /dev/null +++ b/src/core/Shader.js @@ -0,0 +1,46 @@ +import { GLShader } from 'pixi-gl-core'; +import settings from './settings'; + +function checkPrecision(src, def) +{ + if (src instanceof Array) + { + if (src[0].substring(0, 9) !== 'precision') + { + const copy = src.slice(0); + + copy.unshift(`precision ${def} float;`); + + return copy; + } + } + else if (src.substring(0, 9) !== 'precision') + { + return `precision ${def} float;\n${src}`; + } + + return src; +} + +/** + * Wrapper class, webGL Shader for Pixi. + * Adds precision string if vertexSrc or fragmentSrc have no mention of it. + * + * @class + * @extends GLShader + * @memberof PIXI + */ +export default class Shader extends GLShader +{ + /** + * + * @param {WebGLRenderingContext} gl - The current WebGL rendering context + * @param {string|string[]} vertexSrc - The vertex shader source as an array of strings. + * @param {string|string[]} fragmentSrc - The fragment shader source as an array of strings. + */ + constructor(gl, vertexSrc, fragmentSrc) + { + super(gl, checkPrecision(vertexSrc, settings.PRECISION_VERTEX), + checkPrecision(fragmentSrc, settings.PRECISION_FRAGMENT)); + } +} diff --git a/src/core/const.js b/src/core/const.js index ef6f662..3de0efe 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -188,7 +188,7 @@ * The gc modes that are supported by pixi. * * The {@link PIXI.settings.GC_MODE} Garbage Collection mode for pixi textures is AUTO - * If set to GC_MODE, the renderer will occasianally check textures usage. If they are not + * If set to GC_MODE, the renderer will occasionally check textures usage. If they are not * used for a specified period of time they will be removed from the GPU. They will of course * be uploaded again when they are required. This is a silent behind the scenes process that * should ensure that the GPU does not get filled up. diff --git a/src/core/display/Container.js b/src/core/display/Container.js index ed8cd5f..17fde8d 100644 --- a/src/core/display/Container.js +++ b/src/core/display/Container.js @@ -73,11 +73,12 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.push(child); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -108,11 +109,12 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.splice(index, 0, child); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -225,10 +227,11 @@ if (index === -1) return null; child.parent = null; + // ensure child transform will be recalculated + child.transform._parentID = -1; removeItems(this.children, index, 1); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -249,11 +252,12 @@ { const child = this.getChildAt(index); + // ensure child transform will be recalculated.. child.parent = null; + child.transform._parentID = -1; removeItems(this.children, index, 1); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -284,13 +288,12 @@ for (let i = 0; i < removed.length; ++i) { removed[i].parent = null; + if (removed[i].transform) + { + removed[i].transform._parentID = -1; + } } - // ensure a transform will be recalculated.. - if (this.transform) - { - this.transform._parentID = -1; - } this._boundsID++; this.onChildrenChange(beginIndex); diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index f37f2f5..be4c4fb 100644 --- a/src/core/graphics/Graphics.js +++ b/src/core/graphics/Graphics.js @@ -28,8 +28,9 @@ { /** * + * @param {boolean} [nativeLines=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP */ - constructor() + constructor(nativeLines = false) { super(); @@ -50,6 +51,13 @@ this.lineWidth = 0; /** + * If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * + * @member {boolean} + */ + this.nativeLines = nativeLines; + + /** * The color of any lines drawn. * * @member {string} @@ -757,28 +765,14 @@ if (!this._spriteRect) { - if (!Graphics._SPRITE_TEXTURE) - { - Graphics._SPRITE_TEXTURE = RenderTexture.create(10, 10); - - const canvas = document.createElement('canvas'); - - canvas.width = 10; - canvas.height = 10; - - const context = canvas.getContext('2d'); - - context.fillStyle = 'white'; - context.fillRect(0, 0, 10, 10); - - Graphics._SPRITE_TEXTURE = Texture.fromCanvas(canvas); - } - - this._spriteRect = new Sprite(Graphics._SPRITE_TEXTURE); + this._spriteRect = new Sprite(new Texture(Texture.WHITE)); } + + const sprite = this._spriteRect; + if (this.tint === 0xffffff) { - this._spriteRect.tint = this.graphicsData[0].fillColor; + sprite.tint = this.graphicsData[0].fillColor; } else { @@ -792,20 +786,21 @@ t1[1] *= t2[1]; t1[2] *= t2[2]; - this._spriteRect.tint = rgb2hex(t1); + sprite.tint = rgb2hex(t1); } - this._spriteRect.alpha = this.graphicsData[0].fillAlpha; - this._spriteRect.worldAlpha = this.worldAlpha * this._spriteRect.alpha; + sprite.alpha = this.graphicsData[0].fillAlpha; + sprite.worldAlpha = this.worldAlpha * sprite.alpha; + sprite.blendMode = this.blendMode; - Graphics._SPRITE_TEXTURE._frame.width = rect.width; - Graphics._SPRITE_TEXTURE._frame.height = rect.height; + sprite.texture._frame.width = rect.width; + sprite.texture._frame.height = rect.height; - this._spriteRect.transform.worldTransform = this.transform.worldTransform; + sprite.transform.worldTransform = this.transform.worldTransform; - this._spriteRect.anchor.set(-rect.x / rect.width, -rect.y / rect.height); - this._spriteRect._onAnchorUpdate(); + sprite.anchor.set(-rect.x / rect.width, -rect.y / rect.height); + sprite._onAnchorUpdate(); - this._spriteRect._renderWebGL(renderer); + sprite._renderWebGL(renderer); } /** @@ -1031,6 +1026,7 @@ this.fillColor, this.fillAlpha, this.filling, + this.nativeLines, shape ); diff --git a/src/core/graphics/GraphicsData.js b/src/core/graphics/GraphicsData.js index f5a87ae..b6f03c0 100644 --- a/src/core/graphics/GraphicsData.js +++ b/src/core/graphics/GraphicsData.js @@ -14,14 +14,19 @@ * @param {number} fillColor - the color of the fill * @param {number} fillAlpha - the alpha of the fill * @param {boolean} fill - whether or not the shape is filled with a colour + * @param {boolean} nativeLines - the method for drawing lines * @param {PIXI.Circle|PIXI.Rectangle|PIXI.Ellipse|PIXI.Polygon} shape - The shape object to draw. */ - constructor(lineWidth, lineColor, lineAlpha, fillColor, fillAlpha, fill, shape) + constructor(lineWidth, lineColor, lineAlpha, fillColor, fillAlpha, fill, nativeLines, shape) { /** * @member {number} the width of the line to draw */ this.lineWidth = lineWidth; + /** + * @member {boolean} if true the liens will be draw using LINES instead of TRIANGLE_STRIP + */ + this.nativeLines = nativeLines; /** * @member {number} the color of the line to draw @@ -85,6 +90,7 @@ this.fillColor, this.fillAlpha, this.fill, + this.nativeLines, this.shape ); } diff --git a/src/core/graphics/webgl/GraphicsRenderer.js b/src/core/graphics/webgl/GraphicsRenderer.js index 14083fe..67f5e92 100644 --- a/src/core/graphics/webgl/GraphicsRenderer.js +++ b/src/core/graphics/webgl/GraphicsRenderer.js @@ -102,7 +102,15 @@ shaderTemp.uniforms.alpha = graphics.worldAlpha; renderer.bindVao(webGLData.vao); - webGLData.vao.draw(gl.TRIANGLE_STRIP, webGLData.indices.length); + + if (graphics.nativeLines) + { + gl.drawArrays(gl.LINES, 0, webGLData.points.length / 6); + } + else + { + webGLData.vao.draw(gl.TRIANGLE_STRIP, webGLData.indices.length); + } } } diff --git a/src/core/graphics/webgl/utils/buildLine.js b/src/core/graphics/webgl/utils/buildLine.js index ecf43f7..4d6862e 100644 --- a/src/core/graphics/webgl/utils/buildLine.js +++ b/src/core/graphics/webgl/utils/buildLine.js @@ -11,7 +11,29 @@ * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties * @param {object} webGLData - an object containing all the webGL-specific information to create this shape */ -export default function buildLine(graphicsData, webGLData) +export default function (graphicsData, webGLData) +{ + if (graphicsData.nativeLines) + { + buildNativeLine(graphicsData, webGLData); + } + else + { + buildLine(graphicsData, webGLData); + } +} + +/** + * Builds a line to draw using the poligon method. + * + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @private + * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties + * @param {object} webGLData - an object containing all the webGL-specific information to create this shape + */ +function buildLine(graphicsData, webGLData) { // TODO OPTIMISE! let points = graphicsData.points; @@ -224,3 +246,46 @@ indices.push(indexStart - 1); } + +/** + * Builds a line to draw using the gl.drawArrays(gl.LINES) method + * + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @private + * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties + * @param {object} webGLData - an object containing all the webGL-specific information to create this shape + */ +function buildNativeLine(graphicsData, webGLData) +{ + let i = 0; + const points = graphicsData.points; + + if (points.length === 0) return; + + const verts = webGLData.points; + const length = points.length / 2; + + // sort color + const color = hex2rgb(graphicsData.lineColor); + const alpha = graphicsData.lineAlpha; + const r = color[0] * alpha; + const g = color[1] * alpha; + const b = color[2] * alpha; + + for (i = 1; i < length; i++) + { + const p1x = points[(i - 1) * 2]; + const p1y = points[((i - 1) * 2) + 1]; + + const p2x = points[i * 2]; + const p2y = points[(i * 2) + 1]; + + verts.push(p1x, p1y); + verts.push(r, g, b, alpha); + + verts.push(p2x, p2y); + verts.push(r, g, b, alpha); + } +} diff --git a/src/core/graphics/webgl/utils/buildRoundedRectangle.js b/src/core/graphics/webgl/utils/buildRoundedRectangle.js index 9abc807..a6c8461 100644 --- a/src/core/graphics/webgl/utils/buildRoundedRectangle.js +++ b/src/core/graphics/webgl/utils/buildRoundedRectangle.js @@ -77,6 +77,26 @@ } /** + * Calculate a single point for a quadratic bezier curve. + * Utility function used by quadraticBezierCurve. + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @private + * @param {number} n1 - first number + * @param {number} n2 - second number + * @param {number} perc - percentage + * @return {number} the result + * + */ +function getPt(n1, n2, perc) +{ + const diff = n2 - n1; + + return n1 + (diff * perc); +} + +/** * Calculate the points for a quadratic bezier curve. (helper function..) * Based on: https://stackoverflow.com/questions/785097/how-do-i-implement-a-bezier-curve-in-c * @@ -105,13 +125,6 @@ let x = 0; let y = 0; - function getPt(n1, n2, perc) - { - const diff = n2 - n1; - - return n1 + (diff * perc); - } - for (let i = 0, j = 0; i <= n; ++i) { j = i / n; diff --git a/src/core/index.js b/src/core/index.js index f7a817c..4f414e8 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -30,6 +30,7 @@ export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; export { default as CanvasGraphicsRenderer } from './graphics/canvas/CanvasGraphicsRenderer'; +export { default as Spritesheet } from './textures/Spritesheet'; export { default as Texture } from './textures/Texture'; export { default as BaseTexture } from './textures/BaseTexture'; export { default as RenderTexture } from './textures/RenderTexture'; diff --git a/src/core/math/Matrix.js b/src/core/math/Matrix.js index 0b77477..3679860 100644 --- a/src/core/math/Matrix.js +++ b/src/core/math/Matrix.js @@ -13,45 +13,50 @@ export default class Matrix { /** - * + * @param {number} [a=1] - x scale + * @param {number} [b=0] - y skew + * @param {number} [c=0] - x skew + * @param {number} [d=1] - y scale + * @param {number} [tx=0] - x translation + * @param {number} [ty=0] - y translation */ - constructor() + constructor(a = 1, b = 0, c = 0, d = 1, tx = 0, ty = 0) { /** * @member {number} * @default 1 */ - this.a = 1; + this.a = a; /** * @member {number} * @default 0 */ - this.b = 0; + this.b = b; /** * @member {number} * @default 0 */ - this.c = 0; + this.c = c; /** * @member {number} * @default 1 */ - this.d = 1; + this.d = d; /** * @member {number} * @default 0 */ - this.tx = 0; + this.tx = tx; /** * @member {number} * @default 0 */ - this.ty = 0; + this.ty = ty; this.array = null; } diff --git a/package.json b/package.json index 6a366ba..71ade15 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pixi.js", - "version": "4.3.4", + "version": "4.3.5", "description": "Pixi.js is a fast lightweight 2D library that works across all devices.", "author": "Mat Groves", "contributors": [ @@ -61,7 +61,7 @@ "ismobilejs": "^0.4.0", "object-assign": "^4.0.1", "pixi-gl-core": "^1.0.3", - "resource-loader": "^2.0.4" + "resource-loader": "^2.0.6" }, "devDependencies": { "babel-cli": "^6.18.0", @@ -70,9 +70,9 @@ "babel-preset-es2015": "^6.14.0", "babelify": "^7.3.0", "del": "^2.2.0", - "electron-prebuilt": "^1.3.2", + "electron": "^1.4.15", "eslint": "^3.5.0", - "floss": "^1.2.0", + "floss": "^2.0.1", "gh-pages": "^0.11.0", "jaguarjs-jsdoc": "^1.0.1", "js-md5": "^0.4.1", diff --git a/src/accessibility/AccessibilityManager.js b/src/accessibility/AccessibilityManager.js index df2716a..f16bcb1 100644 --- a/src/accessibility/AccessibilityManager.js +++ b/src/accessibility/AccessibilityManager.js @@ -3,7 +3,7 @@ import accessibleTarget from './accessibleTarget'; // add some extra variables to the container.. -Object.assign( +core.utils.mixins.delayMixin( core.DisplayObject.prototype, accessibleTarget ); diff --git a/src/core/Application.js b/src/core/Application.js index ea0f058..5340578 100644 --- a/src/core/Application.js +++ b/src/core/Application.js @@ -1,6 +1,6 @@ import { autoDetectRenderer } from './autoDetectRenderer'; import Container from './display/Container'; -import Ticker from './ticker/Ticker'; +import { shared, Ticker } from './ticker'; /** * Convenience class to create a new PIXI application. @@ -35,9 +35,9 @@ * @param {boolean} [noWebGL=false] - prevents selection of WebGL renderer, even if such is present * @param {boolean} [options.legacy=false] - If true Pixi will aim to ensure compatibility * with older / less advanced devices. If you experience unexplained flickering try setting this to true. - + * @param {boolean} [useSharedTicker=false] - `true` to use PIXI.ticker.shared, `false` to create new ticker. */ - constructor(width, height, options, noWebGL) + constructor(width, height, options, noWebGL, useSharedTicker = false) { /** * WebGL renderer if available, otherwise CanvasRenderer @@ -46,23 +46,46 @@ this.renderer = autoDetectRenderer(width, height, options, noWebGL); /** - * The root display container that's renderered. + * The root display container that's rendered. * @member {PIXI.Container} */ this.stage = new Container(); /** + * Internal reference to the ticker + * @member {PIXI.ticker.Ticker} + * @private + */ + this._ticker = null; + + /** * Ticker for doing render updates. * @member {PIXI.ticker.Ticker} + * @default PIXI.ticker.shared */ - this.ticker = new Ticker(); - - this.ticker.add(this.render, this); + this.ticker = useSharedTicker ? shared : new Ticker(); // Start the rendering this.start(); } + set ticker(ticker) // eslint-disable-line require-jsdoc + { + if (this._ticker) + { + this._ticker.remove(this.render, this); + } + this._ticker = ticker; + if (ticker) + { + ticker.add(this.render, this); + } + } + get ticker() // eslint-disable-line require-jsdoc + { + return this._ticker; + } + /** * Render the current stage. */ @@ -76,7 +99,7 @@ */ stop() { - this.ticker.stop(); + this._ticker.stop(); } /** @@ -84,7 +107,7 @@ */ start() { - this.ticker.start(); + this._ticker.start(); } /** @@ -98,13 +121,22 @@ } /** + * Reference to the renderer's screen rectangle. Its safe to use as filterArea or hitArea for whole screen + * @member {PIXI.Rectangle} + * @readonly + */ + get screen() + { + return this.renderer.screen; + } + + /** * Destroy and don't use after this. * @param {Boolean} [removeView=false] Automatically remove canvas from DOM. */ destroy(removeView) { this.stop(); - this.ticker.remove(this.render, this); this.ticker = null; this.stage.destroy(); diff --git a/src/core/Shader.js b/src/core/Shader.js new file mode 100644 index 0000000..78b17e6 --- /dev/null +++ b/src/core/Shader.js @@ -0,0 +1,46 @@ +import { GLShader } from 'pixi-gl-core'; +import settings from './settings'; + +function checkPrecision(src, def) +{ + if (src instanceof Array) + { + if (src[0].substring(0, 9) !== 'precision') + { + const copy = src.slice(0); + + copy.unshift(`precision ${def} float;`); + + return copy; + } + } + else if (src.substring(0, 9) !== 'precision') + { + return `precision ${def} float;\n${src}`; + } + + return src; +} + +/** + * Wrapper class, webGL Shader for Pixi. + * Adds precision string if vertexSrc or fragmentSrc have no mention of it. + * + * @class + * @extends GLShader + * @memberof PIXI + */ +export default class Shader extends GLShader +{ + /** + * + * @param {WebGLRenderingContext} gl - The current WebGL rendering context + * @param {string|string[]} vertexSrc - The vertex shader source as an array of strings. + * @param {string|string[]} fragmentSrc - The fragment shader source as an array of strings. + */ + constructor(gl, vertexSrc, fragmentSrc) + { + super(gl, checkPrecision(vertexSrc, settings.PRECISION_VERTEX), + checkPrecision(fragmentSrc, settings.PRECISION_FRAGMENT)); + } +} diff --git a/src/core/const.js b/src/core/const.js index ef6f662..3de0efe 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -188,7 +188,7 @@ * The gc modes that are supported by pixi. * * The {@link PIXI.settings.GC_MODE} Garbage Collection mode for pixi textures is AUTO - * If set to GC_MODE, the renderer will occasianally check textures usage. If they are not + * If set to GC_MODE, the renderer will occasionally check textures usage. If they are not * used for a specified period of time they will be removed from the GPU. They will of course * be uploaded again when they are required. This is a silent behind the scenes process that * should ensure that the GPU does not get filled up. diff --git a/src/core/display/Container.js b/src/core/display/Container.js index ed8cd5f..17fde8d 100644 --- a/src/core/display/Container.js +++ b/src/core/display/Container.js @@ -73,11 +73,12 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.push(child); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -108,11 +109,12 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.splice(index, 0, child); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -225,10 +227,11 @@ if (index === -1) return null; child.parent = null; + // ensure child transform will be recalculated + child.transform._parentID = -1; removeItems(this.children, index, 1); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -249,11 +252,12 @@ { const child = this.getChildAt(index); + // ensure child transform will be recalculated.. child.parent = null; + child.transform._parentID = -1; removeItems(this.children, index, 1); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -284,13 +288,12 @@ for (let i = 0; i < removed.length; ++i) { removed[i].parent = null; + if (removed[i].transform) + { + removed[i].transform._parentID = -1; + } } - // ensure a transform will be recalculated.. - if (this.transform) - { - this.transform._parentID = -1; - } this._boundsID++; this.onChildrenChange(beginIndex); diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index f37f2f5..be4c4fb 100644 --- a/src/core/graphics/Graphics.js +++ b/src/core/graphics/Graphics.js @@ -28,8 +28,9 @@ { /** * + * @param {boolean} [nativeLines=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP */ - constructor() + constructor(nativeLines = false) { super(); @@ -50,6 +51,13 @@ this.lineWidth = 0; /** + * If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * + * @member {boolean} + */ + this.nativeLines = nativeLines; + + /** * The color of any lines drawn. * * @member {string} @@ -757,28 +765,14 @@ if (!this._spriteRect) { - if (!Graphics._SPRITE_TEXTURE) - { - Graphics._SPRITE_TEXTURE = RenderTexture.create(10, 10); - - const canvas = document.createElement('canvas'); - - canvas.width = 10; - canvas.height = 10; - - const context = canvas.getContext('2d'); - - context.fillStyle = 'white'; - context.fillRect(0, 0, 10, 10); - - Graphics._SPRITE_TEXTURE = Texture.fromCanvas(canvas); - } - - this._spriteRect = new Sprite(Graphics._SPRITE_TEXTURE); + this._spriteRect = new Sprite(new Texture(Texture.WHITE)); } + + const sprite = this._spriteRect; + if (this.tint === 0xffffff) { - this._spriteRect.tint = this.graphicsData[0].fillColor; + sprite.tint = this.graphicsData[0].fillColor; } else { @@ -792,20 +786,21 @@ t1[1] *= t2[1]; t1[2] *= t2[2]; - this._spriteRect.tint = rgb2hex(t1); + sprite.tint = rgb2hex(t1); } - this._spriteRect.alpha = this.graphicsData[0].fillAlpha; - this._spriteRect.worldAlpha = this.worldAlpha * this._spriteRect.alpha; + sprite.alpha = this.graphicsData[0].fillAlpha; + sprite.worldAlpha = this.worldAlpha * sprite.alpha; + sprite.blendMode = this.blendMode; - Graphics._SPRITE_TEXTURE._frame.width = rect.width; - Graphics._SPRITE_TEXTURE._frame.height = rect.height; + sprite.texture._frame.width = rect.width; + sprite.texture._frame.height = rect.height; - this._spriteRect.transform.worldTransform = this.transform.worldTransform; + sprite.transform.worldTransform = this.transform.worldTransform; - this._spriteRect.anchor.set(-rect.x / rect.width, -rect.y / rect.height); - this._spriteRect._onAnchorUpdate(); + sprite.anchor.set(-rect.x / rect.width, -rect.y / rect.height); + sprite._onAnchorUpdate(); - this._spriteRect._renderWebGL(renderer); + sprite._renderWebGL(renderer); } /** @@ -1031,6 +1026,7 @@ this.fillColor, this.fillAlpha, this.filling, + this.nativeLines, shape ); diff --git a/src/core/graphics/GraphicsData.js b/src/core/graphics/GraphicsData.js index f5a87ae..b6f03c0 100644 --- a/src/core/graphics/GraphicsData.js +++ b/src/core/graphics/GraphicsData.js @@ -14,14 +14,19 @@ * @param {number} fillColor - the color of the fill * @param {number} fillAlpha - the alpha of the fill * @param {boolean} fill - whether or not the shape is filled with a colour + * @param {boolean} nativeLines - the method for drawing lines * @param {PIXI.Circle|PIXI.Rectangle|PIXI.Ellipse|PIXI.Polygon} shape - The shape object to draw. */ - constructor(lineWidth, lineColor, lineAlpha, fillColor, fillAlpha, fill, shape) + constructor(lineWidth, lineColor, lineAlpha, fillColor, fillAlpha, fill, nativeLines, shape) { /** * @member {number} the width of the line to draw */ this.lineWidth = lineWidth; + /** + * @member {boolean} if true the liens will be draw using LINES instead of TRIANGLE_STRIP + */ + this.nativeLines = nativeLines; /** * @member {number} the color of the line to draw @@ -85,6 +90,7 @@ this.fillColor, this.fillAlpha, this.fill, + this.nativeLines, this.shape ); } diff --git a/src/core/graphics/webgl/GraphicsRenderer.js b/src/core/graphics/webgl/GraphicsRenderer.js index 14083fe..67f5e92 100644 --- a/src/core/graphics/webgl/GraphicsRenderer.js +++ b/src/core/graphics/webgl/GraphicsRenderer.js @@ -102,7 +102,15 @@ shaderTemp.uniforms.alpha = graphics.worldAlpha; renderer.bindVao(webGLData.vao); - webGLData.vao.draw(gl.TRIANGLE_STRIP, webGLData.indices.length); + + if (graphics.nativeLines) + { + gl.drawArrays(gl.LINES, 0, webGLData.points.length / 6); + } + else + { + webGLData.vao.draw(gl.TRIANGLE_STRIP, webGLData.indices.length); + } } } diff --git a/src/core/graphics/webgl/utils/buildLine.js b/src/core/graphics/webgl/utils/buildLine.js index ecf43f7..4d6862e 100644 --- a/src/core/graphics/webgl/utils/buildLine.js +++ b/src/core/graphics/webgl/utils/buildLine.js @@ -11,7 +11,29 @@ * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties * @param {object} webGLData - an object containing all the webGL-specific information to create this shape */ -export default function buildLine(graphicsData, webGLData) +export default function (graphicsData, webGLData) +{ + if (graphicsData.nativeLines) + { + buildNativeLine(graphicsData, webGLData); + } + else + { + buildLine(graphicsData, webGLData); + } +} + +/** + * Builds a line to draw using the poligon method. + * + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @private + * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties + * @param {object} webGLData - an object containing all the webGL-specific information to create this shape + */ +function buildLine(graphicsData, webGLData) { // TODO OPTIMISE! let points = graphicsData.points; @@ -224,3 +246,46 @@ indices.push(indexStart - 1); } + +/** + * Builds a line to draw using the gl.drawArrays(gl.LINES) method + * + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @private + * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties + * @param {object} webGLData - an object containing all the webGL-specific information to create this shape + */ +function buildNativeLine(graphicsData, webGLData) +{ + let i = 0; + const points = graphicsData.points; + + if (points.length === 0) return; + + const verts = webGLData.points; + const length = points.length / 2; + + // sort color + const color = hex2rgb(graphicsData.lineColor); + const alpha = graphicsData.lineAlpha; + const r = color[0] * alpha; + const g = color[1] * alpha; + const b = color[2] * alpha; + + for (i = 1; i < length; i++) + { + const p1x = points[(i - 1) * 2]; + const p1y = points[((i - 1) * 2) + 1]; + + const p2x = points[i * 2]; + const p2y = points[(i * 2) + 1]; + + verts.push(p1x, p1y); + verts.push(r, g, b, alpha); + + verts.push(p2x, p2y); + verts.push(r, g, b, alpha); + } +} diff --git a/src/core/graphics/webgl/utils/buildRoundedRectangle.js b/src/core/graphics/webgl/utils/buildRoundedRectangle.js index 9abc807..a6c8461 100644 --- a/src/core/graphics/webgl/utils/buildRoundedRectangle.js +++ b/src/core/graphics/webgl/utils/buildRoundedRectangle.js @@ -77,6 +77,26 @@ } /** + * Calculate a single point for a quadratic bezier curve. + * Utility function used by quadraticBezierCurve. + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @private + * @param {number} n1 - first number + * @param {number} n2 - second number + * @param {number} perc - percentage + * @return {number} the result + * + */ +function getPt(n1, n2, perc) +{ + const diff = n2 - n1; + + return n1 + (diff * perc); +} + +/** * Calculate the points for a quadratic bezier curve. (helper function..) * Based on: https://stackoverflow.com/questions/785097/how-do-i-implement-a-bezier-curve-in-c * @@ -105,13 +125,6 @@ let x = 0; let y = 0; - function getPt(n1, n2, perc) - { - const diff = n2 - n1; - - return n1 + (diff * perc); - } - for (let i = 0, j = 0; i <= n; ++i) { j = i / n; diff --git a/src/core/index.js b/src/core/index.js index f7a817c..4f414e8 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -30,6 +30,7 @@ export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; export { default as CanvasGraphicsRenderer } from './graphics/canvas/CanvasGraphicsRenderer'; +export { default as Spritesheet } from './textures/Spritesheet'; export { default as Texture } from './textures/Texture'; export { default as BaseTexture } from './textures/BaseTexture'; export { default as RenderTexture } from './textures/RenderTexture'; diff --git a/src/core/math/Matrix.js b/src/core/math/Matrix.js index 0b77477..3679860 100644 --- a/src/core/math/Matrix.js +++ b/src/core/math/Matrix.js @@ -13,45 +13,50 @@ export default class Matrix { /** - * + * @param {number} [a=1] - x scale + * @param {number} [b=0] - y skew + * @param {number} [c=0] - x skew + * @param {number} [d=1] - y scale + * @param {number} [tx=0] - x translation + * @param {number} [ty=0] - y translation */ - constructor() + constructor(a = 1, b = 0, c = 0, d = 1, tx = 0, ty = 0) { /** * @member {number} * @default 1 */ - this.a = 1; + this.a = a; /** * @member {number} * @default 0 */ - this.b = 0; + this.b = b; /** * @member {number} * @default 0 */ - this.c = 0; + this.c = c; /** * @member {number} * @default 1 */ - this.d = 1; + this.d = d; /** * @member {number} * @default 0 */ - this.tx = 0; + this.tx = tx; /** * @member {number} * @default 0 */ - this.ty = 0; + this.ty = ty; this.array = null; } diff --git a/src/core/renderers/SystemRenderer.js b/src/core/renderers/SystemRenderer.js index fa116a6..a24c775 100644 --- a/src/core/renderers/SystemRenderer.js +++ b/src/core/renderers/SystemRenderer.js @@ -1,5 +1,5 @@ import { sayHello, hex2string, hex2rgb } from '../utils'; -import { Matrix } from '../math'; +import { Matrix, Rectangle } from '../math'; import { RENDERER_TYPE } from '../const'; import settings from '../settings'; import Container from '../display/Container'; @@ -21,8 +21,8 @@ { /** * @param {string} system - The name of the system this renderer is for. - * @param {number} [width=800] - the width of the canvas view - * @param {number} [height=600] - the height of the canvas view + * @param {number} [screenWidth=800] - the width of the screen + * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false @@ -37,7 +37,7 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(system, width, height, options) + constructor(system, screenWidth, screenHeight, options) { super(); @@ -69,20 +69,13 @@ this.type = RENDERER_TYPE.UNKNOWN; /** - * The width of the canvas view + * Measurements of the screen. (0, 0, screenWidth, screenHeight) * - * @member {number} - * @default 800 - */ - this.width = width || 800; - - /** - * The height of the canvas view + * Its safe to use as filterArea or hitArea for whole stage * - * @member {number} - * @default 600 + * @member {PIXI.Rectangle} */ - this.height = height || 600; + this.screen = new Rectangle(0, 0, screenWidth || 800, screenHeight || 600); /** * The canvas element that everything is drawn to @@ -107,7 +100,7 @@ this.transparent = options.transparent; /** - * Whether the render view should be resized automatically + * Whether css dimensions of canvas view should be resized to screen dimensions automatically * * @member {boolean} */ @@ -192,23 +185,48 @@ } /** - * Resizes the canvas view to the specified width and height + * Same as view.width, actual number of pixels in the canvas by horizontal * - * @param {number} width - the new width of the canvas view - * @param {number} height - the new height of the canvas view + * @member {number} + * @readonly + * @default 800 */ - resize(width, height) + get width() { - this.width = width * this.resolution; - this.height = height * this.resolution; + return this.view.width; + } - this.view.width = this.width; - this.view.height = this.height; + /** + * Same as view.height, actual number of pixels in the canvas by vertical + * + * @member {number} + * @readonly + * @default 600 + */ + get height() + { + return this.view.height; + } + + /** + * Resizes the screen and canvas to the specified width and height + * Canvas dimensions are multiplied by resolution + * + * @param {number} screenWidth - the new width of the screen + * @param {number} screenHeight - the new height of the screen + */ + resize(screenWidth, screenHeight) + { + this.screen.width = screenWidth; + this.screen.height = screenHeight; + + this.view.width = screenWidth * this.resolution; + this.view.height = screenHeight * this.resolution; if (this.autoResize) { - this.view.style.width = `${this.width / this.resolution}px`; - this.view.style.height = `${this.height / this.resolution}px`; + this.view.style.width = `${screenWidth}px`; + this.view.style.height = `${screenHeight}px`; } } @@ -249,11 +267,10 @@ this.type = RENDERER_TYPE.UNKNOWN; - this.width = 0; - this.height = 0; - this.view = null; + this.screen = null; + this.resolution = 0; this.transparent = false; diff --git a/package.json b/package.json index 6a366ba..71ade15 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pixi.js", - "version": "4.3.4", + "version": "4.3.5", "description": "Pixi.js is a fast lightweight 2D library that works across all devices.", "author": "Mat Groves", "contributors": [ @@ -61,7 +61,7 @@ "ismobilejs": "^0.4.0", "object-assign": "^4.0.1", "pixi-gl-core": "^1.0.3", - "resource-loader": "^2.0.4" + "resource-loader": "^2.0.6" }, "devDependencies": { "babel-cli": "^6.18.0", @@ -70,9 +70,9 @@ "babel-preset-es2015": "^6.14.0", "babelify": "^7.3.0", "del": "^2.2.0", - "electron-prebuilt": "^1.3.2", + "electron": "^1.4.15", "eslint": "^3.5.0", - "floss": "^1.2.0", + "floss": "^2.0.1", "gh-pages": "^0.11.0", "jaguarjs-jsdoc": "^1.0.1", "js-md5": "^0.4.1", diff --git a/src/accessibility/AccessibilityManager.js b/src/accessibility/AccessibilityManager.js index df2716a..f16bcb1 100644 --- a/src/accessibility/AccessibilityManager.js +++ b/src/accessibility/AccessibilityManager.js @@ -3,7 +3,7 @@ import accessibleTarget from './accessibleTarget'; // add some extra variables to the container.. -Object.assign( +core.utils.mixins.delayMixin( core.DisplayObject.prototype, accessibleTarget ); diff --git a/src/core/Application.js b/src/core/Application.js index ea0f058..5340578 100644 --- a/src/core/Application.js +++ b/src/core/Application.js @@ -1,6 +1,6 @@ import { autoDetectRenderer } from './autoDetectRenderer'; import Container from './display/Container'; -import Ticker from './ticker/Ticker'; +import { shared, Ticker } from './ticker'; /** * Convenience class to create a new PIXI application. @@ -35,9 +35,9 @@ * @param {boolean} [noWebGL=false] - prevents selection of WebGL renderer, even if such is present * @param {boolean} [options.legacy=false] - If true Pixi will aim to ensure compatibility * with older / less advanced devices. If you experience unexplained flickering try setting this to true. - + * @param {boolean} [useSharedTicker=false] - `true` to use PIXI.ticker.shared, `false` to create new ticker. */ - constructor(width, height, options, noWebGL) + constructor(width, height, options, noWebGL, useSharedTicker = false) { /** * WebGL renderer if available, otherwise CanvasRenderer @@ -46,23 +46,46 @@ this.renderer = autoDetectRenderer(width, height, options, noWebGL); /** - * The root display container that's renderered. + * The root display container that's rendered. * @member {PIXI.Container} */ this.stage = new Container(); /** + * Internal reference to the ticker + * @member {PIXI.ticker.Ticker} + * @private + */ + this._ticker = null; + + /** * Ticker for doing render updates. * @member {PIXI.ticker.Ticker} + * @default PIXI.ticker.shared */ - this.ticker = new Ticker(); - - this.ticker.add(this.render, this); + this.ticker = useSharedTicker ? shared : new Ticker(); // Start the rendering this.start(); } + set ticker(ticker) // eslint-disable-line require-jsdoc + { + if (this._ticker) + { + this._ticker.remove(this.render, this); + } + this._ticker = ticker; + if (ticker) + { + ticker.add(this.render, this); + } + } + get ticker() // eslint-disable-line require-jsdoc + { + return this._ticker; + } + /** * Render the current stage. */ @@ -76,7 +99,7 @@ */ stop() { - this.ticker.stop(); + this._ticker.stop(); } /** @@ -84,7 +107,7 @@ */ start() { - this.ticker.start(); + this._ticker.start(); } /** @@ -98,13 +121,22 @@ } /** + * Reference to the renderer's screen rectangle. Its safe to use as filterArea or hitArea for whole screen + * @member {PIXI.Rectangle} + * @readonly + */ + get screen() + { + return this.renderer.screen; + } + + /** * Destroy and don't use after this. * @param {Boolean} [removeView=false] Automatically remove canvas from DOM. */ destroy(removeView) { this.stop(); - this.ticker.remove(this.render, this); this.ticker = null; this.stage.destroy(); diff --git a/src/core/Shader.js b/src/core/Shader.js new file mode 100644 index 0000000..78b17e6 --- /dev/null +++ b/src/core/Shader.js @@ -0,0 +1,46 @@ +import { GLShader } from 'pixi-gl-core'; +import settings from './settings'; + +function checkPrecision(src, def) +{ + if (src instanceof Array) + { + if (src[0].substring(0, 9) !== 'precision') + { + const copy = src.slice(0); + + copy.unshift(`precision ${def} float;`); + + return copy; + } + } + else if (src.substring(0, 9) !== 'precision') + { + return `precision ${def} float;\n${src}`; + } + + return src; +} + +/** + * Wrapper class, webGL Shader for Pixi. + * Adds precision string if vertexSrc or fragmentSrc have no mention of it. + * + * @class + * @extends GLShader + * @memberof PIXI + */ +export default class Shader extends GLShader +{ + /** + * + * @param {WebGLRenderingContext} gl - The current WebGL rendering context + * @param {string|string[]} vertexSrc - The vertex shader source as an array of strings. + * @param {string|string[]} fragmentSrc - The fragment shader source as an array of strings. + */ + constructor(gl, vertexSrc, fragmentSrc) + { + super(gl, checkPrecision(vertexSrc, settings.PRECISION_VERTEX), + checkPrecision(fragmentSrc, settings.PRECISION_FRAGMENT)); + } +} diff --git a/src/core/const.js b/src/core/const.js index ef6f662..3de0efe 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -188,7 +188,7 @@ * The gc modes that are supported by pixi. * * The {@link PIXI.settings.GC_MODE} Garbage Collection mode for pixi textures is AUTO - * If set to GC_MODE, the renderer will occasianally check textures usage. If they are not + * If set to GC_MODE, the renderer will occasionally check textures usage. If they are not * used for a specified period of time they will be removed from the GPU. They will of course * be uploaded again when they are required. This is a silent behind the scenes process that * should ensure that the GPU does not get filled up. diff --git a/src/core/display/Container.js b/src/core/display/Container.js index ed8cd5f..17fde8d 100644 --- a/src/core/display/Container.js +++ b/src/core/display/Container.js @@ -73,11 +73,12 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.push(child); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -108,11 +109,12 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.splice(index, 0, child); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -225,10 +227,11 @@ if (index === -1) return null; child.parent = null; + // ensure child transform will be recalculated + child.transform._parentID = -1; removeItems(this.children, index, 1); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -249,11 +252,12 @@ { const child = this.getChildAt(index); + // ensure child transform will be recalculated.. child.parent = null; + child.transform._parentID = -1; removeItems(this.children, index, 1); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -284,13 +288,12 @@ for (let i = 0; i < removed.length; ++i) { removed[i].parent = null; + if (removed[i].transform) + { + removed[i].transform._parentID = -1; + } } - // ensure a transform will be recalculated.. - if (this.transform) - { - this.transform._parentID = -1; - } this._boundsID++; this.onChildrenChange(beginIndex); diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index f37f2f5..be4c4fb 100644 --- a/src/core/graphics/Graphics.js +++ b/src/core/graphics/Graphics.js @@ -28,8 +28,9 @@ { /** * + * @param {boolean} [nativeLines=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP */ - constructor() + constructor(nativeLines = false) { super(); @@ -50,6 +51,13 @@ this.lineWidth = 0; /** + * If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * + * @member {boolean} + */ + this.nativeLines = nativeLines; + + /** * The color of any lines drawn. * * @member {string} @@ -757,28 +765,14 @@ if (!this._spriteRect) { - if (!Graphics._SPRITE_TEXTURE) - { - Graphics._SPRITE_TEXTURE = RenderTexture.create(10, 10); - - const canvas = document.createElement('canvas'); - - canvas.width = 10; - canvas.height = 10; - - const context = canvas.getContext('2d'); - - context.fillStyle = 'white'; - context.fillRect(0, 0, 10, 10); - - Graphics._SPRITE_TEXTURE = Texture.fromCanvas(canvas); - } - - this._spriteRect = new Sprite(Graphics._SPRITE_TEXTURE); + this._spriteRect = new Sprite(new Texture(Texture.WHITE)); } + + const sprite = this._spriteRect; + if (this.tint === 0xffffff) { - this._spriteRect.tint = this.graphicsData[0].fillColor; + sprite.tint = this.graphicsData[0].fillColor; } else { @@ -792,20 +786,21 @@ t1[1] *= t2[1]; t1[2] *= t2[2]; - this._spriteRect.tint = rgb2hex(t1); + sprite.tint = rgb2hex(t1); } - this._spriteRect.alpha = this.graphicsData[0].fillAlpha; - this._spriteRect.worldAlpha = this.worldAlpha * this._spriteRect.alpha; + sprite.alpha = this.graphicsData[0].fillAlpha; + sprite.worldAlpha = this.worldAlpha * sprite.alpha; + sprite.blendMode = this.blendMode; - Graphics._SPRITE_TEXTURE._frame.width = rect.width; - Graphics._SPRITE_TEXTURE._frame.height = rect.height; + sprite.texture._frame.width = rect.width; + sprite.texture._frame.height = rect.height; - this._spriteRect.transform.worldTransform = this.transform.worldTransform; + sprite.transform.worldTransform = this.transform.worldTransform; - this._spriteRect.anchor.set(-rect.x / rect.width, -rect.y / rect.height); - this._spriteRect._onAnchorUpdate(); + sprite.anchor.set(-rect.x / rect.width, -rect.y / rect.height); + sprite._onAnchorUpdate(); - this._spriteRect._renderWebGL(renderer); + sprite._renderWebGL(renderer); } /** @@ -1031,6 +1026,7 @@ this.fillColor, this.fillAlpha, this.filling, + this.nativeLines, shape ); diff --git a/src/core/graphics/GraphicsData.js b/src/core/graphics/GraphicsData.js index f5a87ae..b6f03c0 100644 --- a/src/core/graphics/GraphicsData.js +++ b/src/core/graphics/GraphicsData.js @@ -14,14 +14,19 @@ * @param {number} fillColor - the color of the fill * @param {number} fillAlpha - the alpha of the fill * @param {boolean} fill - whether or not the shape is filled with a colour + * @param {boolean} nativeLines - the method for drawing lines * @param {PIXI.Circle|PIXI.Rectangle|PIXI.Ellipse|PIXI.Polygon} shape - The shape object to draw. */ - constructor(lineWidth, lineColor, lineAlpha, fillColor, fillAlpha, fill, shape) + constructor(lineWidth, lineColor, lineAlpha, fillColor, fillAlpha, fill, nativeLines, shape) { /** * @member {number} the width of the line to draw */ this.lineWidth = lineWidth; + /** + * @member {boolean} if true the liens will be draw using LINES instead of TRIANGLE_STRIP + */ + this.nativeLines = nativeLines; /** * @member {number} the color of the line to draw @@ -85,6 +90,7 @@ this.fillColor, this.fillAlpha, this.fill, + this.nativeLines, this.shape ); } diff --git a/src/core/graphics/webgl/GraphicsRenderer.js b/src/core/graphics/webgl/GraphicsRenderer.js index 14083fe..67f5e92 100644 --- a/src/core/graphics/webgl/GraphicsRenderer.js +++ b/src/core/graphics/webgl/GraphicsRenderer.js @@ -102,7 +102,15 @@ shaderTemp.uniforms.alpha = graphics.worldAlpha; renderer.bindVao(webGLData.vao); - webGLData.vao.draw(gl.TRIANGLE_STRIP, webGLData.indices.length); + + if (graphics.nativeLines) + { + gl.drawArrays(gl.LINES, 0, webGLData.points.length / 6); + } + else + { + webGLData.vao.draw(gl.TRIANGLE_STRIP, webGLData.indices.length); + } } } diff --git a/src/core/graphics/webgl/utils/buildLine.js b/src/core/graphics/webgl/utils/buildLine.js index ecf43f7..4d6862e 100644 --- a/src/core/graphics/webgl/utils/buildLine.js +++ b/src/core/graphics/webgl/utils/buildLine.js @@ -11,7 +11,29 @@ * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties * @param {object} webGLData - an object containing all the webGL-specific information to create this shape */ -export default function buildLine(graphicsData, webGLData) +export default function (graphicsData, webGLData) +{ + if (graphicsData.nativeLines) + { + buildNativeLine(graphicsData, webGLData); + } + else + { + buildLine(graphicsData, webGLData); + } +} + +/** + * Builds a line to draw using the poligon method. + * + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @private + * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties + * @param {object} webGLData - an object containing all the webGL-specific information to create this shape + */ +function buildLine(graphicsData, webGLData) { // TODO OPTIMISE! let points = graphicsData.points; @@ -224,3 +246,46 @@ indices.push(indexStart - 1); } + +/** + * Builds a line to draw using the gl.drawArrays(gl.LINES) method + * + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @private + * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties + * @param {object} webGLData - an object containing all the webGL-specific information to create this shape + */ +function buildNativeLine(graphicsData, webGLData) +{ + let i = 0; + const points = graphicsData.points; + + if (points.length === 0) return; + + const verts = webGLData.points; + const length = points.length / 2; + + // sort color + const color = hex2rgb(graphicsData.lineColor); + const alpha = graphicsData.lineAlpha; + const r = color[0] * alpha; + const g = color[1] * alpha; + const b = color[2] * alpha; + + for (i = 1; i < length; i++) + { + const p1x = points[(i - 1) * 2]; + const p1y = points[((i - 1) * 2) + 1]; + + const p2x = points[i * 2]; + const p2y = points[(i * 2) + 1]; + + verts.push(p1x, p1y); + verts.push(r, g, b, alpha); + + verts.push(p2x, p2y); + verts.push(r, g, b, alpha); + } +} diff --git a/src/core/graphics/webgl/utils/buildRoundedRectangle.js b/src/core/graphics/webgl/utils/buildRoundedRectangle.js index 9abc807..a6c8461 100644 --- a/src/core/graphics/webgl/utils/buildRoundedRectangle.js +++ b/src/core/graphics/webgl/utils/buildRoundedRectangle.js @@ -77,6 +77,26 @@ } /** + * Calculate a single point for a quadratic bezier curve. + * Utility function used by quadraticBezierCurve. + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @private + * @param {number} n1 - first number + * @param {number} n2 - second number + * @param {number} perc - percentage + * @return {number} the result + * + */ +function getPt(n1, n2, perc) +{ + const diff = n2 - n1; + + return n1 + (diff * perc); +} + +/** * Calculate the points for a quadratic bezier curve. (helper function..) * Based on: https://stackoverflow.com/questions/785097/how-do-i-implement-a-bezier-curve-in-c * @@ -105,13 +125,6 @@ let x = 0; let y = 0; - function getPt(n1, n2, perc) - { - const diff = n2 - n1; - - return n1 + (diff * perc); - } - for (let i = 0, j = 0; i <= n; ++i) { j = i / n; diff --git a/src/core/index.js b/src/core/index.js index f7a817c..4f414e8 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -30,6 +30,7 @@ export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; export { default as CanvasGraphicsRenderer } from './graphics/canvas/CanvasGraphicsRenderer'; +export { default as Spritesheet } from './textures/Spritesheet'; export { default as Texture } from './textures/Texture'; export { default as BaseTexture } from './textures/BaseTexture'; export { default as RenderTexture } from './textures/RenderTexture'; diff --git a/src/core/math/Matrix.js b/src/core/math/Matrix.js index 0b77477..3679860 100644 --- a/src/core/math/Matrix.js +++ b/src/core/math/Matrix.js @@ -13,45 +13,50 @@ export default class Matrix { /** - * + * @param {number} [a=1] - x scale + * @param {number} [b=0] - y skew + * @param {number} [c=0] - x skew + * @param {number} [d=1] - y scale + * @param {number} [tx=0] - x translation + * @param {number} [ty=0] - y translation */ - constructor() + constructor(a = 1, b = 0, c = 0, d = 1, tx = 0, ty = 0) { /** * @member {number} * @default 1 */ - this.a = 1; + this.a = a; /** * @member {number} * @default 0 */ - this.b = 0; + this.b = b; /** * @member {number} * @default 0 */ - this.c = 0; + this.c = c; /** * @member {number} * @default 1 */ - this.d = 1; + this.d = d; /** * @member {number} * @default 0 */ - this.tx = 0; + this.tx = tx; /** * @member {number} * @default 0 */ - this.ty = 0; + this.ty = ty; this.array = null; } diff --git a/src/core/renderers/SystemRenderer.js b/src/core/renderers/SystemRenderer.js index fa116a6..a24c775 100644 --- a/src/core/renderers/SystemRenderer.js +++ b/src/core/renderers/SystemRenderer.js @@ -1,5 +1,5 @@ import { sayHello, hex2string, hex2rgb } from '../utils'; -import { Matrix } from '../math'; +import { Matrix, Rectangle } from '../math'; import { RENDERER_TYPE } from '../const'; import settings from '../settings'; import Container from '../display/Container'; @@ -21,8 +21,8 @@ { /** * @param {string} system - The name of the system this renderer is for. - * @param {number} [width=800] - the width of the canvas view - * @param {number} [height=600] - the height of the canvas view + * @param {number} [screenWidth=800] - the width of the screen + * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false @@ -37,7 +37,7 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(system, width, height, options) + constructor(system, screenWidth, screenHeight, options) { super(); @@ -69,20 +69,13 @@ this.type = RENDERER_TYPE.UNKNOWN; /** - * The width of the canvas view + * Measurements of the screen. (0, 0, screenWidth, screenHeight) * - * @member {number} - * @default 800 - */ - this.width = width || 800; - - /** - * The height of the canvas view + * Its safe to use as filterArea or hitArea for whole stage * - * @member {number} - * @default 600 + * @member {PIXI.Rectangle} */ - this.height = height || 600; + this.screen = new Rectangle(0, 0, screenWidth || 800, screenHeight || 600); /** * The canvas element that everything is drawn to @@ -107,7 +100,7 @@ this.transparent = options.transparent; /** - * Whether the render view should be resized automatically + * Whether css dimensions of canvas view should be resized to screen dimensions automatically * * @member {boolean} */ @@ -192,23 +185,48 @@ } /** - * Resizes the canvas view to the specified width and height + * Same as view.width, actual number of pixels in the canvas by horizontal * - * @param {number} width - the new width of the canvas view - * @param {number} height - the new height of the canvas view + * @member {number} + * @readonly + * @default 800 */ - resize(width, height) + get width() { - this.width = width * this.resolution; - this.height = height * this.resolution; + return this.view.width; + } - this.view.width = this.width; - this.view.height = this.height; + /** + * Same as view.height, actual number of pixels in the canvas by vertical + * + * @member {number} + * @readonly + * @default 600 + */ + get height() + { + return this.view.height; + } + + /** + * Resizes the screen and canvas to the specified width and height + * Canvas dimensions are multiplied by resolution + * + * @param {number} screenWidth - the new width of the screen + * @param {number} screenHeight - the new height of the screen + */ + resize(screenWidth, screenHeight) + { + this.screen.width = screenWidth; + this.screen.height = screenHeight; + + this.view.width = screenWidth * this.resolution; + this.view.height = screenHeight * this.resolution; if (this.autoResize) { - this.view.style.width = `${this.width / this.resolution}px`; - this.view.style.height = `${this.height / this.resolution}px`; + this.view.style.width = `${screenWidth}px`; + this.view.style.height = `${screenHeight}px`; } } @@ -249,11 +267,10 @@ this.type = RENDERER_TYPE.UNKNOWN; - this.width = 0; - this.height = 0; - this.view = null; + this.screen = null; + this.resolution = 0; this.transparent = false; diff --git a/src/core/renderers/canvas/CanvasRenderer.js b/src/core/renderers/canvas/CanvasRenderer.js index 1bd2d5e..e84e49c 100644 --- a/src/core/renderers/canvas/CanvasRenderer.js +++ b/src/core/renderers/canvas/CanvasRenderer.js @@ -18,8 +18,8 @@ export default class CanvasRenderer extends SystemRenderer { /** - * @param {number} [width=800] - the width of the canvas view - * @param {number} [height=600] - the height of the canvas view + * @param {number} [screenWidth=800] - the width of the screen + * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false @@ -34,9 +34,9 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(width, height, options = {}) + constructor(screenWidth, screenHeight, options = {}) { - super('Canvas', width, height, options); + super('Canvas', screenWidth, screenHeight, options); this.type = RENDERER_TYPE.CANVAS; @@ -96,7 +96,7 @@ this.context = null; this.renderingToScreen = false; - this.resize(width, height); + this.resize(screenWidth, screenHeight); } /** @@ -283,12 +283,12 @@ * * @extends PIXI.SystemRenderer#resize * - * @param {number} width - The new width of the canvas view - * @param {number} height - The new height of the canvas view + * @param {number} screenWidth - the new width of the screen + * @param {number} screenHeight - the new height of the screen */ - resize(width, height) + resize(screenWidth, screenHeight) { - super.resize(width, height); + super.resize(screenWidth, screenHeight); // reset the scale mode.. oddly this seems to be reset when the canvas is resized. // surely a browser bug?? Let pixi fix that for you.. diff --git a/package.json b/package.json index 6a366ba..71ade15 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pixi.js", - "version": "4.3.4", + "version": "4.3.5", "description": "Pixi.js is a fast lightweight 2D library that works across all devices.", "author": "Mat Groves", "contributors": [ @@ -61,7 +61,7 @@ "ismobilejs": "^0.4.0", "object-assign": "^4.0.1", "pixi-gl-core": "^1.0.3", - "resource-loader": "^2.0.4" + "resource-loader": "^2.0.6" }, "devDependencies": { "babel-cli": "^6.18.0", @@ -70,9 +70,9 @@ "babel-preset-es2015": "^6.14.0", "babelify": "^7.3.0", "del": "^2.2.0", - "electron-prebuilt": "^1.3.2", + "electron": "^1.4.15", "eslint": "^3.5.0", - "floss": "^1.2.0", + "floss": "^2.0.1", "gh-pages": "^0.11.0", "jaguarjs-jsdoc": "^1.0.1", "js-md5": "^0.4.1", diff --git a/src/accessibility/AccessibilityManager.js b/src/accessibility/AccessibilityManager.js index df2716a..f16bcb1 100644 --- a/src/accessibility/AccessibilityManager.js +++ b/src/accessibility/AccessibilityManager.js @@ -3,7 +3,7 @@ import accessibleTarget from './accessibleTarget'; // add some extra variables to the container.. -Object.assign( +core.utils.mixins.delayMixin( core.DisplayObject.prototype, accessibleTarget ); diff --git a/src/core/Application.js b/src/core/Application.js index ea0f058..5340578 100644 --- a/src/core/Application.js +++ b/src/core/Application.js @@ -1,6 +1,6 @@ import { autoDetectRenderer } from './autoDetectRenderer'; import Container from './display/Container'; -import Ticker from './ticker/Ticker'; +import { shared, Ticker } from './ticker'; /** * Convenience class to create a new PIXI application. @@ -35,9 +35,9 @@ * @param {boolean} [noWebGL=false] - prevents selection of WebGL renderer, even if such is present * @param {boolean} [options.legacy=false] - If true Pixi will aim to ensure compatibility * with older / less advanced devices. If you experience unexplained flickering try setting this to true. - + * @param {boolean} [useSharedTicker=false] - `true` to use PIXI.ticker.shared, `false` to create new ticker. */ - constructor(width, height, options, noWebGL) + constructor(width, height, options, noWebGL, useSharedTicker = false) { /** * WebGL renderer if available, otherwise CanvasRenderer @@ -46,23 +46,46 @@ this.renderer = autoDetectRenderer(width, height, options, noWebGL); /** - * The root display container that's renderered. + * The root display container that's rendered. * @member {PIXI.Container} */ this.stage = new Container(); /** + * Internal reference to the ticker + * @member {PIXI.ticker.Ticker} + * @private + */ + this._ticker = null; + + /** * Ticker for doing render updates. * @member {PIXI.ticker.Ticker} + * @default PIXI.ticker.shared */ - this.ticker = new Ticker(); - - this.ticker.add(this.render, this); + this.ticker = useSharedTicker ? shared : new Ticker(); // Start the rendering this.start(); } + set ticker(ticker) // eslint-disable-line require-jsdoc + { + if (this._ticker) + { + this._ticker.remove(this.render, this); + } + this._ticker = ticker; + if (ticker) + { + ticker.add(this.render, this); + } + } + get ticker() // eslint-disable-line require-jsdoc + { + return this._ticker; + } + /** * Render the current stage. */ @@ -76,7 +99,7 @@ */ stop() { - this.ticker.stop(); + this._ticker.stop(); } /** @@ -84,7 +107,7 @@ */ start() { - this.ticker.start(); + this._ticker.start(); } /** @@ -98,13 +121,22 @@ } /** + * Reference to the renderer's screen rectangle. Its safe to use as filterArea or hitArea for whole screen + * @member {PIXI.Rectangle} + * @readonly + */ + get screen() + { + return this.renderer.screen; + } + + /** * Destroy and don't use after this. * @param {Boolean} [removeView=false] Automatically remove canvas from DOM. */ destroy(removeView) { this.stop(); - this.ticker.remove(this.render, this); this.ticker = null; this.stage.destroy(); diff --git a/src/core/Shader.js b/src/core/Shader.js new file mode 100644 index 0000000..78b17e6 --- /dev/null +++ b/src/core/Shader.js @@ -0,0 +1,46 @@ +import { GLShader } from 'pixi-gl-core'; +import settings from './settings'; + +function checkPrecision(src, def) +{ + if (src instanceof Array) + { + if (src[0].substring(0, 9) !== 'precision') + { + const copy = src.slice(0); + + copy.unshift(`precision ${def} float;`); + + return copy; + } + } + else if (src.substring(0, 9) !== 'precision') + { + return `precision ${def} float;\n${src}`; + } + + return src; +} + +/** + * Wrapper class, webGL Shader for Pixi. + * Adds precision string if vertexSrc or fragmentSrc have no mention of it. + * + * @class + * @extends GLShader + * @memberof PIXI + */ +export default class Shader extends GLShader +{ + /** + * + * @param {WebGLRenderingContext} gl - The current WebGL rendering context + * @param {string|string[]} vertexSrc - The vertex shader source as an array of strings. + * @param {string|string[]} fragmentSrc - The fragment shader source as an array of strings. + */ + constructor(gl, vertexSrc, fragmentSrc) + { + super(gl, checkPrecision(vertexSrc, settings.PRECISION_VERTEX), + checkPrecision(fragmentSrc, settings.PRECISION_FRAGMENT)); + } +} diff --git a/src/core/const.js b/src/core/const.js index ef6f662..3de0efe 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -188,7 +188,7 @@ * The gc modes that are supported by pixi. * * The {@link PIXI.settings.GC_MODE} Garbage Collection mode for pixi textures is AUTO - * If set to GC_MODE, the renderer will occasianally check textures usage. If they are not + * If set to GC_MODE, the renderer will occasionally check textures usage. If they are not * used for a specified period of time they will be removed from the GPU. They will of course * be uploaded again when they are required. This is a silent behind the scenes process that * should ensure that the GPU does not get filled up. diff --git a/src/core/display/Container.js b/src/core/display/Container.js index ed8cd5f..17fde8d 100644 --- a/src/core/display/Container.js +++ b/src/core/display/Container.js @@ -73,11 +73,12 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.push(child); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -108,11 +109,12 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.splice(index, 0, child); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -225,10 +227,11 @@ if (index === -1) return null; child.parent = null; + // ensure child transform will be recalculated + child.transform._parentID = -1; removeItems(this.children, index, 1); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -249,11 +252,12 @@ { const child = this.getChildAt(index); + // ensure child transform will be recalculated.. child.parent = null; + child.transform._parentID = -1; removeItems(this.children, index, 1); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -284,13 +288,12 @@ for (let i = 0; i < removed.length; ++i) { removed[i].parent = null; + if (removed[i].transform) + { + removed[i].transform._parentID = -1; + } } - // ensure a transform will be recalculated.. - if (this.transform) - { - this.transform._parentID = -1; - } this._boundsID++; this.onChildrenChange(beginIndex); diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index f37f2f5..be4c4fb 100644 --- a/src/core/graphics/Graphics.js +++ b/src/core/graphics/Graphics.js @@ -28,8 +28,9 @@ { /** * + * @param {boolean} [nativeLines=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP */ - constructor() + constructor(nativeLines = false) { super(); @@ -50,6 +51,13 @@ this.lineWidth = 0; /** + * If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * + * @member {boolean} + */ + this.nativeLines = nativeLines; + + /** * The color of any lines drawn. * * @member {string} @@ -757,28 +765,14 @@ if (!this._spriteRect) { - if (!Graphics._SPRITE_TEXTURE) - { - Graphics._SPRITE_TEXTURE = RenderTexture.create(10, 10); - - const canvas = document.createElement('canvas'); - - canvas.width = 10; - canvas.height = 10; - - const context = canvas.getContext('2d'); - - context.fillStyle = 'white'; - context.fillRect(0, 0, 10, 10); - - Graphics._SPRITE_TEXTURE = Texture.fromCanvas(canvas); - } - - this._spriteRect = new Sprite(Graphics._SPRITE_TEXTURE); + this._spriteRect = new Sprite(new Texture(Texture.WHITE)); } + + const sprite = this._spriteRect; + if (this.tint === 0xffffff) { - this._spriteRect.tint = this.graphicsData[0].fillColor; + sprite.tint = this.graphicsData[0].fillColor; } else { @@ -792,20 +786,21 @@ t1[1] *= t2[1]; t1[2] *= t2[2]; - this._spriteRect.tint = rgb2hex(t1); + sprite.tint = rgb2hex(t1); } - this._spriteRect.alpha = this.graphicsData[0].fillAlpha; - this._spriteRect.worldAlpha = this.worldAlpha * this._spriteRect.alpha; + sprite.alpha = this.graphicsData[0].fillAlpha; + sprite.worldAlpha = this.worldAlpha * sprite.alpha; + sprite.blendMode = this.blendMode; - Graphics._SPRITE_TEXTURE._frame.width = rect.width; - Graphics._SPRITE_TEXTURE._frame.height = rect.height; + sprite.texture._frame.width = rect.width; + sprite.texture._frame.height = rect.height; - this._spriteRect.transform.worldTransform = this.transform.worldTransform; + sprite.transform.worldTransform = this.transform.worldTransform; - this._spriteRect.anchor.set(-rect.x / rect.width, -rect.y / rect.height); - this._spriteRect._onAnchorUpdate(); + sprite.anchor.set(-rect.x / rect.width, -rect.y / rect.height); + sprite._onAnchorUpdate(); - this._spriteRect._renderWebGL(renderer); + sprite._renderWebGL(renderer); } /** @@ -1031,6 +1026,7 @@ this.fillColor, this.fillAlpha, this.filling, + this.nativeLines, shape ); diff --git a/src/core/graphics/GraphicsData.js b/src/core/graphics/GraphicsData.js index f5a87ae..b6f03c0 100644 --- a/src/core/graphics/GraphicsData.js +++ b/src/core/graphics/GraphicsData.js @@ -14,14 +14,19 @@ * @param {number} fillColor - the color of the fill * @param {number} fillAlpha - the alpha of the fill * @param {boolean} fill - whether or not the shape is filled with a colour + * @param {boolean} nativeLines - the method for drawing lines * @param {PIXI.Circle|PIXI.Rectangle|PIXI.Ellipse|PIXI.Polygon} shape - The shape object to draw. */ - constructor(lineWidth, lineColor, lineAlpha, fillColor, fillAlpha, fill, shape) + constructor(lineWidth, lineColor, lineAlpha, fillColor, fillAlpha, fill, nativeLines, shape) { /** * @member {number} the width of the line to draw */ this.lineWidth = lineWidth; + /** + * @member {boolean} if true the liens will be draw using LINES instead of TRIANGLE_STRIP + */ + this.nativeLines = nativeLines; /** * @member {number} the color of the line to draw @@ -85,6 +90,7 @@ this.fillColor, this.fillAlpha, this.fill, + this.nativeLines, this.shape ); } diff --git a/src/core/graphics/webgl/GraphicsRenderer.js b/src/core/graphics/webgl/GraphicsRenderer.js index 14083fe..67f5e92 100644 --- a/src/core/graphics/webgl/GraphicsRenderer.js +++ b/src/core/graphics/webgl/GraphicsRenderer.js @@ -102,7 +102,15 @@ shaderTemp.uniforms.alpha = graphics.worldAlpha; renderer.bindVao(webGLData.vao); - webGLData.vao.draw(gl.TRIANGLE_STRIP, webGLData.indices.length); + + if (graphics.nativeLines) + { + gl.drawArrays(gl.LINES, 0, webGLData.points.length / 6); + } + else + { + webGLData.vao.draw(gl.TRIANGLE_STRIP, webGLData.indices.length); + } } } diff --git a/src/core/graphics/webgl/utils/buildLine.js b/src/core/graphics/webgl/utils/buildLine.js index ecf43f7..4d6862e 100644 --- a/src/core/graphics/webgl/utils/buildLine.js +++ b/src/core/graphics/webgl/utils/buildLine.js @@ -11,7 +11,29 @@ * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties * @param {object} webGLData - an object containing all the webGL-specific information to create this shape */ -export default function buildLine(graphicsData, webGLData) +export default function (graphicsData, webGLData) +{ + if (graphicsData.nativeLines) + { + buildNativeLine(graphicsData, webGLData); + } + else + { + buildLine(graphicsData, webGLData); + } +} + +/** + * Builds a line to draw using the poligon method. + * + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @private + * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties + * @param {object} webGLData - an object containing all the webGL-specific information to create this shape + */ +function buildLine(graphicsData, webGLData) { // TODO OPTIMISE! let points = graphicsData.points; @@ -224,3 +246,46 @@ indices.push(indexStart - 1); } + +/** + * Builds a line to draw using the gl.drawArrays(gl.LINES) method + * + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @private + * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties + * @param {object} webGLData - an object containing all the webGL-specific information to create this shape + */ +function buildNativeLine(graphicsData, webGLData) +{ + let i = 0; + const points = graphicsData.points; + + if (points.length === 0) return; + + const verts = webGLData.points; + const length = points.length / 2; + + // sort color + const color = hex2rgb(graphicsData.lineColor); + const alpha = graphicsData.lineAlpha; + const r = color[0] * alpha; + const g = color[1] * alpha; + const b = color[2] * alpha; + + for (i = 1; i < length; i++) + { + const p1x = points[(i - 1) * 2]; + const p1y = points[((i - 1) * 2) + 1]; + + const p2x = points[i * 2]; + const p2y = points[(i * 2) + 1]; + + verts.push(p1x, p1y); + verts.push(r, g, b, alpha); + + verts.push(p2x, p2y); + verts.push(r, g, b, alpha); + } +} diff --git a/src/core/graphics/webgl/utils/buildRoundedRectangle.js b/src/core/graphics/webgl/utils/buildRoundedRectangle.js index 9abc807..a6c8461 100644 --- a/src/core/graphics/webgl/utils/buildRoundedRectangle.js +++ b/src/core/graphics/webgl/utils/buildRoundedRectangle.js @@ -77,6 +77,26 @@ } /** + * Calculate a single point for a quadratic bezier curve. + * Utility function used by quadraticBezierCurve. + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @private + * @param {number} n1 - first number + * @param {number} n2 - second number + * @param {number} perc - percentage + * @return {number} the result + * + */ +function getPt(n1, n2, perc) +{ + const diff = n2 - n1; + + return n1 + (diff * perc); +} + +/** * Calculate the points for a quadratic bezier curve. (helper function..) * Based on: https://stackoverflow.com/questions/785097/how-do-i-implement-a-bezier-curve-in-c * @@ -105,13 +125,6 @@ let x = 0; let y = 0; - function getPt(n1, n2, perc) - { - const diff = n2 - n1; - - return n1 + (diff * perc); - } - for (let i = 0, j = 0; i <= n; ++i) { j = i / n; diff --git a/src/core/index.js b/src/core/index.js index f7a817c..4f414e8 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -30,6 +30,7 @@ export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; export { default as CanvasGraphicsRenderer } from './graphics/canvas/CanvasGraphicsRenderer'; +export { default as Spritesheet } from './textures/Spritesheet'; export { default as Texture } from './textures/Texture'; export { default as BaseTexture } from './textures/BaseTexture'; export { default as RenderTexture } from './textures/RenderTexture'; diff --git a/src/core/math/Matrix.js b/src/core/math/Matrix.js index 0b77477..3679860 100644 --- a/src/core/math/Matrix.js +++ b/src/core/math/Matrix.js @@ -13,45 +13,50 @@ export default class Matrix { /** - * + * @param {number} [a=1] - x scale + * @param {number} [b=0] - y skew + * @param {number} [c=0] - x skew + * @param {number} [d=1] - y scale + * @param {number} [tx=0] - x translation + * @param {number} [ty=0] - y translation */ - constructor() + constructor(a = 1, b = 0, c = 0, d = 1, tx = 0, ty = 0) { /** * @member {number} * @default 1 */ - this.a = 1; + this.a = a; /** * @member {number} * @default 0 */ - this.b = 0; + this.b = b; /** * @member {number} * @default 0 */ - this.c = 0; + this.c = c; /** * @member {number} * @default 1 */ - this.d = 1; + this.d = d; /** * @member {number} * @default 0 */ - this.tx = 0; + this.tx = tx; /** * @member {number} * @default 0 */ - this.ty = 0; + this.ty = ty; this.array = null; } diff --git a/src/core/renderers/SystemRenderer.js b/src/core/renderers/SystemRenderer.js index fa116a6..a24c775 100644 --- a/src/core/renderers/SystemRenderer.js +++ b/src/core/renderers/SystemRenderer.js @@ -1,5 +1,5 @@ import { sayHello, hex2string, hex2rgb } from '../utils'; -import { Matrix } from '../math'; +import { Matrix, Rectangle } from '../math'; import { RENDERER_TYPE } from '../const'; import settings from '../settings'; import Container from '../display/Container'; @@ -21,8 +21,8 @@ { /** * @param {string} system - The name of the system this renderer is for. - * @param {number} [width=800] - the width of the canvas view - * @param {number} [height=600] - the height of the canvas view + * @param {number} [screenWidth=800] - the width of the screen + * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false @@ -37,7 +37,7 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(system, width, height, options) + constructor(system, screenWidth, screenHeight, options) { super(); @@ -69,20 +69,13 @@ this.type = RENDERER_TYPE.UNKNOWN; /** - * The width of the canvas view + * Measurements of the screen. (0, 0, screenWidth, screenHeight) * - * @member {number} - * @default 800 - */ - this.width = width || 800; - - /** - * The height of the canvas view + * Its safe to use as filterArea or hitArea for whole stage * - * @member {number} - * @default 600 + * @member {PIXI.Rectangle} */ - this.height = height || 600; + this.screen = new Rectangle(0, 0, screenWidth || 800, screenHeight || 600); /** * The canvas element that everything is drawn to @@ -107,7 +100,7 @@ this.transparent = options.transparent; /** - * Whether the render view should be resized automatically + * Whether css dimensions of canvas view should be resized to screen dimensions automatically * * @member {boolean} */ @@ -192,23 +185,48 @@ } /** - * Resizes the canvas view to the specified width and height + * Same as view.width, actual number of pixels in the canvas by horizontal * - * @param {number} width - the new width of the canvas view - * @param {number} height - the new height of the canvas view + * @member {number} + * @readonly + * @default 800 */ - resize(width, height) + get width() { - this.width = width * this.resolution; - this.height = height * this.resolution; + return this.view.width; + } - this.view.width = this.width; - this.view.height = this.height; + /** + * Same as view.height, actual number of pixels in the canvas by vertical + * + * @member {number} + * @readonly + * @default 600 + */ + get height() + { + return this.view.height; + } + + /** + * Resizes the screen and canvas to the specified width and height + * Canvas dimensions are multiplied by resolution + * + * @param {number} screenWidth - the new width of the screen + * @param {number} screenHeight - the new height of the screen + */ + resize(screenWidth, screenHeight) + { + this.screen.width = screenWidth; + this.screen.height = screenHeight; + + this.view.width = screenWidth * this.resolution; + this.view.height = screenHeight * this.resolution; if (this.autoResize) { - this.view.style.width = `${this.width / this.resolution}px`; - this.view.style.height = `${this.height / this.resolution}px`; + this.view.style.width = `${screenWidth}px`; + this.view.style.height = `${screenHeight}px`; } } @@ -249,11 +267,10 @@ this.type = RENDERER_TYPE.UNKNOWN; - this.width = 0; - this.height = 0; - this.view = null; + this.screen = null; + this.resolution = 0; this.transparent = false; diff --git a/src/core/renderers/canvas/CanvasRenderer.js b/src/core/renderers/canvas/CanvasRenderer.js index 1bd2d5e..e84e49c 100644 --- a/src/core/renderers/canvas/CanvasRenderer.js +++ b/src/core/renderers/canvas/CanvasRenderer.js @@ -18,8 +18,8 @@ export default class CanvasRenderer extends SystemRenderer { /** - * @param {number} [width=800] - the width of the canvas view - * @param {number} [height=600] - the height of the canvas view + * @param {number} [screenWidth=800] - the width of the screen + * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false @@ -34,9 +34,9 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(width, height, options = {}) + constructor(screenWidth, screenHeight, options = {}) { - super('Canvas', width, height, options); + super('Canvas', screenWidth, screenHeight, options); this.type = RENDERER_TYPE.CANVAS; @@ -96,7 +96,7 @@ this.context = null; this.renderingToScreen = false; - this.resize(width, height); + this.resize(screenWidth, screenHeight); } /** @@ -283,12 +283,12 @@ * * @extends PIXI.SystemRenderer#resize * - * @param {number} width - The new width of the canvas view - * @param {number} height - The new height of the canvas view + * @param {number} screenWidth - the new width of the screen + * @param {number} screenHeight - the new height of the screen */ - resize(width, height) + resize(screenWidth, screenHeight) { - super.resize(width, height); + super.resize(screenWidth, screenHeight); // reset the scale mode.. oddly this seems to be reset when the canvas is resized. // surely a browser bug?? Let pixi fix that for you.. diff --git a/src/core/renderers/webgl/TextureManager.js b/src/core/renderers/webgl/TextureManager.js index b685c1a..8227289 100644 --- a/src/core/renderers/webgl/TextureManager.js +++ b/src/core/renderers/webgl/TextureManager.js @@ -61,7 +61,7 @@ * Updates and/or Creates a WebGL texture for the renderer's context. * * @param {PIXI.BaseTexture|PIXI.Texture} texture - the texture to update - * @param {Number} location - the location the texture will be bound to. + * @param {number} location - the location the texture will be bound to. * @return {GLTexture} The gl texture. */ updateTexture(texture, location) diff --git a/package.json b/package.json index 6a366ba..71ade15 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pixi.js", - "version": "4.3.4", + "version": "4.3.5", "description": "Pixi.js is a fast lightweight 2D library that works across all devices.", "author": "Mat Groves", "contributors": [ @@ -61,7 +61,7 @@ "ismobilejs": "^0.4.0", "object-assign": "^4.0.1", "pixi-gl-core": "^1.0.3", - "resource-loader": "^2.0.4" + "resource-loader": "^2.0.6" }, "devDependencies": { "babel-cli": "^6.18.0", @@ -70,9 +70,9 @@ "babel-preset-es2015": "^6.14.0", "babelify": "^7.3.0", "del": "^2.2.0", - "electron-prebuilt": "^1.3.2", + "electron": "^1.4.15", "eslint": "^3.5.0", - "floss": "^1.2.0", + "floss": "^2.0.1", "gh-pages": "^0.11.0", "jaguarjs-jsdoc": "^1.0.1", "js-md5": "^0.4.1", diff --git a/src/accessibility/AccessibilityManager.js b/src/accessibility/AccessibilityManager.js index df2716a..f16bcb1 100644 --- a/src/accessibility/AccessibilityManager.js +++ b/src/accessibility/AccessibilityManager.js @@ -3,7 +3,7 @@ import accessibleTarget from './accessibleTarget'; // add some extra variables to the container.. -Object.assign( +core.utils.mixins.delayMixin( core.DisplayObject.prototype, accessibleTarget ); diff --git a/src/core/Application.js b/src/core/Application.js index ea0f058..5340578 100644 --- a/src/core/Application.js +++ b/src/core/Application.js @@ -1,6 +1,6 @@ import { autoDetectRenderer } from './autoDetectRenderer'; import Container from './display/Container'; -import Ticker from './ticker/Ticker'; +import { shared, Ticker } from './ticker'; /** * Convenience class to create a new PIXI application. @@ -35,9 +35,9 @@ * @param {boolean} [noWebGL=false] - prevents selection of WebGL renderer, even if such is present * @param {boolean} [options.legacy=false] - If true Pixi will aim to ensure compatibility * with older / less advanced devices. If you experience unexplained flickering try setting this to true. - + * @param {boolean} [useSharedTicker=false] - `true` to use PIXI.ticker.shared, `false` to create new ticker. */ - constructor(width, height, options, noWebGL) + constructor(width, height, options, noWebGL, useSharedTicker = false) { /** * WebGL renderer if available, otherwise CanvasRenderer @@ -46,23 +46,46 @@ this.renderer = autoDetectRenderer(width, height, options, noWebGL); /** - * The root display container that's renderered. + * The root display container that's rendered. * @member {PIXI.Container} */ this.stage = new Container(); /** + * Internal reference to the ticker + * @member {PIXI.ticker.Ticker} + * @private + */ + this._ticker = null; + + /** * Ticker for doing render updates. * @member {PIXI.ticker.Ticker} + * @default PIXI.ticker.shared */ - this.ticker = new Ticker(); - - this.ticker.add(this.render, this); + this.ticker = useSharedTicker ? shared : new Ticker(); // Start the rendering this.start(); } + set ticker(ticker) // eslint-disable-line require-jsdoc + { + if (this._ticker) + { + this._ticker.remove(this.render, this); + } + this._ticker = ticker; + if (ticker) + { + ticker.add(this.render, this); + } + } + get ticker() // eslint-disable-line require-jsdoc + { + return this._ticker; + } + /** * Render the current stage. */ @@ -76,7 +99,7 @@ */ stop() { - this.ticker.stop(); + this._ticker.stop(); } /** @@ -84,7 +107,7 @@ */ start() { - this.ticker.start(); + this._ticker.start(); } /** @@ -98,13 +121,22 @@ } /** + * Reference to the renderer's screen rectangle. Its safe to use as filterArea or hitArea for whole screen + * @member {PIXI.Rectangle} + * @readonly + */ + get screen() + { + return this.renderer.screen; + } + + /** * Destroy and don't use after this. * @param {Boolean} [removeView=false] Automatically remove canvas from DOM. */ destroy(removeView) { this.stop(); - this.ticker.remove(this.render, this); this.ticker = null; this.stage.destroy(); diff --git a/src/core/Shader.js b/src/core/Shader.js new file mode 100644 index 0000000..78b17e6 --- /dev/null +++ b/src/core/Shader.js @@ -0,0 +1,46 @@ +import { GLShader } from 'pixi-gl-core'; +import settings from './settings'; + +function checkPrecision(src, def) +{ + if (src instanceof Array) + { + if (src[0].substring(0, 9) !== 'precision') + { + const copy = src.slice(0); + + copy.unshift(`precision ${def} float;`); + + return copy; + } + } + else if (src.substring(0, 9) !== 'precision') + { + return `precision ${def} float;\n${src}`; + } + + return src; +} + +/** + * Wrapper class, webGL Shader for Pixi. + * Adds precision string if vertexSrc or fragmentSrc have no mention of it. + * + * @class + * @extends GLShader + * @memberof PIXI + */ +export default class Shader extends GLShader +{ + /** + * + * @param {WebGLRenderingContext} gl - The current WebGL rendering context + * @param {string|string[]} vertexSrc - The vertex shader source as an array of strings. + * @param {string|string[]} fragmentSrc - The fragment shader source as an array of strings. + */ + constructor(gl, vertexSrc, fragmentSrc) + { + super(gl, checkPrecision(vertexSrc, settings.PRECISION_VERTEX), + checkPrecision(fragmentSrc, settings.PRECISION_FRAGMENT)); + } +} diff --git a/src/core/const.js b/src/core/const.js index ef6f662..3de0efe 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -188,7 +188,7 @@ * The gc modes that are supported by pixi. * * The {@link PIXI.settings.GC_MODE} Garbage Collection mode for pixi textures is AUTO - * If set to GC_MODE, the renderer will occasianally check textures usage. If they are not + * If set to GC_MODE, the renderer will occasionally check textures usage. If they are not * used for a specified period of time they will be removed from the GPU. They will of course * be uploaded again when they are required. This is a silent behind the scenes process that * should ensure that the GPU does not get filled up. diff --git a/src/core/display/Container.js b/src/core/display/Container.js index ed8cd5f..17fde8d 100644 --- a/src/core/display/Container.js +++ b/src/core/display/Container.js @@ -73,11 +73,12 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.push(child); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -108,11 +109,12 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.splice(index, 0, child); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -225,10 +227,11 @@ if (index === -1) return null; child.parent = null; + // ensure child transform will be recalculated + child.transform._parentID = -1; removeItems(this.children, index, 1); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -249,11 +252,12 @@ { const child = this.getChildAt(index); + // ensure child transform will be recalculated.. child.parent = null; + child.transform._parentID = -1; removeItems(this.children, index, 1); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -284,13 +288,12 @@ for (let i = 0; i < removed.length; ++i) { removed[i].parent = null; + if (removed[i].transform) + { + removed[i].transform._parentID = -1; + } } - // ensure a transform will be recalculated.. - if (this.transform) - { - this.transform._parentID = -1; - } this._boundsID++; this.onChildrenChange(beginIndex); diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index f37f2f5..be4c4fb 100644 --- a/src/core/graphics/Graphics.js +++ b/src/core/graphics/Graphics.js @@ -28,8 +28,9 @@ { /** * + * @param {boolean} [nativeLines=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP */ - constructor() + constructor(nativeLines = false) { super(); @@ -50,6 +51,13 @@ this.lineWidth = 0; /** + * If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * + * @member {boolean} + */ + this.nativeLines = nativeLines; + + /** * The color of any lines drawn. * * @member {string} @@ -757,28 +765,14 @@ if (!this._spriteRect) { - if (!Graphics._SPRITE_TEXTURE) - { - Graphics._SPRITE_TEXTURE = RenderTexture.create(10, 10); - - const canvas = document.createElement('canvas'); - - canvas.width = 10; - canvas.height = 10; - - const context = canvas.getContext('2d'); - - context.fillStyle = 'white'; - context.fillRect(0, 0, 10, 10); - - Graphics._SPRITE_TEXTURE = Texture.fromCanvas(canvas); - } - - this._spriteRect = new Sprite(Graphics._SPRITE_TEXTURE); + this._spriteRect = new Sprite(new Texture(Texture.WHITE)); } + + const sprite = this._spriteRect; + if (this.tint === 0xffffff) { - this._spriteRect.tint = this.graphicsData[0].fillColor; + sprite.tint = this.graphicsData[0].fillColor; } else { @@ -792,20 +786,21 @@ t1[1] *= t2[1]; t1[2] *= t2[2]; - this._spriteRect.tint = rgb2hex(t1); + sprite.tint = rgb2hex(t1); } - this._spriteRect.alpha = this.graphicsData[0].fillAlpha; - this._spriteRect.worldAlpha = this.worldAlpha * this._spriteRect.alpha; + sprite.alpha = this.graphicsData[0].fillAlpha; + sprite.worldAlpha = this.worldAlpha * sprite.alpha; + sprite.blendMode = this.blendMode; - Graphics._SPRITE_TEXTURE._frame.width = rect.width; - Graphics._SPRITE_TEXTURE._frame.height = rect.height; + sprite.texture._frame.width = rect.width; + sprite.texture._frame.height = rect.height; - this._spriteRect.transform.worldTransform = this.transform.worldTransform; + sprite.transform.worldTransform = this.transform.worldTransform; - this._spriteRect.anchor.set(-rect.x / rect.width, -rect.y / rect.height); - this._spriteRect._onAnchorUpdate(); + sprite.anchor.set(-rect.x / rect.width, -rect.y / rect.height); + sprite._onAnchorUpdate(); - this._spriteRect._renderWebGL(renderer); + sprite._renderWebGL(renderer); } /** @@ -1031,6 +1026,7 @@ this.fillColor, this.fillAlpha, this.filling, + this.nativeLines, shape ); diff --git a/src/core/graphics/GraphicsData.js b/src/core/graphics/GraphicsData.js index f5a87ae..b6f03c0 100644 --- a/src/core/graphics/GraphicsData.js +++ b/src/core/graphics/GraphicsData.js @@ -14,14 +14,19 @@ * @param {number} fillColor - the color of the fill * @param {number} fillAlpha - the alpha of the fill * @param {boolean} fill - whether or not the shape is filled with a colour + * @param {boolean} nativeLines - the method for drawing lines * @param {PIXI.Circle|PIXI.Rectangle|PIXI.Ellipse|PIXI.Polygon} shape - The shape object to draw. */ - constructor(lineWidth, lineColor, lineAlpha, fillColor, fillAlpha, fill, shape) + constructor(lineWidth, lineColor, lineAlpha, fillColor, fillAlpha, fill, nativeLines, shape) { /** * @member {number} the width of the line to draw */ this.lineWidth = lineWidth; + /** + * @member {boolean} if true the liens will be draw using LINES instead of TRIANGLE_STRIP + */ + this.nativeLines = nativeLines; /** * @member {number} the color of the line to draw @@ -85,6 +90,7 @@ this.fillColor, this.fillAlpha, this.fill, + this.nativeLines, this.shape ); } diff --git a/src/core/graphics/webgl/GraphicsRenderer.js b/src/core/graphics/webgl/GraphicsRenderer.js index 14083fe..67f5e92 100644 --- a/src/core/graphics/webgl/GraphicsRenderer.js +++ b/src/core/graphics/webgl/GraphicsRenderer.js @@ -102,7 +102,15 @@ shaderTemp.uniforms.alpha = graphics.worldAlpha; renderer.bindVao(webGLData.vao); - webGLData.vao.draw(gl.TRIANGLE_STRIP, webGLData.indices.length); + + if (graphics.nativeLines) + { + gl.drawArrays(gl.LINES, 0, webGLData.points.length / 6); + } + else + { + webGLData.vao.draw(gl.TRIANGLE_STRIP, webGLData.indices.length); + } } } diff --git a/src/core/graphics/webgl/utils/buildLine.js b/src/core/graphics/webgl/utils/buildLine.js index ecf43f7..4d6862e 100644 --- a/src/core/graphics/webgl/utils/buildLine.js +++ b/src/core/graphics/webgl/utils/buildLine.js @@ -11,7 +11,29 @@ * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties * @param {object} webGLData - an object containing all the webGL-specific information to create this shape */ -export default function buildLine(graphicsData, webGLData) +export default function (graphicsData, webGLData) +{ + if (graphicsData.nativeLines) + { + buildNativeLine(graphicsData, webGLData); + } + else + { + buildLine(graphicsData, webGLData); + } +} + +/** + * Builds a line to draw using the poligon method. + * + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @private + * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties + * @param {object} webGLData - an object containing all the webGL-specific information to create this shape + */ +function buildLine(graphicsData, webGLData) { // TODO OPTIMISE! let points = graphicsData.points; @@ -224,3 +246,46 @@ indices.push(indexStart - 1); } + +/** + * Builds a line to draw using the gl.drawArrays(gl.LINES) method + * + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @private + * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties + * @param {object} webGLData - an object containing all the webGL-specific information to create this shape + */ +function buildNativeLine(graphicsData, webGLData) +{ + let i = 0; + const points = graphicsData.points; + + if (points.length === 0) return; + + const verts = webGLData.points; + const length = points.length / 2; + + // sort color + const color = hex2rgb(graphicsData.lineColor); + const alpha = graphicsData.lineAlpha; + const r = color[0] * alpha; + const g = color[1] * alpha; + const b = color[2] * alpha; + + for (i = 1; i < length; i++) + { + const p1x = points[(i - 1) * 2]; + const p1y = points[((i - 1) * 2) + 1]; + + const p2x = points[i * 2]; + const p2y = points[(i * 2) + 1]; + + verts.push(p1x, p1y); + verts.push(r, g, b, alpha); + + verts.push(p2x, p2y); + verts.push(r, g, b, alpha); + } +} diff --git a/src/core/graphics/webgl/utils/buildRoundedRectangle.js b/src/core/graphics/webgl/utils/buildRoundedRectangle.js index 9abc807..a6c8461 100644 --- a/src/core/graphics/webgl/utils/buildRoundedRectangle.js +++ b/src/core/graphics/webgl/utils/buildRoundedRectangle.js @@ -77,6 +77,26 @@ } /** + * Calculate a single point for a quadratic bezier curve. + * Utility function used by quadraticBezierCurve. + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @private + * @param {number} n1 - first number + * @param {number} n2 - second number + * @param {number} perc - percentage + * @return {number} the result + * + */ +function getPt(n1, n2, perc) +{ + const diff = n2 - n1; + + return n1 + (diff * perc); +} + +/** * Calculate the points for a quadratic bezier curve. (helper function..) * Based on: https://stackoverflow.com/questions/785097/how-do-i-implement-a-bezier-curve-in-c * @@ -105,13 +125,6 @@ let x = 0; let y = 0; - function getPt(n1, n2, perc) - { - const diff = n2 - n1; - - return n1 + (diff * perc); - } - for (let i = 0, j = 0; i <= n; ++i) { j = i / n; diff --git a/src/core/index.js b/src/core/index.js index f7a817c..4f414e8 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -30,6 +30,7 @@ export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; export { default as CanvasGraphicsRenderer } from './graphics/canvas/CanvasGraphicsRenderer'; +export { default as Spritesheet } from './textures/Spritesheet'; export { default as Texture } from './textures/Texture'; export { default as BaseTexture } from './textures/BaseTexture'; export { default as RenderTexture } from './textures/RenderTexture'; diff --git a/src/core/math/Matrix.js b/src/core/math/Matrix.js index 0b77477..3679860 100644 --- a/src/core/math/Matrix.js +++ b/src/core/math/Matrix.js @@ -13,45 +13,50 @@ export default class Matrix { /** - * + * @param {number} [a=1] - x scale + * @param {number} [b=0] - y skew + * @param {number} [c=0] - x skew + * @param {number} [d=1] - y scale + * @param {number} [tx=0] - x translation + * @param {number} [ty=0] - y translation */ - constructor() + constructor(a = 1, b = 0, c = 0, d = 1, tx = 0, ty = 0) { /** * @member {number} * @default 1 */ - this.a = 1; + this.a = a; /** * @member {number} * @default 0 */ - this.b = 0; + this.b = b; /** * @member {number} * @default 0 */ - this.c = 0; + this.c = c; /** * @member {number} * @default 1 */ - this.d = 1; + this.d = d; /** * @member {number} * @default 0 */ - this.tx = 0; + this.tx = tx; /** * @member {number} * @default 0 */ - this.ty = 0; + this.ty = ty; this.array = null; } diff --git a/src/core/renderers/SystemRenderer.js b/src/core/renderers/SystemRenderer.js index fa116a6..a24c775 100644 --- a/src/core/renderers/SystemRenderer.js +++ b/src/core/renderers/SystemRenderer.js @@ -1,5 +1,5 @@ import { sayHello, hex2string, hex2rgb } from '../utils'; -import { Matrix } from '../math'; +import { Matrix, Rectangle } from '../math'; import { RENDERER_TYPE } from '../const'; import settings from '../settings'; import Container from '../display/Container'; @@ -21,8 +21,8 @@ { /** * @param {string} system - The name of the system this renderer is for. - * @param {number} [width=800] - the width of the canvas view - * @param {number} [height=600] - the height of the canvas view + * @param {number} [screenWidth=800] - the width of the screen + * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false @@ -37,7 +37,7 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(system, width, height, options) + constructor(system, screenWidth, screenHeight, options) { super(); @@ -69,20 +69,13 @@ this.type = RENDERER_TYPE.UNKNOWN; /** - * The width of the canvas view + * Measurements of the screen. (0, 0, screenWidth, screenHeight) * - * @member {number} - * @default 800 - */ - this.width = width || 800; - - /** - * The height of the canvas view + * Its safe to use as filterArea or hitArea for whole stage * - * @member {number} - * @default 600 + * @member {PIXI.Rectangle} */ - this.height = height || 600; + this.screen = new Rectangle(0, 0, screenWidth || 800, screenHeight || 600); /** * The canvas element that everything is drawn to @@ -107,7 +100,7 @@ this.transparent = options.transparent; /** - * Whether the render view should be resized automatically + * Whether css dimensions of canvas view should be resized to screen dimensions automatically * * @member {boolean} */ @@ -192,23 +185,48 @@ } /** - * Resizes the canvas view to the specified width and height + * Same as view.width, actual number of pixels in the canvas by horizontal * - * @param {number} width - the new width of the canvas view - * @param {number} height - the new height of the canvas view + * @member {number} + * @readonly + * @default 800 */ - resize(width, height) + get width() { - this.width = width * this.resolution; - this.height = height * this.resolution; + return this.view.width; + } - this.view.width = this.width; - this.view.height = this.height; + /** + * Same as view.height, actual number of pixels in the canvas by vertical + * + * @member {number} + * @readonly + * @default 600 + */ + get height() + { + return this.view.height; + } + + /** + * Resizes the screen and canvas to the specified width and height + * Canvas dimensions are multiplied by resolution + * + * @param {number} screenWidth - the new width of the screen + * @param {number} screenHeight - the new height of the screen + */ + resize(screenWidth, screenHeight) + { + this.screen.width = screenWidth; + this.screen.height = screenHeight; + + this.view.width = screenWidth * this.resolution; + this.view.height = screenHeight * this.resolution; if (this.autoResize) { - this.view.style.width = `${this.width / this.resolution}px`; - this.view.style.height = `${this.height / this.resolution}px`; + this.view.style.width = `${screenWidth}px`; + this.view.style.height = `${screenHeight}px`; } } @@ -249,11 +267,10 @@ this.type = RENDERER_TYPE.UNKNOWN; - this.width = 0; - this.height = 0; - this.view = null; + this.screen = null; + this.resolution = 0; this.transparent = false; diff --git a/src/core/renderers/canvas/CanvasRenderer.js b/src/core/renderers/canvas/CanvasRenderer.js index 1bd2d5e..e84e49c 100644 --- a/src/core/renderers/canvas/CanvasRenderer.js +++ b/src/core/renderers/canvas/CanvasRenderer.js @@ -18,8 +18,8 @@ export default class CanvasRenderer extends SystemRenderer { /** - * @param {number} [width=800] - the width of the canvas view - * @param {number} [height=600] - the height of the canvas view + * @param {number} [screenWidth=800] - the width of the screen + * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false @@ -34,9 +34,9 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(width, height, options = {}) + constructor(screenWidth, screenHeight, options = {}) { - super('Canvas', width, height, options); + super('Canvas', screenWidth, screenHeight, options); this.type = RENDERER_TYPE.CANVAS; @@ -96,7 +96,7 @@ this.context = null; this.renderingToScreen = false; - this.resize(width, height); + this.resize(screenWidth, screenHeight); } /** @@ -283,12 +283,12 @@ * * @extends PIXI.SystemRenderer#resize * - * @param {number} width - The new width of the canvas view - * @param {number} height - The new height of the canvas view + * @param {number} screenWidth - the new width of the screen + * @param {number} screenHeight - the new height of the screen */ - resize(width, height) + resize(screenWidth, screenHeight) { - super.resize(width, height); + super.resize(screenWidth, screenHeight); // reset the scale mode.. oddly this seems to be reset when the canvas is resized. // surely a browser bug?? Let pixi fix that for you.. diff --git a/src/core/renderers/webgl/TextureManager.js b/src/core/renderers/webgl/TextureManager.js index b685c1a..8227289 100644 --- a/src/core/renderers/webgl/TextureManager.js +++ b/src/core/renderers/webgl/TextureManager.js @@ -61,7 +61,7 @@ * Updates and/or Creates a WebGL texture for the renderer's context. * * @param {PIXI.BaseTexture|PIXI.Texture} texture - the texture to update - * @param {Number} location - the location the texture will be bound to. + * @param {number} location - the location the texture will be bound to. * @return {GLTexture} The gl texture. */ updateTexture(texture, location) diff --git a/src/core/renderers/webgl/WebGLRenderer.js b/src/core/renderers/webgl/WebGLRenderer.js index e4cd8a0..830b382 100644 --- a/src/core/renderers/webgl/WebGLRenderer.js +++ b/src/core/renderers/webgl/WebGLRenderer.js @@ -31,8 +31,8 @@ { /** * - * @param {number} [width=0] - the width of the canvas view - * @param {number} [height=0] - the height of the canvas view + * @param {number} [screenWidth=800] - the width of the screen + * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false @@ -53,9 +53,9 @@ * @param {boolean} [options.legacy=false] - If true Pixi will aim to ensure compatibility * with older / less advanced devices. If you experiance unexplained flickering try setting this to true. */ - constructor(width, height, options = {}) + constructor(screenWidth, screenHeight, options = {}) { - super('WebGL', width, height, options); + super('WebGL', screenWidth, screenHeight, options); this.legacy = !!options.legacy; @@ -245,7 +245,7 @@ glCore._testingContext = gl; // setup the width/height properties and gl viewport - this.resize(this.width, this.height); + this.resize(this.screen.width, this.screen.height); } /** @@ -343,16 +343,16 @@ /** * Resizes the webGL view to the specified width and height. * - * @param {number} width - the new width of the webGL view - * @param {number} height - the new height of the webGL view + * @param {number} screenWidth - the new width of the screen + * @param {number} screenHeight - the new height of the screen */ - resize(width, height) + resize(screenWidth, screenHeight) { // if(width * this.resolution === this.width && height * this.resolution === this.height)return; - SystemRenderer.prototype.resize.call(this, width, height); + SystemRenderer.prototype.resize.call(this, screenWidth, screenHeight); - this.rootRenderTarget.resize(width, height); + this.rootRenderTarget.resize(screenWidth, screenHeight); if (this._activeRenderTarget === this.rootRenderTarget) { @@ -396,6 +396,26 @@ } /** + * Erases the render texture and fills the drawing area with a colour + * + * @param {PIXI.RenderTexture} renderTexture - The render texture to clear + * @param {number} [clearColor] - The colour + * @return {PIXI.WebGLRenderer} Returns itself. + */ + clearRenderTexture(renderTexture, clearColor) + { + const baseTexture = renderTexture.baseTexture; + const renderTarget = baseTexture._glRenderTargets[this.CONTEXT_UID]; + + if (renderTarget) + { + renderTarget.clear(clearColor); + } + + return this; + } + + /** * Binds a render texture for rendering * * @param {PIXI.RenderTexture} renderTexture - The render texture to render @@ -474,9 +494,10 @@ * Changes the current GLShader to the one given in parameter * * @param {PIXI.glCore.Shader} shader - the new glShader + * @param {boolean} [autoProject=true] - Whether automatically set the projection matrix * @return {PIXI.WebGLRenderer} Returns itself. */ - _bindGLShader(shader) + _bindGLShader(shader, autoProject) { // TODO cache if (this._activeShader !== shader) @@ -484,8 +505,14 @@ this._activeShader = shader; shader.bind(); - // automatically set the projection matrix - shader.uniforms.projectionMatrix = this._activeRenderTarget.projectionMatrix.toArray(true); + // `autoProject` normally would be a default parameter set to true + // but because of how Babel transpiles default parameters + // it hinders the performance of this method. + if (autoProject !== false) + { + // automatically set the projection matrix + shader.uniforms.projectionMatrix = this._activeRenderTarget.projectionMatrix.toArray(true); + } } return this; diff --git a/package.json b/package.json index 6a366ba..71ade15 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pixi.js", - "version": "4.3.4", + "version": "4.3.5", "description": "Pixi.js is a fast lightweight 2D library that works across all devices.", "author": "Mat Groves", "contributors": [ @@ -61,7 +61,7 @@ "ismobilejs": "^0.4.0", "object-assign": "^4.0.1", "pixi-gl-core": "^1.0.3", - "resource-loader": "^2.0.4" + "resource-loader": "^2.0.6" }, "devDependencies": { "babel-cli": "^6.18.0", @@ -70,9 +70,9 @@ "babel-preset-es2015": "^6.14.0", "babelify": "^7.3.0", "del": "^2.2.0", - "electron-prebuilt": "^1.3.2", + "electron": "^1.4.15", "eslint": "^3.5.0", - "floss": "^1.2.0", + "floss": "^2.0.1", "gh-pages": "^0.11.0", "jaguarjs-jsdoc": "^1.0.1", "js-md5": "^0.4.1", diff --git a/src/accessibility/AccessibilityManager.js b/src/accessibility/AccessibilityManager.js index df2716a..f16bcb1 100644 --- a/src/accessibility/AccessibilityManager.js +++ b/src/accessibility/AccessibilityManager.js @@ -3,7 +3,7 @@ import accessibleTarget from './accessibleTarget'; // add some extra variables to the container.. -Object.assign( +core.utils.mixins.delayMixin( core.DisplayObject.prototype, accessibleTarget ); diff --git a/src/core/Application.js b/src/core/Application.js index ea0f058..5340578 100644 --- a/src/core/Application.js +++ b/src/core/Application.js @@ -1,6 +1,6 @@ import { autoDetectRenderer } from './autoDetectRenderer'; import Container from './display/Container'; -import Ticker from './ticker/Ticker'; +import { shared, Ticker } from './ticker'; /** * Convenience class to create a new PIXI application. @@ -35,9 +35,9 @@ * @param {boolean} [noWebGL=false] - prevents selection of WebGL renderer, even if such is present * @param {boolean} [options.legacy=false] - If true Pixi will aim to ensure compatibility * with older / less advanced devices. If you experience unexplained flickering try setting this to true. - + * @param {boolean} [useSharedTicker=false] - `true` to use PIXI.ticker.shared, `false` to create new ticker. */ - constructor(width, height, options, noWebGL) + constructor(width, height, options, noWebGL, useSharedTicker = false) { /** * WebGL renderer if available, otherwise CanvasRenderer @@ -46,23 +46,46 @@ this.renderer = autoDetectRenderer(width, height, options, noWebGL); /** - * The root display container that's renderered. + * The root display container that's rendered. * @member {PIXI.Container} */ this.stage = new Container(); /** + * Internal reference to the ticker + * @member {PIXI.ticker.Ticker} + * @private + */ + this._ticker = null; + + /** * Ticker for doing render updates. * @member {PIXI.ticker.Ticker} + * @default PIXI.ticker.shared */ - this.ticker = new Ticker(); - - this.ticker.add(this.render, this); + this.ticker = useSharedTicker ? shared : new Ticker(); // Start the rendering this.start(); } + set ticker(ticker) // eslint-disable-line require-jsdoc + { + if (this._ticker) + { + this._ticker.remove(this.render, this); + } + this._ticker = ticker; + if (ticker) + { + ticker.add(this.render, this); + } + } + get ticker() // eslint-disable-line require-jsdoc + { + return this._ticker; + } + /** * Render the current stage. */ @@ -76,7 +99,7 @@ */ stop() { - this.ticker.stop(); + this._ticker.stop(); } /** @@ -84,7 +107,7 @@ */ start() { - this.ticker.start(); + this._ticker.start(); } /** @@ -98,13 +121,22 @@ } /** + * Reference to the renderer's screen rectangle. Its safe to use as filterArea or hitArea for whole screen + * @member {PIXI.Rectangle} + * @readonly + */ + get screen() + { + return this.renderer.screen; + } + + /** * Destroy and don't use after this. * @param {Boolean} [removeView=false] Automatically remove canvas from DOM. */ destroy(removeView) { this.stop(); - this.ticker.remove(this.render, this); this.ticker = null; this.stage.destroy(); diff --git a/src/core/Shader.js b/src/core/Shader.js new file mode 100644 index 0000000..78b17e6 --- /dev/null +++ b/src/core/Shader.js @@ -0,0 +1,46 @@ +import { GLShader } from 'pixi-gl-core'; +import settings from './settings'; + +function checkPrecision(src, def) +{ + if (src instanceof Array) + { + if (src[0].substring(0, 9) !== 'precision') + { + const copy = src.slice(0); + + copy.unshift(`precision ${def} float;`); + + return copy; + } + } + else if (src.substring(0, 9) !== 'precision') + { + return `precision ${def} float;\n${src}`; + } + + return src; +} + +/** + * Wrapper class, webGL Shader for Pixi. + * Adds precision string if vertexSrc or fragmentSrc have no mention of it. + * + * @class + * @extends GLShader + * @memberof PIXI + */ +export default class Shader extends GLShader +{ + /** + * + * @param {WebGLRenderingContext} gl - The current WebGL rendering context + * @param {string|string[]} vertexSrc - The vertex shader source as an array of strings. + * @param {string|string[]} fragmentSrc - The fragment shader source as an array of strings. + */ + constructor(gl, vertexSrc, fragmentSrc) + { + super(gl, checkPrecision(vertexSrc, settings.PRECISION_VERTEX), + checkPrecision(fragmentSrc, settings.PRECISION_FRAGMENT)); + } +} diff --git a/src/core/const.js b/src/core/const.js index ef6f662..3de0efe 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -188,7 +188,7 @@ * The gc modes that are supported by pixi. * * The {@link PIXI.settings.GC_MODE} Garbage Collection mode for pixi textures is AUTO - * If set to GC_MODE, the renderer will occasianally check textures usage. If they are not + * If set to GC_MODE, the renderer will occasionally check textures usage. If they are not * used for a specified period of time they will be removed from the GPU. They will of course * be uploaded again when they are required. This is a silent behind the scenes process that * should ensure that the GPU does not get filled up. diff --git a/src/core/display/Container.js b/src/core/display/Container.js index ed8cd5f..17fde8d 100644 --- a/src/core/display/Container.js +++ b/src/core/display/Container.js @@ -73,11 +73,12 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.push(child); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -108,11 +109,12 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.splice(index, 0, child); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -225,10 +227,11 @@ if (index === -1) return null; child.parent = null; + // ensure child transform will be recalculated + child.transform._parentID = -1; removeItems(this.children, index, 1); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -249,11 +252,12 @@ { const child = this.getChildAt(index); + // ensure child transform will be recalculated.. child.parent = null; + child.transform._parentID = -1; removeItems(this.children, index, 1); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -284,13 +288,12 @@ for (let i = 0; i < removed.length; ++i) { removed[i].parent = null; + if (removed[i].transform) + { + removed[i].transform._parentID = -1; + } } - // ensure a transform will be recalculated.. - if (this.transform) - { - this.transform._parentID = -1; - } this._boundsID++; this.onChildrenChange(beginIndex); diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index f37f2f5..be4c4fb 100644 --- a/src/core/graphics/Graphics.js +++ b/src/core/graphics/Graphics.js @@ -28,8 +28,9 @@ { /** * + * @param {boolean} [nativeLines=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP */ - constructor() + constructor(nativeLines = false) { super(); @@ -50,6 +51,13 @@ this.lineWidth = 0; /** + * If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * + * @member {boolean} + */ + this.nativeLines = nativeLines; + + /** * The color of any lines drawn. * * @member {string} @@ -757,28 +765,14 @@ if (!this._spriteRect) { - if (!Graphics._SPRITE_TEXTURE) - { - Graphics._SPRITE_TEXTURE = RenderTexture.create(10, 10); - - const canvas = document.createElement('canvas'); - - canvas.width = 10; - canvas.height = 10; - - const context = canvas.getContext('2d'); - - context.fillStyle = 'white'; - context.fillRect(0, 0, 10, 10); - - Graphics._SPRITE_TEXTURE = Texture.fromCanvas(canvas); - } - - this._spriteRect = new Sprite(Graphics._SPRITE_TEXTURE); + this._spriteRect = new Sprite(new Texture(Texture.WHITE)); } + + const sprite = this._spriteRect; + if (this.tint === 0xffffff) { - this._spriteRect.tint = this.graphicsData[0].fillColor; + sprite.tint = this.graphicsData[0].fillColor; } else { @@ -792,20 +786,21 @@ t1[1] *= t2[1]; t1[2] *= t2[2]; - this._spriteRect.tint = rgb2hex(t1); + sprite.tint = rgb2hex(t1); } - this._spriteRect.alpha = this.graphicsData[0].fillAlpha; - this._spriteRect.worldAlpha = this.worldAlpha * this._spriteRect.alpha; + sprite.alpha = this.graphicsData[0].fillAlpha; + sprite.worldAlpha = this.worldAlpha * sprite.alpha; + sprite.blendMode = this.blendMode; - Graphics._SPRITE_TEXTURE._frame.width = rect.width; - Graphics._SPRITE_TEXTURE._frame.height = rect.height; + sprite.texture._frame.width = rect.width; + sprite.texture._frame.height = rect.height; - this._spriteRect.transform.worldTransform = this.transform.worldTransform; + sprite.transform.worldTransform = this.transform.worldTransform; - this._spriteRect.anchor.set(-rect.x / rect.width, -rect.y / rect.height); - this._spriteRect._onAnchorUpdate(); + sprite.anchor.set(-rect.x / rect.width, -rect.y / rect.height); + sprite._onAnchorUpdate(); - this._spriteRect._renderWebGL(renderer); + sprite._renderWebGL(renderer); } /** @@ -1031,6 +1026,7 @@ this.fillColor, this.fillAlpha, this.filling, + this.nativeLines, shape ); diff --git a/src/core/graphics/GraphicsData.js b/src/core/graphics/GraphicsData.js index f5a87ae..b6f03c0 100644 --- a/src/core/graphics/GraphicsData.js +++ b/src/core/graphics/GraphicsData.js @@ -14,14 +14,19 @@ * @param {number} fillColor - the color of the fill * @param {number} fillAlpha - the alpha of the fill * @param {boolean} fill - whether or not the shape is filled with a colour + * @param {boolean} nativeLines - the method for drawing lines * @param {PIXI.Circle|PIXI.Rectangle|PIXI.Ellipse|PIXI.Polygon} shape - The shape object to draw. */ - constructor(lineWidth, lineColor, lineAlpha, fillColor, fillAlpha, fill, shape) + constructor(lineWidth, lineColor, lineAlpha, fillColor, fillAlpha, fill, nativeLines, shape) { /** * @member {number} the width of the line to draw */ this.lineWidth = lineWidth; + /** + * @member {boolean} if true the liens will be draw using LINES instead of TRIANGLE_STRIP + */ + this.nativeLines = nativeLines; /** * @member {number} the color of the line to draw @@ -85,6 +90,7 @@ this.fillColor, this.fillAlpha, this.fill, + this.nativeLines, this.shape ); } diff --git a/src/core/graphics/webgl/GraphicsRenderer.js b/src/core/graphics/webgl/GraphicsRenderer.js index 14083fe..67f5e92 100644 --- a/src/core/graphics/webgl/GraphicsRenderer.js +++ b/src/core/graphics/webgl/GraphicsRenderer.js @@ -102,7 +102,15 @@ shaderTemp.uniforms.alpha = graphics.worldAlpha; renderer.bindVao(webGLData.vao); - webGLData.vao.draw(gl.TRIANGLE_STRIP, webGLData.indices.length); + + if (graphics.nativeLines) + { + gl.drawArrays(gl.LINES, 0, webGLData.points.length / 6); + } + else + { + webGLData.vao.draw(gl.TRIANGLE_STRIP, webGLData.indices.length); + } } } diff --git a/src/core/graphics/webgl/utils/buildLine.js b/src/core/graphics/webgl/utils/buildLine.js index ecf43f7..4d6862e 100644 --- a/src/core/graphics/webgl/utils/buildLine.js +++ b/src/core/graphics/webgl/utils/buildLine.js @@ -11,7 +11,29 @@ * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties * @param {object} webGLData - an object containing all the webGL-specific information to create this shape */ -export default function buildLine(graphicsData, webGLData) +export default function (graphicsData, webGLData) +{ + if (graphicsData.nativeLines) + { + buildNativeLine(graphicsData, webGLData); + } + else + { + buildLine(graphicsData, webGLData); + } +} + +/** + * Builds a line to draw using the poligon method. + * + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @private + * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties + * @param {object} webGLData - an object containing all the webGL-specific information to create this shape + */ +function buildLine(graphicsData, webGLData) { // TODO OPTIMISE! let points = graphicsData.points; @@ -224,3 +246,46 @@ indices.push(indexStart - 1); } + +/** + * Builds a line to draw using the gl.drawArrays(gl.LINES) method + * + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @private + * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties + * @param {object} webGLData - an object containing all the webGL-specific information to create this shape + */ +function buildNativeLine(graphicsData, webGLData) +{ + let i = 0; + const points = graphicsData.points; + + if (points.length === 0) return; + + const verts = webGLData.points; + const length = points.length / 2; + + // sort color + const color = hex2rgb(graphicsData.lineColor); + const alpha = graphicsData.lineAlpha; + const r = color[0] * alpha; + const g = color[1] * alpha; + const b = color[2] * alpha; + + for (i = 1; i < length; i++) + { + const p1x = points[(i - 1) * 2]; + const p1y = points[((i - 1) * 2) + 1]; + + const p2x = points[i * 2]; + const p2y = points[(i * 2) + 1]; + + verts.push(p1x, p1y); + verts.push(r, g, b, alpha); + + verts.push(p2x, p2y); + verts.push(r, g, b, alpha); + } +} diff --git a/src/core/graphics/webgl/utils/buildRoundedRectangle.js b/src/core/graphics/webgl/utils/buildRoundedRectangle.js index 9abc807..a6c8461 100644 --- a/src/core/graphics/webgl/utils/buildRoundedRectangle.js +++ b/src/core/graphics/webgl/utils/buildRoundedRectangle.js @@ -77,6 +77,26 @@ } /** + * Calculate a single point for a quadratic bezier curve. + * Utility function used by quadraticBezierCurve. + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @private + * @param {number} n1 - first number + * @param {number} n2 - second number + * @param {number} perc - percentage + * @return {number} the result + * + */ +function getPt(n1, n2, perc) +{ + const diff = n2 - n1; + + return n1 + (diff * perc); +} + +/** * Calculate the points for a quadratic bezier curve. (helper function..) * Based on: https://stackoverflow.com/questions/785097/how-do-i-implement-a-bezier-curve-in-c * @@ -105,13 +125,6 @@ let x = 0; let y = 0; - function getPt(n1, n2, perc) - { - const diff = n2 - n1; - - return n1 + (diff * perc); - } - for (let i = 0, j = 0; i <= n; ++i) { j = i / n; diff --git a/src/core/index.js b/src/core/index.js index f7a817c..4f414e8 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -30,6 +30,7 @@ export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; export { default as CanvasGraphicsRenderer } from './graphics/canvas/CanvasGraphicsRenderer'; +export { default as Spritesheet } from './textures/Spritesheet'; export { default as Texture } from './textures/Texture'; export { default as BaseTexture } from './textures/BaseTexture'; export { default as RenderTexture } from './textures/RenderTexture'; diff --git a/src/core/math/Matrix.js b/src/core/math/Matrix.js index 0b77477..3679860 100644 --- a/src/core/math/Matrix.js +++ b/src/core/math/Matrix.js @@ -13,45 +13,50 @@ export default class Matrix { /** - * + * @param {number} [a=1] - x scale + * @param {number} [b=0] - y skew + * @param {number} [c=0] - x skew + * @param {number} [d=1] - y scale + * @param {number} [tx=0] - x translation + * @param {number} [ty=0] - y translation */ - constructor() + constructor(a = 1, b = 0, c = 0, d = 1, tx = 0, ty = 0) { /** * @member {number} * @default 1 */ - this.a = 1; + this.a = a; /** * @member {number} * @default 0 */ - this.b = 0; + this.b = b; /** * @member {number} * @default 0 */ - this.c = 0; + this.c = c; /** * @member {number} * @default 1 */ - this.d = 1; + this.d = d; /** * @member {number} * @default 0 */ - this.tx = 0; + this.tx = tx; /** * @member {number} * @default 0 */ - this.ty = 0; + this.ty = ty; this.array = null; } diff --git a/src/core/renderers/SystemRenderer.js b/src/core/renderers/SystemRenderer.js index fa116a6..a24c775 100644 --- a/src/core/renderers/SystemRenderer.js +++ b/src/core/renderers/SystemRenderer.js @@ -1,5 +1,5 @@ import { sayHello, hex2string, hex2rgb } from '../utils'; -import { Matrix } from '../math'; +import { Matrix, Rectangle } from '../math'; import { RENDERER_TYPE } from '../const'; import settings from '../settings'; import Container from '../display/Container'; @@ -21,8 +21,8 @@ { /** * @param {string} system - The name of the system this renderer is for. - * @param {number} [width=800] - the width of the canvas view - * @param {number} [height=600] - the height of the canvas view + * @param {number} [screenWidth=800] - the width of the screen + * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false @@ -37,7 +37,7 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(system, width, height, options) + constructor(system, screenWidth, screenHeight, options) { super(); @@ -69,20 +69,13 @@ this.type = RENDERER_TYPE.UNKNOWN; /** - * The width of the canvas view + * Measurements of the screen. (0, 0, screenWidth, screenHeight) * - * @member {number} - * @default 800 - */ - this.width = width || 800; - - /** - * The height of the canvas view + * Its safe to use as filterArea or hitArea for whole stage * - * @member {number} - * @default 600 + * @member {PIXI.Rectangle} */ - this.height = height || 600; + this.screen = new Rectangle(0, 0, screenWidth || 800, screenHeight || 600); /** * The canvas element that everything is drawn to @@ -107,7 +100,7 @@ this.transparent = options.transparent; /** - * Whether the render view should be resized automatically + * Whether css dimensions of canvas view should be resized to screen dimensions automatically * * @member {boolean} */ @@ -192,23 +185,48 @@ } /** - * Resizes the canvas view to the specified width and height + * Same as view.width, actual number of pixels in the canvas by horizontal * - * @param {number} width - the new width of the canvas view - * @param {number} height - the new height of the canvas view + * @member {number} + * @readonly + * @default 800 */ - resize(width, height) + get width() { - this.width = width * this.resolution; - this.height = height * this.resolution; + return this.view.width; + } - this.view.width = this.width; - this.view.height = this.height; + /** + * Same as view.height, actual number of pixels in the canvas by vertical + * + * @member {number} + * @readonly + * @default 600 + */ + get height() + { + return this.view.height; + } + + /** + * Resizes the screen and canvas to the specified width and height + * Canvas dimensions are multiplied by resolution + * + * @param {number} screenWidth - the new width of the screen + * @param {number} screenHeight - the new height of the screen + */ + resize(screenWidth, screenHeight) + { + this.screen.width = screenWidth; + this.screen.height = screenHeight; + + this.view.width = screenWidth * this.resolution; + this.view.height = screenHeight * this.resolution; if (this.autoResize) { - this.view.style.width = `${this.width / this.resolution}px`; - this.view.style.height = `${this.height / this.resolution}px`; + this.view.style.width = `${screenWidth}px`; + this.view.style.height = `${screenHeight}px`; } } @@ -249,11 +267,10 @@ this.type = RENDERER_TYPE.UNKNOWN; - this.width = 0; - this.height = 0; - this.view = null; + this.screen = null; + this.resolution = 0; this.transparent = false; diff --git a/src/core/renderers/canvas/CanvasRenderer.js b/src/core/renderers/canvas/CanvasRenderer.js index 1bd2d5e..e84e49c 100644 --- a/src/core/renderers/canvas/CanvasRenderer.js +++ b/src/core/renderers/canvas/CanvasRenderer.js @@ -18,8 +18,8 @@ export default class CanvasRenderer extends SystemRenderer { /** - * @param {number} [width=800] - the width of the canvas view - * @param {number} [height=600] - the height of the canvas view + * @param {number} [screenWidth=800] - the width of the screen + * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false @@ -34,9 +34,9 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(width, height, options = {}) + constructor(screenWidth, screenHeight, options = {}) { - super('Canvas', width, height, options); + super('Canvas', screenWidth, screenHeight, options); this.type = RENDERER_TYPE.CANVAS; @@ -96,7 +96,7 @@ this.context = null; this.renderingToScreen = false; - this.resize(width, height); + this.resize(screenWidth, screenHeight); } /** @@ -283,12 +283,12 @@ * * @extends PIXI.SystemRenderer#resize * - * @param {number} width - The new width of the canvas view - * @param {number} height - The new height of the canvas view + * @param {number} screenWidth - the new width of the screen + * @param {number} screenHeight - the new height of the screen */ - resize(width, height) + resize(screenWidth, screenHeight) { - super.resize(width, height); + super.resize(screenWidth, screenHeight); // reset the scale mode.. oddly this seems to be reset when the canvas is resized. // surely a browser bug?? Let pixi fix that for you.. diff --git a/src/core/renderers/webgl/TextureManager.js b/src/core/renderers/webgl/TextureManager.js index b685c1a..8227289 100644 --- a/src/core/renderers/webgl/TextureManager.js +++ b/src/core/renderers/webgl/TextureManager.js @@ -61,7 +61,7 @@ * Updates and/or Creates a WebGL texture for the renderer's context. * * @param {PIXI.BaseTexture|PIXI.Texture} texture - the texture to update - * @param {Number} location - the location the texture will be bound to. + * @param {number} location - the location the texture will be bound to. * @return {GLTexture} The gl texture. */ updateTexture(texture, location) diff --git a/src/core/renderers/webgl/WebGLRenderer.js b/src/core/renderers/webgl/WebGLRenderer.js index e4cd8a0..830b382 100644 --- a/src/core/renderers/webgl/WebGLRenderer.js +++ b/src/core/renderers/webgl/WebGLRenderer.js @@ -31,8 +31,8 @@ { /** * - * @param {number} [width=0] - the width of the canvas view - * @param {number} [height=0] - the height of the canvas view + * @param {number} [screenWidth=800] - the width of the screen + * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false @@ -53,9 +53,9 @@ * @param {boolean} [options.legacy=false] - If true Pixi will aim to ensure compatibility * with older / less advanced devices. If you experiance unexplained flickering try setting this to true. */ - constructor(width, height, options = {}) + constructor(screenWidth, screenHeight, options = {}) { - super('WebGL', width, height, options); + super('WebGL', screenWidth, screenHeight, options); this.legacy = !!options.legacy; @@ -245,7 +245,7 @@ glCore._testingContext = gl; // setup the width/height properties and gl viewport - this.resize(this.width, this.height); + this.resize(this.screen.width, this.screen.height); } /** @@ -343,16 +343,16 @@ /** * Resizes the webGL view to the specified width and height. * - * @param {number} width - the new width of the webGL view - * @param {number} height - the new height of the webGL view + * @param {number} screenWidth - the new width of the screen + * @param {number} screenHeight - the new height of the screen */ - resize(width, height) + resize(screenWidth, screenHeight) { // if(width * this.resolution === this.width && height * this.resolution === this.height)return; - SystemRenderer.prototype.resize.call(this, width, height); + SystemRenderer.prototype.resize.call(this, screenWidth, screenHeight); - this.rootRenderTarget.resize(width, height); + this.rootRenderTarget.resize(screenWidth, screenHeight); if (this._activeRenderTarget === this.rootRenderTarget) { @@ -396,6 +396,26 @@ } /** + * Erases the render texture and fills the drawing area with a colour + * + * @param {PIXI.RenderTexture} renderTexture - The render texture to clear + * @param {number} [clearColor] - The colour + * @return {PIXI.WebGLRenderer} Returns itself. + */ + clearRenderTexture(renderTexture, clearColor) + { + const baseTexture = renderTexture.baseTexture; + const renderTarget = baseTexture._glRenderTargets[this.CONTEXT_UID]; + + if (renderTarget) + { + renderTarget.clear(clearColor); + } + + return this; + } + + /** * Binds a render texture for rendering * * @param {PIXI.RenderTexture} renderTexture - The render texture to render @@ -474,9 +494,10 @@ * Changes the current GLShader to the one given in parameter * * @param {PIXI.glCore.Shader} shader - the new glShader + * @param {boolean} [autoProject=true] - Whether automatically set the projection matrix * @return {PIXI.WebGLRenderer} Returns itself. */ - _bindGLShader(shader) + _bindGLShader(shader, autoProject) { // TODO cache if (this._activeShader !== shader) @@ -484,8 +505,14 @@ this._activeShader = shader; shader.bind(); - // automatically set the projection matrix - shader.uniforms.projectionMatrix = this._activeRenderTarget.projectionMatrix.toArray(true); + // `autoProject` normally would be a default parameter set to true + // but because of how Babel transpiles default parameters + // it hinders the performance of this method. + if (autoProject !== false) + { + // automatically set the projection matrix + shader.uniforms.projectionMatrix = this._activeRenderTarget.projectionMatrix.toArray(true); + } } return this; diff --git a/src/core/renderers/webgl/WebGLState.js b/src/core/renderers/webgl/WebGLState.js index ba6ecc5..6c846d5 100755 --- a/src/core/renderers/webgl/WebGLState.js +++ b/src/core/renderers/webgl/WebGLState.js @@ -82,18 +82,20 @@ push() { // next state.. - let state = this.stack[++this.stackIndex]; + let state = this.stack[this.stackIndex]; if (!state) { state = this.stack[this.stackIndex] = new Uint8Array(16); } + ++this.stackIndex; + // copy state.. // set active state so we can force overrides of gl state for (let i = 0; i < this.activeState.length; i++) { - this.activeState[i] = state[i]; + state[i] = this.activeState[i]; } } diff --git a/package.json b/package.json index 6a366ba..71ade15 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pixi.js", - "version": "4.3.4", + "version": "4.3.5", "description": "Pixi.js is a fast lightweight 2D library that works across all devices.", "author": "Mat Groves", "contributors": [ @@ -61,7 +61,7 @@ "ismobilejs": "^0.4.0", "object-assign": "^4.0.1", "pixi-gl-core": "^1.0.3", - "resource-loader": "^2.0.4" + "resource-loader": "^2.0.6" }, "devDependencies": { "babel-cli": "^6.18.0", @@ -70,9 +70,9 @@ "babel-preset-es2015": "^6.14.0", "babelify": "^7.3.0", "del": "^2.2.0", - "electron-prebuilt": "^1.3.2", + "electron": "^1.4.15", "eslint": "^3.5.0", - "floss": "^1.2.0", + "floss": "^2.0.1", "gh-pages": "^0.11.0", "jaguarjs-jsdoc": "^1.0.1", "js-md5": "^0.4.1", diff --git a/src/accessibility/AccessibilityManager.js b/src/accessibility/AccessibilityManager.js index df2716a..f16bcb1 100644 --- a/src/accessibility/AccessibilityManager.js +++ b/src/accessibility/AccessibilityManager.js @@ -3,7 +3,7 @@ import accessibleTarget from './accessibleTarget'; // add some extra variables to the container.. -Object.assign( +core.utils.mixins.delayMixin( core.DisplayObject.prototype, accessibleTarget ); diff --git a/src/core/Application.js b/src/core/Application.js index ea0f058..5340578 100644 --- a/src/core/Application.js +++ b/src/core/Application.js @@ -1,6 +1,6 @@ import { autoDetectRenderer } from './autoDetectRenderer'; import Container from './display/Container'; -import Ticker from './ticker/Ticker'; +import { shared, Ticker } from './ticker'; /** * Convenience class to create a new PIXI application. @@ -35,9 +35,9 @@ * @param {boolean} [noWebGL=false] - prevents selection of WebGL renderer, even if such is present * @param {boolean} [options.legacy=false] - If true Pixi will aim to ensure compatibility * with older / less advanced devices. If you experience unexplained flickering try setting this to true. - + * @param {boolean} [useSharedTicker=false] - `true` to use PIXI.ticker.shared, `false` to create new ticker. */ - constructor(width, height, options, noWebGL) + constructor(width, height, options, noWebGL, useSharedTicker = false) { /** * WebGL renderer if available, otherwise CanvasRenderer @@ -46,23 +46,46 @@ this.renderer = autoDetectRenderer(width, height, options, noWebGL); /** - * The root display container that's renderered. + * The root display container that's rendered. * @member {PIXI.Container} */ this.stage = new Container(); /** + * Internal reference to the ticker + * @member {PIXI.ticker.Ticker} + * @private + */ + this._ticker = null; + + /** * Ticker for doing render updates. * @member {PIXI.ticker.Ticker} + * @default PIXI.ticker.shared */ - this.ticker = new Ticker(); - - this.ticker.add(this.render, this); + this.ticker = useSharedTicker ? shared : new Ticker(); // Start the rendering this.start(); } + set ticker(ticker) // eslint-disable-line require-jsdoc + { + if (this._ticker) + { + this._ticker.remove(this.render, this); + } + this._ticker = ticker; + if (ticker) + { + ticker.add(this.render, this); + } + } + get ticker() // eslint-disable-line require-jsdoc + { + return this._ticker; + } + /** * Render the current stage. */ @@ -76,7 +99,7 @@ */ stop() { - this.ticker.stop(); + this._ticker.stop(); } /** @@ -84,7 +107,7 @@ */ start() { - this.ticker.start(); + this._ticker.start(); } /** @@ -98,13 +121,22 @@ } /** + * Reference to the renderer's screen rectangle. Its safe to use as filterArea or hitArea for whole screen + * @member {PIXI.Rectangle} + * @readonly + */ + get screen() + { + return this.renderer.screen; + } + + /** * Destroy and don't use after this. * @param {Boolean} [removeView=false] Automatically remove canvas from DOM. */ destroy(removeView) { this.stop(); - this.ticker.remove(this.render, this); this.ticker = null; this.stage.destroy(); diff --git a/src/core/Shader.js b/src/core/Shader.js new file mode 100644 index 0000000..78b17e6 --- /dev/null +++ b/src/core/Shader.js @@ -0,0 +1,46 @@ +import { GLShader } from 'pixi-gl-core'; +import settings from './settings'; + +function checkPrecision(src, def) +{ + if (src instanceof Array) + { + if (src[0].substring(0, 9) !== 'precision') + { + const copy = src.slice(0); + + copy.unshift(`precision ${def} float;`); + + return copy; + } + } + else if (src.substring(0, 9) !== 'precision') + { + return `precision ${def} float;\n${src}`; + } + + return src; +} + +/** + * Wrapper class, webGL Shader for Pixi. + * Adds precision string if vertexSrc or fragmentSrc have no mention of it. + * + * @class + * @extends GLShader + * @memberof PIXI + */ +export default class Shader extends GLShader +{ + /** + * + * @param {WebGLRenderingContext} gl - The current WebGL rendering context + * @param {string|string[]} vertexSrc - The vertex shader source as an array of strings. + * @param {string|string[]} fragmentSrc - The fragment shader source as an array of strings. + */ + constructor(gl, vertexSrc, fragmentSrc) + { + super(gl, checkPrecision(vertexSrc, settings.PRECISION_VERTEX), + checkPrecision(fragmentSrc, settings.PRECISION_FRAGMENT)); + } +} diff --git a/src/core/const.js b/src/core/const.js index ef6f662..3de0efe 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -188,7 +188,7 @@ * The gc modes that are supported by pixi. * * The {@link PIXI.settings.GC_MODE} Garbage Collection mode for pixi textures is AUTO - * If set to GC_MODE, the renderer will occasianally check textures usage. If they are not + * If set to GC_MODE, the renderer will occasionally check textures usage. If they are not * used for a specified period of time they will be removed from the GPU. They will of course * be uploaded again when they are required. This is a silent behind the scenes process that * should ensure that the GPU does not get filled up. diff --git a/src/core/display/Container.js b/src/core/display/Container.js index ed8cd5f..17fde8d 100644 --- a/src/core/display/Container.js +++ b/src/core/display/Container.js @@ -73,11 +73,12 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.push(child); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -108,11 +109,12 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.splice(index, 0, child); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -225,10 +227,11 @@ if (index === -1) return null; child.parent = null; + // ensure child transform will be recalculated + child.transform._parentID = -1; removeItems(this.children, index, 1); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -249,11 +252,12 @@ { const child = this.getChildAt(index); + // ensure child transform will be recalculated.. child.parent = null; + child.transform._parentID = -1; removeItems(this.children, index, 1); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -284,13 +288,12 @@ for (let i = 0; i < removed.length; ++i) { removed[i].parent = null; + if (removed[i].transform) + { + removed[i].transform._parentID = -1; + } } - // ensure a transform will be recalculated.. - if (this.transform) - { - this.transform._parentID = -1; - } this._boundsID++; this.onChildrenChange(beginIndex); diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index f37f2f5..be4c4fb 100644 --- a/src/core/graphics/Graphics.js +++ b/src/core/graphics/Graphics.js @@ -28,8 +28,9 @@ { /** * + * @param {boolean} [nativeLines=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP */ - constructor() + constructor(nativeLines = false) { super(); @@ -50,6 +51,13 @@ this.lineWidth = 0; /** + * If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * + * @member {boolean} + */ + this.nativeLines = nativeLines; + + /** * The color of any lines drawn. * * @member {string} @@ -757,28 +765,14 @@ if (!this._spriteRect) { - if (!Graphics._SPRITE_TEXTURE) - { - Graphics._SPRITE_TEXTURE = RenderTexture.create(10, 10); - - const canvas = document.createElement('canvas'); - - canvas.width = 10; - canvas.height = 10; - - const context = canvas.getContext('2d'); - - context.fillStyle = 'white'; - context.fillRect(0, 0, 10, 10); - - Graphics._SPRITE_TEXTURE = Texture.fromCanvas(canvas); - } - - this._spriteRect = new Sprite(Graphics._SPRITE_TEXTURE); + this._spriteRect = new Sprite(new Texture(Texture.WHITE)); } + + const sprite = this._spriteRect; + if (this.tint === 0xffffff) { - this._spriteRect.tint = this.graphicsData[0].fillColor; + sprite.tint = this.graphicsData[0].fillColor; } else { @@ -792,20 +786,21 @@ t1[1] *= t2[1]; t1[2] *= t2[2]; - this._spriteRect.tint = rgb2hex(t1); + sprite.tint = rgb2hex(t1); } - this._spriteRect.alpha = this.graphicsData[0].fillAlpha; - this._spriteRect.worldAlpha = this.worldAlpha * this._spriteRect.alpha; + sprite.alpha = this.graphicsData[0].fillAlpha; + sprite.worldAlpha = this.worldAlpha * sprite.alpha; + sprite.blendMode = this.blendMode; - Graphics._SPRITE_TEXTURE._frame.width = rect.width; - Graphics._SPRITE_TEXTURE._frame.height = rect.height; + sprite.texture._frame.width = rect.width; + sprite.texture._frame.height = rect.height; - this._spriteRect.transform.worldTransform = this.transform.worldTransform; + sprite.transform.worldTransform = this.transform.worldTransform; - this._spriteRect.anchor.set(-rect.x / rect.width, -rect.y / rect.height); - this._spriteRect._onAnchorUpdate(); + sprite.anchor.set(-rect.x / rect.width, -rect.y / rect.height); + sprite._onAnchorUpdate(); - this._spriteRect._renderWebGL(renderer); + sprite._renderWebGL(renderer); } /** @@ -1031,6 +1026,7 @@ this.fillColor, this.fillAlpha, this.filling, + this.nativeLines, shape ); diff --git a/src/core/graphics/GraphicsData.js b/src/core/graphics/GraphicsData.js index f5a87ae..b6f03c0 100644 --- a/src/core/graphics/GraphicsData.js +++ b/src/core/graphics/GraphicsData.js @@ -14,14 +14,19 @@ * @param {number} fillColor - the color of the fill * @param {number} fillAlpha - the alpha of the fill * @param {boolean} fill - whether or not the shape is filled with a colour + * @param {boolean} nativeLines - the method for drawing lines * @param {PIXI.Circle|PIXI.Rectangle|PIXI.Ellipse|PIXI.Polygon} shape - The shape object to draw. */ - constructor(lineWidth, lineColor, lineAlpha, fillColor, fillAlpha, fill, shape) + constructor(lineWidth, lineColor, lineAlpha, fillColor, fillAlpha, fill, nativeLines, shape) { /** * @member {number} the width of the line to draw */ this.lineWidth = lineWidth; + /** + * @member {boolean} if true the liens will be draw using LINES instead of TRIANGLE_STRIP + */ + this.nativeLines = nativeLines; /** * @member {number} the color of the line to draw @@ -85,6 +90,7 @@ this.fillColor, this.fillAlpha, this.fill, + this.nativeLines, this.shape ); } diff --git a/src/core/graphics/webgl/GraphicsRenderer.js b/src/core/graphics/webgl/GraphicsRenderer.js index 14083fe..67f5e92 100644 --- a/src/core/graphics/webgl/GraphicsRenderer.js +++ b/src/core/graphics/webgl/GraphicsRenderer.js @@ -102,7 +102,15 @@ shaderTemp.uniforms.alpha = graphics.worldAlpha; renderer.bindVao(webGLData.vao); - webGLData.vao.draw(gl.TRIANGLE_STRIP, webGLData.indices.length); + + if (graphics.nativeLines) + { + gl.drawArrays(gl.LINES, 0, webGLData.points.length / 6); + } + else + { + webGLData.vao.draw(gl.TRIANGLE_STRIP, webGLData.indices.length); + } } } diff --git a/src/core/graphics/webgl/utils/buildLine.js b/src/core/graphics/webgl/utils/buildLine.js index ecf43f7..4d6862e 100644 --- a/src/core/graphics/webgl/utils/buildLine.js +++ b/src/core/graphics/webgl/utils/buildLine.js @@ -11,7 +11,29 @@ * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties * @param {object} webGLData - an object containing all the webGL-specific information to create this shape */ -export default function buildLine(graphicsData, webGLData) +export default function (graphicsData, webGLData) +{ + if (graphicsData.nativeLines) + { + buildNativeLine(graphicsData, webGLData); + } + else + { + buildLine(graphicsData, webGLData); + } +} + +/** + * Builds a line to draw using the poligon method. + * + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @private + * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties + * @param {object} webGLData - an object containing all the webGL-specific information to create this shape + */ +function buildLine(graphicsData, webGLData) { // TODO OPTIMISE! let points = graphicsData.points; @@ -224,3 +246,46 @@ indices.push(indexStart - 1); } + +/** + * Builds a line to draw using the gl.drawArrays(gl.LINES) method + * + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @private + * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties + * @param {object} webGLData - an object containing all the webGL-specific information to create this shape + */ +function buildNativeLine(graphicsData, webGLData) +{ + let i = 0; + const points = graphicsData.points; + + if (points.length === 0) return; + + const verts = webGLData.points; + const length = points.length / 2; + + // sort color + const color = hex2rgb(graphicsData.lineColor); + const alpha = graphicsData.lineAlpha; + const r = color[0] * alpha; + const g = color[1] * alpha; + const b = color[2] * alpha; + + for (i = 1; i < length; i++) + { + const p1x = points[(i - 1) * 2]; + const p1y = points[((i - 1) * 2) + 1]; + + const p2x = points[i * 2]; + const p2y = points[(i * 2) + 1]; + + verts.push(p1x, p1y); + verts.push(r, g, b, alpha); + + verts.push(p2x, p2y); + verts.push(r, g, b, alpha); + } +} diff --git a/src/core/graphics/webgl/utils/buildRoundedRectangle.js b/src/core/graphics/webgl/utils/buildRoundedRectangle.js index 9abc807..a6c8461 100644 --- a/src/core/graphics/webgl/utils/buildRoundedRectangle.js +++ b/src/core/graphics/webgl/utils/buildRoundedRectangle.js @@ -77,6 +77,26 @@ } /** + * Calculate a single point for a quadratic bezier curve. + * Utility function used by quadraticBezierCurve. + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @private + * @param {number} n1 - first number + * @param {number} n2 - second number + * @param {number} perc - percentage + * @return {number} the result + * + */ +function getPt(n1, n2, perc) +{ + const diff = n2 - n1; + + return n1 + (diff * perc); +} + +/** * Calculate the points for a quadratic bezier curve. (helper function..) * Based on: https://stackoverflow.com/questions/785097/how-do-i-implement-a-bezier-curve-in-c * @@ -105,13 +125,6 @@ let x = 0; let y = 0; - function getPt(n1, n2, perc) - { - const diff = n2 - n1; - - return n1 + (diff * perc); - } - for (let i = 0, j = 0; i <= n; ++i) { j = i / n; diff --git a/src/core/index.js b/src/core/index.js index f7a817c..4f414e8 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -30,6 +30,7 @@ export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; export { default as CanvasGraphicsRenderer } from './graphics/canvas/CanvasGraphicsRenderer'; +export { default as Spritesheet } from './textures/Spritesheet'; export { default as Texture } from './textures/Texture'; export { default as BaseTexture } from './textures/BaseTexture'; export { default as RenderTexture } from './textures/RenderTexture'; diff --git a/src/core/math/Matrix.js b/src/core/math/Matrix.js index 0b77477..3679860 100644 --- a/src/core/math/Matrix.js +++ b/src/core/math/Matrix.js @@ -13,45 +13,50 @@ export default class Matrix { /** - * + * @param {number} [a=1] - x scale + * @param {number} [b=0] - y skew + * @param {number} [c=0] - x skew + * @param {number} [d=1] - y scale + * @param {number} [tx=0] - x translation + * @param {number} [ty=0] - y translation */ - constructor() + constructor(a = 1, b = 0, c = 0, d = 1, tx = 0, ty = 0) { /** * @member {number} * @default 1 */ - this.a = 1; + this.a = a; /** * @member {number} * @default 0 */ - this.b = 0; + this.b = b; /** * @member {number} * @default 0 */ - this.c = 0; + this.c = c; /** * @member {number} * @default 1 */ - this.d = 1; + this.d = d; /** * @member {number} * @default 0 */ - this.tx = 0; + this.tx = tx; /** * @member {number} * @default 0 */ - this.ty = 0; + this.ty = ty; this.array = null; } diff --git a/src/core/renderers/SystemRenderer.js b/src/core/renderers/SystemRenderer.js index fa116a6..a24c775 100644 --- a/src/core/renderers/SystemRenderer.js +++ b/src/core/renderers/SystemRenderer.js @@ -1,5 +1,5 @@ import { sayHello, hex2string, hex2rgb } from '../utils'; -import { Matrix } from '../math'; +import { Matrix, Rectangle } from '../math'; import { RENDERER_TYPE } from '../const'; import settings from '../settings'; import Container from '../display/Container'; @@ -21,8 +21,8 @@ { /** * @param {string} system - The name of the system this renderer is for. - * @param {number} [width=800] - the width of the canvas view - * @param {number} [height=600] - the height of the canvas view + * @param {number} [screenWidth=800] - the width of the screen + * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false @@ -37,7 +37,7 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(system, width, height, options) + constructor(system, screenWidth, screenHeight, options) { super(); @@ -69,20 +69,13 @@ this.type = RENDERER_TYPE.UNKNOWN; /** - * The width of the canvas view + * Measurements of the screen. (0, 0, screenWidth, screenHeight) * - * @member {number} - * @default 800 - */ - this.width = width || 800; - - /** - * The height of the canvas view + * Its safe to use as filterArea or hitArea for whole stage * - * @member {number} - * @default 600 + * @member {PIXI.Rectangle} */ - this.height = height || 600; + this.screen = new Rectangle(0, 0, screenWidth || 800, screenHeight || 600); /** * The canvas element that everything is drawn to @@ -107,7 +100,7 @@ this.transparent = options.transparent; /** - * Whether the render view should be resized automatically + * Whether css dimensions of canvas view should be resized to screen dimensions automatically * * @member {boolean} */ @@ -192,23 +185,48 @@ } /** - * Resizes the canvas view to the specified width and height + * Same as view.width, actual number of pixels in the canvas by horizontal * - * @param {number} width - the new width of the canvas view - * @param {number} height - the new height of the canvas view + * @member {number} + * @readonly + * @default 800 */ - resize(width, height) + get width() { - this.width = width * this.resolution; - this.height = height * this.resolution; + return this.view.width; + } - this.view.width = this.width; - this.view.height = this.height; + /** + * Same as view.height, actual number of pixels in the canvas by vertical + * + * @member {number} + * @readonly + * @default 600 + */ + get height() + { + return this.view.height; + } + + /** + * Resizes the screen and canvas to the specified width and height + * Canvas dimensions are multiplied by resolution + * + * @param {number} screenWidth - the new width of the screen + * @param {number} screenHeight - the new height of the screen + */ + resize(screenWidth, screenHeight) + { + this.screen.width = screenWidth; + this.screen.height = screenHeight; + + this.view.width = screenWidth * this.resolution; + this.view.height = screenHeight * this.resolution; if (this.autoResize) { - this.view.style.width = `${this.width / this.resolution}px`; - this.view.style.height = `${this.height / this.resolution}px`; + this.view.style.width = `${screenWidth}px`; + this.view.style.height = `${screenHeight}px`; } } @@ -249,11 +267,10 @@ this.type = RENDERER_TYPE.UNKNOWN; - this.width = 0; - this.height = 0; - this.view = null; + this.screen = null; + this.resolution = 0; this.transparent = false; diff --git a/src/core/renderers/canvas/CanvasRenderer.js b/src/core/renderers/canvas/CanvasRenderer.js index 1bd2d5e..e84e49c 100644 --- a/src/core/renderers/canvas/CanvasRenderer.js +++ b/src/core/renderers/canvas/CanvasRenderer.js @@ -18,8 +18,8 @@ export default class CanvasRenderer extends SystemRenderer { /** - * @param {number} [width=800] - the width of the canvas view - * @param {number} [height=600] - the height of the canvas view + * @param {number} [screenWidth=800] - the width of the screen + * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false @@ -34,9 +34,9 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(width, height, options = {}) + constructor(screenWidth, screenHeight, options = {}) { - super('Canvas', width, height, options); + super('Canvas', screenWidth, screenHeight, options); this.type = RENDERER_TYPE.CANVAS; @@ -96,7 +96,7 @@ this.context = null; this.renderingToScreen = false; - this.resize(width, height); + this.resize(screenWidth, screenHeight); } /** @@ -283,12 +283,12 @@ * * @extends PIXI.SystemRenderer#resize * - * @param {number} width - The new width of the canvas view - * @param {number} height - The new height of the canvas view + * @param {number} screenWidth - the new width of the screen + * @param {number} screenHeight - the new height of the screen */ - resize(width, height) + resize(screenWidth, screenHeight) { - super.resize(width, height); + super.resize(screenWidth, screenHeight); // reset the scale mode.. oddly this seems to be reset when the canvas is resized. // surely a browser bug?? Let pixi fix that for you.. diff --git a/src/core/renderers/webgl/TextureManager.js b/src/core/renderers/webgl/TextureManager.js index b685c1a..8227289 100644 --- a/src/core/renderers/webgl/TextureManager.js +++ b/src/core/renderers/webgl/TextureManager.js @@ -61,7 +61,7 @@ * Updates and/or Creates a WebGL texture for the renderer's context. * * @param {PIXI.BaseTexture|PIXI.Texture} texture - the texture to update - * @param {Number} location - the location the texture will be bound to. + * @param {number} location - the location the texture will be bound to. * @return {GLTexture} The gl texture. */ updateTexture(texture, location) diff --git a/src/core/renderers/webgl/WebGLRenderer.js b/src/core/renderers/webgl/WebGLRenderer.js index e4cd8a0..830b382 100644 --- a/src/core/renderers/webgl/WebGLRenderer.js +++ b/src/core/renderers/webgl/WebGLRenderer.js @@ -31,8 +31,8 @@ { /** * - * @param {number} [width=0] - the width of the canvas view - * @param {number} [height=0] - the height of the canvas view + * @param {number} [screenWidth=800] - the width of the screen + * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false @@ -53,9 +53,9 @@ * @param {boolean} [options.legacy=false] - If true Pixi will aim to ensure compatibility * with older / less advanced devices. If you experiance unexplained flickering try setting this to true. */ - constructor(width, height, options = {}) + constructor(screenWidth, screenHeight, options = {}) { - super('WebGL', width, height, options); + super('WebGL', screenWidth, screenHeight, options); this.legacy = !!options.legacy; @@ -245,7 +245,7 @@ glCore._testingContext = gl; // setup the width/height properties and gl viewport - this.resize(this.width, this.height); + this.resize(this.screen.width, this.screen.height); } /** @@ -343,16 +343,16 @@ /** * Resizes the webGL view to the specified width and height. * - * @param {number} width - the new width of the webGL view - * @param {number} height - the new height of the webGL view + * @param {number} screenWidth - the new width of the screen + * @param {number} screenHeight - the new height of the screen */ - resize(width, height) + resize(screenWidth, screenHeight) { // if(width * this.resolution === this.width && height * this.resolution === this.height)return; - SystemRenderer.prototype.resize.call(this, width, height); + SystemRenderer.prototype.resize.call(this, screenWidth, screenHeight); - this.rootRenderTarget.resize(width, height); + this.rootRenderTarget.resize(screenWidth, screenHeight); if (this._activeRenderTarget === this.rootRenderTarget) { @@ -396,6 +396,26 @@ } /** + * Erases the render texture and fills the drawing area with a colour + * + * @param {PIXI.RenderTexture} renderTexture - The render texture to clear + * @param {number} [clearColor] - The colour + * @return {PIXI.WebGLRenderer} Returns itself. + */ + clearRenderTexture(renderTexture, clearColor) + { + const baseTexture = renderTexture.baseTexture; + const renderTarget = baseTexture._glRenderTargets[this.CONTEXT_UID]; + + if (renderTarget) + { + renderTarget.clear(clearColor); + } + + return this; + } + + /** * Binds a render texture for rendering * * @param {PIXI.RenderTexture} renderTexture - The render texture to render @@ -474,9 +494,10 @@ * Changes the current GLShader to the one given in parameter * * @param {PIXI.glCore.Shader} shader - the new glShader + * @param {boolean} [autoProject=true] - Whether automatically set the projection matrix * @return {PIXI.WebGLRenderer} Returns itself. */ - _bindGLShader(shader) + _bindGLShader(shader, autoProject) { // TODO cache if (this._activeShader !== shader) @@ -484,8 +505,14 @@ this._activeShader = shader; shader.bind(); - // automatically set the projection matrix - shader.uniforms.projectionMatrix = this._activeRenderTarget.projectionMatrix.toArray(true); + // `autoProject` normally would be a default parameter set to true + // but because of how Babel transpiles default parameters + // it hinders the performance of this method. + if (autoProject !== false) + { + // automatically set the projection matrix + shader.uniforms.projectionMatrix = this._activeRenderTarget.projectionMatrix.toArray(true); + } } return this; diff --git a/src/core/renderers/webgl/WebGLState.js b/src/core/renderers/webgl/WebGLState.js index ba6ecc5..6c846d5 100755 --- a/src/core/renderers/webgl/WebGLState.js +++ b/src/core/renderers/webgl/WebGLState.js @@ -82,18 +82,20 @@ push() { // next state.. - let state = this.stack[++this.stackIndex]; + let state = this.stack[this.stackIndex]; if (!state) { state = this.stack[this.stackIndex] = new Uint8Array(16); } + ++this.stackIndex; + // copy state.. // set active state so we can force overrides of gl state for (let i = 0; i < this.activeState.length; i++) { - this.activeState[i] = state[i]; + state[i] = this.activeState[i]; } } diff --git a/src/core/renderers/webgl/filters/Filter.js b/src/core/renderers/webgl/filters/Filter.js index 2c3eaad..4eb6128 100644 --- a/src/core/renderers/webgl/filters/Filter.js +++ b/src/core/renderers/webgl/filters/Filter.js @@ -9,7 +9,7 @@ * @memberof PIXI * @extends PIXI.Shader */ -class Filter extends Shader +export default class Filter extends Shader { /** * @param {string} [vertexSrc] - The source of the vertex shader. @@ -56,15 +56,16 @@ * @param {PIXI.RenderTarget} input - The input render target. * @param {PIXI.RenderTarget} output - The target to output to. * @param {boolean} clear - Should the output be cleared before rendering to it + * @param {object} [currentState] - It's current state of filter. + * There are some useful properties in the currentState : + * target, filters, sourceFrame, destinationFrame, renderTarget, resolution */ - apply(filterManager, input, output, clear) + apply(filterManager, input, output, clear, currentState) // eslint-disable-line no-unused-vars { // do as you please! - filterManager.applyFilter(this, input, output, clear); + filterManager.applyFilter(this, input, output, clear, currentState); // or just do a regular render.. } } - -export default Filter; diff --git a/package.json b/package.json index 6a366ba..71ade15 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pixi.js", - "version": "4.3.4", + "version": "4.3.5", "description": "Pixi.js is a fast lightweight 2D library that works across all devices.", "author": "Mat Groves", "contributors": [ @@ -61,7 +61,7 @@ "ismobilejs": "^0.4.0", "object-assign": "^4.0.1", "pixi-gl-core": "^1.0.3", - "resource-loader": "^2.0.4" + "resource-loader": "^2.0.6" }, "devDependencies": { "babel-cli": "^6.18.0", @@ -70,9 +70,9 @@ "babel-preset-es2015": "^6.14.0", "babelify": "^7.3.0", "del": "^2.2.0", - "electron-prebuilt": "^1.3.2", + "electron": "^1.4.15", "eslint": "^3.5.0", - "floss": "^1.2.0", + "floss": "^2.0.1", "gh-pages": "^0.11.0", "jaguarjs-jsdoc": "^1.0.1", "js-md5": "^0.4.1", diff --git a/src/accessibility/AccessibilityManager.js b/src/accessibility/AccessibilityManager.js index df2716a..f16bcb1 100644 --- a/src/accessibility/AccessibilityManager.js +++ b/src/accessibility/AccessibilityManager.js @@ -3,7 +3,7 @@ import accessibleTarget from './accessibleTarget'; // add some extra variables to the container.. -Object.assign( +core.utils.mixins.delayMixin( core.DisplayObject.prototype, accessibleTarget ); diff --git a/src/core/Application.js b/src/core/Application.js index ea0f058..5340578 100644 --- a/src/core/Application.js +++ b/src/core/Application.js @@ -1,6 +1,6 @@ import { autoDetectRenderer } from './autoDetectRenderer'; import Container from './display/Container'; -import Ticker from './ticker/Ticker'; +import { shared, Ticker } from './ticker'; /** * Convenience class to create a new PIXI application. @@ -35,9 +35,9 @@ * @param {boolean} [noWebGL=false] - prevents selection of WebGL renderer, even if such is present * @param {boolean} [options.legacy=false] - If true Pixi will aim to ensure compatibility * with older / less advanced devices. If you experience unexplained flickering try setting this to true. - + * @param {boolean} [useSharedTicker=false] - `true` to use PIXI.ticker.shared, `false` to create new ticker. */ - constructor(width, height, options, noWebGL) + constructor(width, height, options, noWebGL, useSharedTicker = false) { /** * WebGL renderer if available, otherwise CanvasRenderer @@ -46,23 +46,46 @@ this.renderer = autoDetectRenderer(width, height, options, noWebGL); /** - * The root display container that's renderered. + * The root display container that's rendered. * @member {PIXI.Container} */ this.stage = new Container(); /** + * Internal reference to the ticker + * @member {PIXI.ticker.Ticker} + * @private + */ + this._ticker = null; + + /** * Ticker for doing render updates. * @member {PIXI.ticker.Ticker} + * @default PIXI.ticker.shared */ - this.ticker = new Ticker(); - - this.ticker.add(this.render, this); + this.ticker = useSharedTicker ? shared : new Ticker(); // Start the rendering this.start(); } + set ticker(ticker) // eslint-disable-line require-jsdoc + { + if (this._ticker) + { + this._ticker.remove(this.render, this); + } + this._ticker = ticker; + if (ticker) + { + ticker.add(this.render, this); + } + } + get ticker() // eslint-disable-line require-jsdoc + { + return this._ticker; + } + /** * Render the current stage. */ @@ -76,7 +99,7 @@ */ stop() { - this.ticker.stop(); + this._ticker.stop(); } /** @@ -84,7 +107,7 @@ */ start() { - this.ticker.start(); + this._ticker.start(); } /** @@ -98,13 +121,22 @@ } /** + * Reference to the renderer's screen rectangle. Its safe to use as filterArea or hitArea for whole screen + * @member {PIXI.Rectangle} + * @readonly + */ + get screen() + { + return this.renderer.screen; + } + + /** * Destroy and don't use after this. * @param {Boolean} [removeView=false] Automatically remove canvas from DOM. */ destroy(removeView) { this.stop(); - this.ticker.remove(this.render, this); this.ticker = null; this.stage.destroy(); diff --git a/src/core/Shader.js b/src/core/Shader.js new file mode 100644 index 0000000..78b17e6 --- /dev/null +++ b/src/core/Shader.js @@ -0,0 +1,46 @@ +import { GLShader } from 'pixi-gl-core'; +import settings from './settings'; + +function checkPrecision(src, def) +{ + if (src instanceof Array) + { + if (src[0].substring(0, 9) !== 'precision') + { + const copy = src.slice(0); + + copy.unshift(`precision ${def} float;`); + + return copy; + } + } + else if (src.substring(0, 9) !== 'precision') + { + return `precision ${def} float;\n${src}`; + } + + return src; +} + +/** + * Wrapper class, webGL Shader for Pixi. + * Adds precision string if vertexSrc or fragmentSrc have no mention of it. + * + * @class + * @extends GLShader + * @memberof PIXI + */ +export default class Shader extends GLShader +{ + /** + * + * @param {WebGLRenderingContext} gl - The current WebGL rendering context + * @param {string|string[]} vertexSrc - The vertex shader source as an array of strings. + * @param {string|string[]} fragmentSrc - The fragment shader source as an array of strings. + */ + constructor(gl, vertexSrc, fragmentSrc) + { + super(gl, checkPrecision(vertexSrc, settings.PRECISION_VERTEX), + checkPrecision(fragmentSrc, settings.PRECISION_FRAGMENT)); + } +} diff --git a/src/core/const.js b/src/core/const.js index ef6f662..3de0efe 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -188,7 +188,7 @@ * The gc modes that are supported by pixi. * * The {@link PIXI.settings.GC_MODE} Garbage Collection mode for pixi textures is AUTO - * If set to GC_MODE, the renderer will occasianally check textures usage. If they are not + * If set to GC_MODE, the renderer will occasionally check textures usage. If they are not * used for a specified period of time they will be removed from the GPU. They will of course * be uploaded again when they are required. This is a silent behind the scenes process that * should ensure that the GPU does not get filled up. diff --git a/src/core/display/Container.js b/src/core/display/Container.js index ed8cd5f..17fde8d 100644 --- a/src/core/display/Container.js +++ b/src/core/display/Container.js @@ -73,11 +73,12 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.push(child); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -108,11 +109,12 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.splice(index, 0, child); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -225,10 +227,11 @@ if (index === -1) return null; child.parent = null; + // ensure child transform will be recalculated + child.transform._parentID = -1; removeItems(this.children, index, 1); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -249,11 +252,12 @@ { const child = this.getChildAt(index); + // ensure child transform will be recalculated.. child.parent = null; + child.transform._parentID = -1; removeItems(this.children, index, 1); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -284,13 +288,12 @@ for (let i = 0; i < removed.length; ++i) { removed[i].parent = null; + if (removed[i].transform) + { + removed[i].transform._parentID = -1; + } } - // ensure a transform will be recalculated.. - if (this.transform) - { - this.transform._parentID = -1; - } this._boundsID++; this.onChildrenChange(beginIndex); diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index f37f2f5..be4c4fb 100644 --- a/src/core/graphics/Graphics.js +++ b/src/core/graphics/Graphics.js @@ -28,8 +28,9 @@ { /** * + * @param {boolean} [nativeLines=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP */ - constructor() + constructor(nativeLines = false) { super(); @@ -50,6 +51,13 @@ this.lineWidth = 0; /** + * If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * + * @member {boolean} + */ + this.nativeLines = nativeLines; + + /** * The color of any lines drawn. * * @member {string} @@ -757,28 +765,14 @@ if (!this._spriteRect) { - if (!Graphics._SPRITE_TEXTURE) - { - Graphics._SPRITE_TEXTURE = RenderTexture.create(10, 10); - - const canvas = document.createElement('canvas'); - - canvas.width = 10; - canvas.height = 10; - - const context = canvas.getContext('2d'); - - context.fillStyle = 'white'; - context.fillRect(0, 0, 10, 10); - - Graphics._SPRITE_TEXTURE = Texture.fromCanvas(canvas); - } - - this._spriteRect = new Sprite(Graphics._SPRITE_TEXTURE); + this._spriteRect = new Sprite(new Texture(Texture.WHITE)); } + + const sprite = this._spriteRect; + if (this.tint === 0xffffff) { - this._spriteRect.tint = this.graphicsData[0].fillColor; + sprite.tint = this.graphicsData[0].fillColor; } else { @@ -792,20 +786,21 @@ t1[1] *= t2[1]; t1[2] *= t2[2]; - this._spriteRect.tint = rgb2hex(t1); + sprite.tint = rgb2hex(t1); } - this._spriteRect.alpha = this.graphicsData[0].fillAlpha; - this._spriteRect.worldAlpha = this.worldAlpha * this._spriteRect.alpha; + sprite.alpha = this.graphicsData[0].fillAlpha; + sprite.worldAlpha = this.worldAlpha * sprite.alpha; + sprite.blendMode = this.blendMode; - Graphics._SPRITE_TEXTURE._frame.width = rect.width; - Graphics._SPRITE_TEXTURE._frame.height = rect.height; + sprite.texture._frame.width = rect.width; + sprite.texture._frame.height = rect.height; - this._spriteRect.transform.worldTransform = this.transform.worldTransform; + sprite.transform.worldTransform = this.transform.worldTransform; - this._spriteRect.anchor.set(-rect.x / rect.width, -rect.y / rect.height); - this._spriteRect._onAnchorUpdate(); + sprite.anchor.set(-rect.x / rect.width, -rect.y / rect.height); + sprite._onAnchorUpdate(); - this._spriteRect._renderWebGL(renderer); + sprite._renderWebGL(renderer); } /** @@ -1031,6 +1026,7 @@ this.fillColor, this.fillAlpha, this.filling, + this.nativeLines, shape ); diff --git a/src/core/graphics/GraphicsData.js b/src/core/graphics/GraphicsData.js index f5a87ae..b6f03c0 100644 --- a/src/core/graphics/GraphicsData.js +++ b/src/core/graphics/GraphicsData.js @@ -14,14 +14,19 @@ * @param {number} fillColor - the color of the fill * @param {number} fillAlpha - the alpha of the fill * @param {boolean} fill - whether or not the shape is filled with a colour + * @param {boolean} nativeLines - the method for drawing lines * @param {PIXI.Circle|PIXI.Rectangle|PIXI.Ellipse|PIXI.Polygon} shape - The shape object to draw. */ - constructor(lineWidth, lineColor, lineAlpha, fillColor, fillAlpha, fill, shape) + constructor(lineWidth, lineColor, lineAlpha, fillColor, fillAlpha, fill, nativeLines, shape) { /** * @member {number} the width of the line to draw */ this.lineWidth = lineWidth; + /** + * @member {boolean} if true the liens will be draw using LINES instead of TRIANGLE_STRIP + */ + this.nativeLines = nativeLines; /** * @member {number} the color of the line to draw @@ -85,6 +90,7 @@ this.fillColor, this.fillAlpha, this.fill, + this.nativeLines, this.shape ); } diff --git a/src/core/graphics/webgl/GraphicsRenderer.js b/src/core/graphics/webgl/GraphicsRenderer.js index 14083fe..67f5e92 100644 --- a/src/core/graphics/webgl/GraphicsRenderer.js +++ b/src/core/graphics/webgl/GraphicsRenderer.js @@ -102,7 +102,15 @@ shaderTemp.uniforms.alpha = graphics.worldAlpha; renderer.bindVao(webGLData.vao); - webGLData.vao.draw(gl.TRIANGLE_STRIP, webGLData.indices.length); + + if (graphics.nativeLines) + { + gl.drawArrays(gl.LINES, 0, webGLData.points.length / 6); + } + else + { + webGLData.vao.draw(gl.TRIANGLE_STRIP, webGLData.indices.length); + } } } diff --git a/src/core/graphics/webgl/utils/buildLine.js b/src/core/graphics/webgl/utils/buildLine.js index ecf43f7..4d6862e 100644 --- a/src/core/graphics/webgl/utils/buildLine.js +++ b/src/core/graphics/webgl/utils/buildLine.js @@ -11,7 +11,29 @@ * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties * @param {object} webGLData - an object containing all the webGL-specific information to create this shape */ -export default function buildLine(graphicsData, webGLData) +export default function (graphicsData, webGLData) +{ + if (graphicsData.nativeLines) + { + buildNativeLine(graphicsData, webGLData); + } + else + { + buildLine(graphicsData, webGLData); + } +} + +/** + * Builds a line to draw using the poligon method. + * + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @private + * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties + * @param {object} webGLData - an object containing all the webGL-specific information to create this shape + */ +function buildLine(graphicsData, webGLData) { // TODO OPTIMISE! let points = graphicsData.points; @@ -224,3 +246,46 @@ indices.push(indexStart - 1); } + +/** + * Builds a line to draw using the gl.drawArrays(gl.LINES) method + * + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @private + * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties + * @param {object} webGLData - an object containing all the webGL-specific information to create this shape + */ +function buildNativeLine(graphicsData, webGLData) +{ + let i = 0; + const points = graphicsData.points; + + if (points.length === 0) return; + + const verts = webGLData.points; + const length = points.length / 2; + + // sort color + const color = hex2rgb(graphicsData.lineColor); + const alpha = graphicsData.lineAlpha; + const r = color[0] * alpha; + const g = color[1] * alpha; + const b = color[2] * alpha; + + for (i = 1; i < length; i++) + { + const p1x = points[(i - 1) * 2]; + const p1y = points[((i - 1) * 2) + 1]; + + const p2x = points[i * 2]; + const p2y = points[(i * 2) + 1]; + + verts.push(p1x, p1y); + verts.push(r, g, b, alpha); + + verts.push(p2x, p2y); + verts.push(r, g, b, alpha); + } +} diff --git a/src/core/graphics/webgl/utils/buildRoundedRectangle.js b/src/core/graphics/webgl/utils/buildRoundedRectangle.js index 9abc807..a6c8461 100644 --- a/src/core/graphics/webgl/utils/buildRoundedRectangle.js +++ b/src/core/graphics/webgl/utils/buildRoundedRectangle.js @@ -77,6 +77,26 @@ } /** + * Calculate a single point for a quadratic bezier curve. + * Utility function used by quadraticBezierCurve. + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @private + * @param {number} n1 - first number + * @param {number} n2 - second number + * @param {number} perc - percentage + * @return {number} the result + * + */ +function getPt(n1, n2, perc) +{ + const diff = n2 - n1; + + return n1 + (diff * perc); +} + +/** * Calculate the points for a quadratic bezier curve. (helper function..) * Based on: https://stackoverflow.com/questions/785097/how-do-i-implement-a-bezier-curve-in-c * @@ -105,13 +125,6 @@ let x = 0; let y = 0; - function getPt(n1, n2, perc) - { - const diff = n2 - n1; - - return n1 + (diff * perc); - } - for (let i = 0, j = 0; i <= n; ++i) { j = i / n; diff --git a/src/core/index.js b/src/core/index.js index f7a817c..4f414e8 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -30,6 +30,7 @@ export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; export { default as CanvasGraphicsRenderer } from './graphics/canvas/CanvasGraphicsRenderer'; +export { default as Spritesheet } from './textures/Spritesheet'; export { default as Texture } from './textures/Texture'; export { default as BaseTexture } from './textures/BaseTexture'; export { default as RenderTexture } from './textures/RenderTexture'; diff --git a/src/core/math/Matrix.js b/src/core/math/Matrix.js index 0b77477..3679860 100644 --- a/src/core/math/Matrix.js +++ b/src/core/math/Matrix.js @@ -13,45 +13,50 @@ export default class Matrix { /** - * + * @param {number} [a=1] - x scale + * @param {number} [b=0] - y skew + * @param {number} [c=0] - x skew + * @param {number} [d=1] - y scale + * @param {number} [tx=0] - x translation + * @param {number} [ty=0] - y translation */ - constructor() + constructor(a = 1, b = 0, c = 0, d = 1, tx = 0, ty = 0) { /** * @member {number} * @default 1 */ - this.a = 1; + this.a = a; /** * @member {number} * @default 0 */ - this.b = 0; + this.b = b; /** * @member {number} * @default 0 */ - this.c = 0; + this.c = c; /** * @member {number} * @default 1 */ - this.d = 1; + this.d = d; /** * @member {number} * @default 0 */ - this.tx = 0; + this.tx = tx; /** * @member {number} * @default 0 */ - this.ty = 0; + this.ty = ty; this.array = null; } diff --git a/src/core/renderers/SystemRenderer.js b/src/core/renderers/SystemRenderer.js index fa116a6..a24c775 100644 --- a/src/core/renderers/SystemRenderer.js +++ b/src/core/renderers/SystemRenderer.js @@ -1,5 +1,5 @@ import { sayHello, hex2string, hex2rgb } from '../utils'; -import { Matrix } from '../math'; +import { Matrix, Rectangle } from '../math'; import { RENDERER_TYPE } from '../const'; import settings from '../settings'; import Container from '../display/Container'; @@ -21,8 +21,8 @@ { /** * @param {string} system - The name of the system this renderer is for. - * @param {number} [width=800] - the width of the canvas view - * @param {number} [height=600] - the height of the canvas view + * @param {number} [screenWidth=800] - the width of the screen + * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false @@ -37,7 +37,7 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(system, width, height, options) + constructor(system, screenWidth, screenHeight, options) { super(); @@ -69,20 +69,13 @@ this.type = RENDERER_TYPE.UNKNOWN; /** - * The width of the canvas view + * Measurements of the screen. (0, 0, screenWidth, screenHeight) * - * @member {number} - * @default 800 - */ - this.width = width || 800; - - /** - * The height of the canvas view + * Its safe to use as filterArea or hitArea for whole stage * - * @member {number} - * @default 600 + * @member {PIXI.Rectangle} */ - this.height = height || 600; + this.screen = new Rectangle(0, 0, screenWidth || 800, screenHeight || 600); /** * The canvas element that everything is drawn to @@ -107,7 +100,7 @@ this.transparent = options.transparent; /** - * Whether the render view should be resized automatically + * Whether css dimensions of canvas view should be resized to screen dimensions automatically * * @member {boolean} */ @@ -192,23 +185,48 @@ } /** - * Resizes the canvas view to the specified width and height + * Same as view.width, actual number of pixels in the canvas by horizontal * - * @param {number} width - the new width of the canvas view - * @param {number} height - the new height of the canvas view + * @member {number} + * @readonly + * @default 800 */ - resize(width, height) + get width() { - this.width = width * this.resolution; - this.height = height * this.resolution; + return this.view.width; + } - this.view.width = this.width; - this.view.height = this.height; + /** + * Same as view.height, actual number of pixels in the canvas by vertical + * + * @member {number} + * @readonly + * @default 600 + */ + get height() + { + return this.view.height; + } + + /** + * Resizes the screen and canvas to the specified width and height + * Canvas dimensions are multiplied by resolution + * + * @param {number} screenWidth - the new width of the screen + * @param {number} screenHeight - the new height of the screen + */ + resize(screenWidth, screenHeight) + { + this.screen.width = screenWidth; + this.screen.height = screenHeight; + + this.view.width = screenWidth * this.resolution; + this.view.height = screenHeight * this.resolution; if (this.autoResize) { - this.view.style.width = `${this.width / this.resolution}px`; - this.view.style.height = `${this.height / this.resolution}px`; + this.view.style.width = `${screenWidth}px`; + this.view.style.height = `${screenHeight}px`; } } @@ -249,11 +267,10 @@ this.type = RENDERER_TYPE.UNKNOWN; - this.width = 0; - this.height = 0; - this.view = null; + this.screen = null; + this.resolution = 0; this.transparent = false; diff --git a/src/core/renderers/canvas/CanvasRenderer.js b/src/core/renderers/canvas/CanvasRenderer.js index 1bd2d5e..e84e49c 100644 --- a/src/core/renderers/canvas/CanvasRenderer.js +++ b/src/core/renderers/canvas/CanvasRenderer.js @@ -18,8 +18,8 @@ export default class CanvasRenderer extends SystemRenderer { /** - * @param {number} [width=800] - the width of the canvas view - * @param {number} [height=600] - the height of the canvas view + * @param {number} [screenWidth=800] - the width of the screen + * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false @@ -34,9 +34,9 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(width, height, options = {}) + constructor(screenWidth, screenHeight, options = {}) { - super('Canvas', width, height, options); + super('Canvas', screenWidth, screenHeight, options); this.type = RENDERER_TYPE.CANVAS; @@ -96,7 +96,7 @@ this.context = null; this.renderingToScreen = false; - this.resize(width, height); + this.resize(screenWidth, screenHeight); } /** @@ -283,12 +283,12 @@ * * @extends PIXI.SystemRenderer#resize * - * @param {number} width - The new width of the canvas view - * @param {number} height - The new height of the canvas view + * @param {number} screenWidth - the new width of the screen + * @param {number} screenHeight - the new height of the screen */ - resize(width, height) + resize(screenWidth, screenHeight) { - super.resize(width, height); + super.resize(screenWidth, screenHeight); // reset the scale mode.. oddly this seems to be reset when the canvas is resized. // surely a browser bug?? Let pixi fix that for you.. diff --git a/src/core/renderers/webgl/TextureManager.js b/src/core/renderers/webgl/TextureManager.js index b685c1a..8227289 100644 --- a/src/core/renderers/webgl/TextureManager.js +++ b/src/core/renderers/webgl/TextureManager.js @@ -61,7 +61,7 @@ * Updates and/or Creates a WebGL texture for the renderer's context. * * @param {PIXI.BaseTexture|PIXI.Texture} texture - the texture to update - * @param {Number} location - the location the texture will be bound to. + * @param {number} location - the location the texture will be bound to. * @return {GLTexture} The gl texture. */ updateTexture(texture, location) diff --git a/src/core/renderers/webgl/WebGLRenderer.js b/src/core/renderers/webgl/WebGLRenderer.js index e4cd8a0..830b382 100644 --- a/src/core/renderers/webgl/WebGLRenderer.js +++ b/src/core/renderers/webgl/WebGLRenderer.js @@ -31,8 +31,8 @@ { /** * - * @param {number} [width=0] - the width of the canvas view - * @param {number} [height=0] - the height of the canvas view + * @param {number} [screenWidth=800] - the width of the screen + * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false @@ -53,9 +53,9 @@ * @param {boolean} [options.legacy=false] - If true Pixi will aim to ensure compatibility * with older / less advanced devices. If you experiance unexplained flickering try setting this to true. */ - constructor(width, height, options = {}) + constructor(screenWidth, screenHeight, options = {}) { - super('WebGL', width, height, options); + super('WebGL', screenWidth, screenHeight, options); this.legacy = !!options.legacy; @@ -245,7 +245,7 @@ glCore._testingContext = gl; // setup the width/height properties and gl viewport - this.resize(this.width, this.height); + this.resize(this.screen.width, this.screen.height); } /** @@ -343,16 +343,16 @@ /** * Resizes the webGL view to the specified width and height. * - * @param {number} width - the new width of the webGL view - * @param {number} height - the new height of the webGL view + * @param {number} screenWidth - the new width of the screen + * @param {number} screenHeight - the new height of the screen */ - resize(width, height) + resize(screenWidth, screenHeight) { // if(width * this.resolution === this.width && height * this.resolution === this.height)return; - SystemRenderer.prototype.resize.call(this, width, height); + SystemRenderer.prototype.resize.call(this, screenWidth, screenHeight); - this.rootRenderTarget.resize(width, height); + this.rootRenderTarget.resize(screenWidth, screenHeight); if (this._activeRenderTarget === this.rootRenderTarget) { @@ -396,6 +396,26 @@ } /** + * Erases the render texture and fills the drawing area with a colour + * + * @param {PIXI.RenderTexture} renderTexture - The render texture to clear + * @param {number} [clearColor] - The colour + * @return {PIXI.WebGLRenderer} Returns itself. + */ + clearRenderTexture(renderTexture, clearColor) + { + const baseTexture = renderTexture.baseTexture; + const renderTarget = baseTexture._glRenderTargets[this.CONTEXT_UID]; + + if (renderTarget) + { + renderTarget.clear(clearColor); + } + + return this; + } + + /** * Binds a render texture for rendering * * @param {PIXI.RenderTexture} renderTexture - The render texture to render @@ -474,9 +494,10 @@ * Changes the current GLShader to the one given in parameter * * @param {PIXI.glCore.Shader} shader - the new glShader + * @param {boolean} [autoProject=true] - Whether automatically set the projection matrix * @return {PIXI.WebGLRenderer} Returns itself. */ - _bindGLShader(shader) + _bindGLShader(shader, autoProject) { // TODO cache if (this._activeShader !== shader) @@ -484,8 +505,14 @@ this._activeShader = shader; shader.bind(); - // automatically set the projection matrix - shader.uniforms.projectionMatrix = this._activeRenderTarget.projectionMatrix.toArray(true); + // `autoProject` normally would be a default parameter set to true + // but because of how Babel transpiles default parameters + // it hinders the performance of this method. + if (autoProject !== false) + { + // automatically set the projection matrix + shader.uniforms.projectionMatrix = this._activeRenderTarget.projectionMatrix.toArray(true); + } } return this; diff --git a/src/core/renderers/webgl/WebGLState.js b/src/core/renderers/webgl/WebGLState.js index ba6ecc5..6c846d5 100755 --- a/src/core/renderers/webgl/WebGLState.js +++ b/src/core/renderers/webgl/WebGLState.js @@ -82,18 +82,20 @@ push() { // next state.. - let state = this.stack[++this.stackIndex]; + let state = this.stack[this.stackIndex]; if (!state) { state = this.stack[this.stackIndex] = new Uint8Array(16); } + ++this.stackIndex; + // copy state.. // set active state so we can force overrides of gl state for (let i = 0; i < this.activeState.length; i++) { - this.activeState[i] = state[i]; + state[i] = this.activeState[i]; } } diff --git a/src/core/renderers/webgl/filters/Filter.js b/src/core/renderers/webgl/filters/Filter.js index 2c3eaad..4eb6128 100644 --- a/src/core/renderers/webgl/filters/Filter.js +++ b/src/core/renderers/webgl/filters/Filter.js @@ -9,7 +9,7 @@ * @memberof PIXI * @extends PIXI.Shader */ -class Filter extends Shader +export default class Filter extends Shader { /** * @param {string} [vertexSrc] - The source of the vertex shader. @@ -56,15 +56,16 @@ * @param {PIXI.RenderTarget} input - The input render target. * @param {PIXI.RenderTarget} output - The target to output to. * @param {boolean} clear - Should the output be cleared before rendering to it + * @param {object} [currentState] - It's current state of filter. + * There are some useful properties in the currentState : + * target, filters, sourceFrame, destinationFrame, renderTarget, resolution */ - apply(filterManager, input, output, clear) + apply(filterManager, input, output, clear, currentState) // eslint-disable-line no-unused-vars { // do as you please! - filterManager.applyFilter(this, input, output, clear); + filterManager.applyFilter(this, input, output, clear, currentState); // or just do a regular render.. } } - -export default Filter; diff --git a/src/core/renderers/webgl/filters/extractUniformsFromSrc.js b/src/core/renderers/webgl/filters/extractUniformsFromSrc.js index d78a0b1..4893214 100644 --- a/src/core/renderers/webgl/filters/extractUniformsFromSrc.js +++ b/src/core/renderers/webgl/filters/extractUniformsFromSrc.js @@ -12,7 +12,7 @@ function extractUniformsFromString(string) { - const maskRegex = new RegExp('^(projectionMatrix|uSampler|filterArea)$'); + const maskRegex = new RegExp('^(projectionMatrix|uSampler|filterArea|filterClamp)$'); const uniforms = {}; let nameSplit; diff --git a/package.json b/package.json index 6a366ba..71ade15 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pixi.js", - "version": "4.3.4", + "version": "4.3.5", "description": "Pixi.js is a fast lightweight 2D library that works across all devices.", "author": "Mat Groves", "contributors": [ @@ -61,7 +61,7 @@ "ismobilejs": "^0.4.0", "object-assign": "^4.0.1", "pixi-gl-core": "^1.0.3", - "resource-loader": "^2.0.4" + "resource-loader": "^2.0.6" }, "devDependencies": { "babel-cli": "^6.18.0", @@ -70,9 +70,9 @@ "babel-preset-es2015": "^6.14.0", "babelify": "^7.3.0", "del": "^2.2.0", - "electron-prebuilt": "^1.3.2", + "electron": "^1.4.15", "eslint": "^3.5.0", - "floss": "^1.2.0", + "floss": "^2.0.1", "gh-pages": "^0.11.0", "jaguarjs-jsdoc": "^1.0.1", "js-md5": "^0.4.1", diff --git a/src/accessibility/AccessibilityManager.js b/src/accessibility/AccessibilityManager.js index df2716a..f16bcb1 100644 --- a/src/accessibility/AccessibilityManager.js +++ b/src/accessibility/AccessibilityManager.js @@ -3,7 +3,7 @@ import accessibleTarget from './accessibleTarget'; // add some extra variables to the container.. -Object.assign( +core.utils.mixins.delayMixin( core.DisplayObject.prototype, accessibleTarget ); diff --git a/src/core/Application.js b/src/core/Application.js index ea0f058..5340578 100644 --- a/src/core/Application.js +++ b/src/core/Application.js @@ -1,6 +1,6 @@ import { autoDetectRenderer } from './autoDetectRenderer'; import Container from './display/Container'; -import Ticker from './ticker/Ticker'; +import { shared, Ticker } from './ticker'; /** * Convenience class to create a new PIXI application. @@ -35,9 +35,9 @@ * @param {boolean} [noWebGL=false] - prevents selection of WebGL renderer, even if such is present * @param {boolean} [options.legacy=false] - If true Pixi will aim to ensure compatibility * with older / less advanced devices. If you experience unexplained flickering try setting this to true. - + * @param {boolean} [useSharedTicker=false] - `true` to use PIXI.ticker.shared, `false` to create new ticker. */ - constructor(width, height, options, noWebGL) + constructor(width, height, options, noWebGL, useSharedTicker = false) { /** * WebGL renderer if available, otherwise CanvasRenderer @@ -46,23 +46,46 @@ this.renderer = autoDetectRenderer(width, height, options, noWebGL); /** - * The root display container that's renderered. + * The root display container that's rendered. * @member {PIXI.Container} */ this.stage = new Container(); /** + * Internal reference to the ticker + * @member {PIXI.ticker.Ticker} + * @private + */ + this._ticker = null; + + /** * Ticker for doing render updates. * @member {PIXI.ticker.Ticker} + * @default PIXI.ticker.shared */ - this.ticker = new Ticker(); - - this.ticker.add(this.render, this); + this.ticker = useSharedTicker ? shared : new Ticker(); // Start the rendering this.start(); } + set ticker(ticker) // eslint-disable-line require-jsdoc + { + if (this._ticker) + { + this._ticker.remove(this.render, this); + } + this._ticker = ticker; + if (ticker) + { + ticker.add(this.render, this); + } + } + get ticker() // eslint-disable-line require-jsdoc + { + return this._ticker; + } + /** * Render the current stage. */ @@ -76,7 +99,7 @@ */ stop() { - this.ticker.stop(); + this._ticker.stop(); } /** @@ -84,7 +107,7 @@ */ start() { - this.ticker.start(); + this._ticker.start(); } /** @@ -98,13 +121,22 @@ } /** + * Reference to the renderer's screen rectangle. Its safe to use as filterArea or hitArea for whole screen + * @member {PIXI.Rectangle} + * @readonly + */ + get screen() + { + return this.renderer.screen; + } + + /** * Destroy and don't use after this. * @param {Boolean} [removeView=false] Automatically remove canvas from DOM. */ destroy(removeView) { this.stop(); - this.ticker.remove(this.render, this); this.ticker = null; this.stage.destroy(); diff --git a/src/core/Shader.js b/src/core/Shader.js new file mode 100644 index 0000000..78b17e6 --- /dev/null +++ b/src/core/Shader.js @@ -0,0 +1,46 @@ +import { GLShader } from 'pixi-gl-core'; +import settings from './settings'; + +function checkPrecision(src, def) +{ + if (src instanceof Array) + { + if (src[0].substring(0, 9) !== 'precision') + { + const copy = src.slice(0); + + copy.unshift(`precision ${def} float;`); + + return copy; + } + } + else if (src.substring(0, 9) !== 'precision') + { + return `precision ${def} float;\n${src}`; + } + + return src; +} + +/** + * Wrapper class, webGL Shader for Pixi. + * Adds precision string if vertexSrc or fragmentSrc have no mention of it. + * + * @class + * @extends GLShader + * @memberof PIXI + */ +export default class Shader extends GLShader +{ + /** + * + * @param {WebGLRenderingContext} gl - The current WebGL rendering context + * @param {string|string[]} vertexSrc - The vertex shader source as an array of strings. + * @param {string|string[]} fragmentSrc - The fragment shader source as an array of strings. + */ + constructor(gl, vertexSrc, fragmentSrc) + { + super(gl, checkPrecision(vertexSrc, settings.PRECISION_VERTEX), + checkPrecision(fragmentSrc, settings.PRECISION_FRAGMENT)); + } +} diff --git a/src/core/const.js b/src/core/const.js index ef6f662..3de0efe 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -188,7 +188,7 @@ * The gc modes that are supported by pixi. * * The {@link PIXI.settings.GC_MODE} Garbage Collection mode for pixi textures is AUTO - * If set to GC_MODE, the renderer will occasianally check textures usage. If they are not + * If set to GC_MODE, the renderer will occasionally check textures usage. If they are not * used for a specified period of time they will be removed from the GPU. They will of course * be uploaded again when they are required. This is a silent behind the scenes process that * should ensure that the GPU does not get filled up. diff --git a/src/core/display/Container.js b/src/core/display/Container.js index ed8cd5f..17fde8d 100644 --- a/src/core/display/Container.js +++ b/src/core/display/Container.js @@ -73,11 +73,12 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.push(child); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -108,11 +109,12 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.splice(index, 0, child); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -225,10 +227,11 @@ if (index === -1) return null; child.parent = null; + // ensure child transform will be recalculated + child.transform._parentID = -1; removeItems(this.children, index, 1); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -249,11 +252,12 @@ { const child = this.getChildAt(index); + // ensure child transform will be recalculated.. child.parent = null; + child.transform._parentID = -1; removeItems(this.children, index, 1); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -284,13 +288,12 @@ for (let i = 0; i < removed.length; ++i) { removed[i].parent = null; + if (removed[i].transform) + { + removed[i].transform._parentID = -1; + } } - // ensure a transform will be recalculated.. - if (this.transform) - { - this.transform._parentID = -1; - } this._boundsID++; this.onChildrenChange(beginIndex); diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index f37f2f5..be4c4fb 100644 --- a/src/core/graphics/Graphics.js +++ b/src/core/graphics/Graphics.js @@ -28,8 +28,9 @@ { /** * + * @param {boolean} [nativeLines=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP */ - constructor() + constructor(nativeLines = false) { super(); @@ -50,6 +51,13 @@ this.lineWidth = 0; /** + * If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * + * @member {boolean} + */ + this.nativeLines = nativeLines; + + /** * The color of any lines drawn. * * @member {string} @@ -757,28 +765,14 @@ if (!this._spriteRect) { - if (!Graphics._SPRITE_TEXTURE) - { - Graphics._SPRITE_TEXTURE = RenderTexture.create(10, 10); - - const canvas = document.createElement('canvas'); - - canvas.width = 10; - canvas.height = 10; - - const context = canvas.getContext('2d'); - - context.fillStyle = 'white'; - context.fillRect(0, 0, 10, 10); - - Graphics._SPRITE_TEXTURE = Texture.fromCanvas(canvas); - } - - this._spriteRect = new Sprite(Graphics._SPRITE_TEXTURE); + this._spriteRect = new Sprite(new Texture(Texture.WHITE)); } + + const sprite = this._spriteRect; + if (this.tint === 0xffffff) { - this._spriteRect.tint = this.graphicsData[0].fillColor; + sprite.tint = this.graphicsData[0].fillColor; } else { @@ -792,20 +786,21 @@ t1[1] *= t2[1]; t1[2] *= t2[2]; - this._spriteRect.tint = rgb2hex(t1); + sprite.tint = rgb2hex(t1); } - this._spriteRect.alpha = this.graphicsData[0].fillAlpha; - this._spriteRect.worldAlpha = this.worldAlpha * this._spriteRect.alpha; + sprite.alpha = this.graphicsData[0].fillAlpha; + sprite.worldAlpha = this.worldAlpha * sprite.alpha; + sprite.blendMode = this.blendMode; - Graphics._SPRITE_TEXTURE._frame.width = rect.width; - Graphics._SPRITE_TEXTURE._frame.height = rect.height; + sprite.texture._frame.width = rect.width; + sprite.texture._frame.height = rect.height; - this._spriteRect.transform.worldTransform = this.transform.worldTransform; + sprite.transform.worldTransform = this.transform.worldTransform; - this._spriteRect.anchor.set(-rect.x / rect.width, -rect.y / rect.height); - this._spriteRect._onAnchorUpdate(); + sprite.anchor.set(-rect.x / rect.width, -rect.y / rect.height); + sprite._onAnchorUpdate(); - this._spriteRect._renderWebGL(renderer); + sprite._renderWebGL(renderer); } /** @@ -1031,6 +1026,7 @@ this.fillColor, this.fillAlpha, this.filling, + this.nativeLines, shape ); diff --git a/src/core/graphics/GraphicsData.js b/src/core/graphics/GraphicsData.js index f5a87ae..b6f03c0 100644 --- a/src/core/graphics/GraphicsData.js +++ b/src/core/graphics/GraphicsData.js @@ -14,14 +14,19 @@ * @param {number} fillColor - the color of the fill * @param {number} fillAlpha - the alpha of the fill * @param {boolean} fill - whether or not the shape is filled with a colour + * @param {boolean} nativeLines - the method for drawing lines * @param {PIXI.Circle|PIXI.Rectangle|PIXI.Ellipse|PIXI.Polygon} shape - The shape object to draw. */ - constructor(lineWidth, lineColor, lineAlpha, fillColor, fillAlpha, fill, shape) + constructor(lineWidth, lineColor, lineAlpha, fillColor, fillAlpha, fill, nativeLines, shape) { /** * @member {number} the width of the line to draw */ this.lineWidth = lineWidth; + /** + * @member {boolean} if true the liens will be draw using LINES instead of TRIANGLE_STRIP + */ + this.nativeLines = nativeLines; /** * @member {number} the color of the line to draw @@ -85,6 +90,7 @@ this.fillColor, this.fillAlpha, this.fill, + this.nativeLines, this.shape ); } diff --git a/src/core/graphics/webgl/GraphicsRenderer.js b/src/core/graphics/webgl/GraphicsRenderer.js index 14083fe..67f5e92 100644 --- a/src/core/graphics/webgl/GraphicsRenderer.js +++ b/src/core/graphics/webgl/GraphicsRenderer.js @@ -102,7 +102,15 @@ shaderTemp.uniforms.alpha = graphics.worldAlpha; renderer.bindVao(webGLData.vao); - webGLData.vao.draw(gl.TRIANGLE_STRIP, webGLData.indices.length); + + if (graphics.nativeLines) + { + gl.drawArrays(gl.LINES, 0, webGLData.points.length / 6); + } + else + { + webGLData.vao.draw(gl.TRIANGLE_STRIP, webGLData.indices.length); + } } } diff --git a/src/core/graphics/webgl/utils/buildLine.js b/src/core/graphics/webgl/utils/buildLine.js index ecf43f7..4d6862e 100644 --- a/src/core/graphics/webgl/utils/buildLine.js +++ b/src/core/graphics/webgl/utils/buildLine.js @@ -11,7 +11,29 @@ * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties * @param {object} webGLData - an object containing all the webGL-specific information to create this shape */ -export default function buildLine(graphicsData, webGLData) +export default function (graphicsData, webGLData) +{ + if (graphicsData.nativeLines) + { + buildNativeLine(graphicsData, webGLData); + } + else + { + buildLine(graphicsData, webGLData); + } +} + +/** + * Builds a line to draw using the poligon method. + * + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @private + * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties + * @param {object} webGLData - an object containing all the webGL-specific information to create this shape + */ +function buildLine(graphicsData, webGLData) { // TODO OPTIMISE! let points = graphicsData.points; @@ -224,3 +246,46 @@ indices.push(indexStart - 1); } + +/** + * Builds a line to draw using the gl.drawArrays(gl.LINES) method + * + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @private + * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties + * @param {object} webGLData - an object containing all the webGL-specific information to create this shape + */ +function buildNativeLine(graphicsData, webGLData) +{ + let i = 0; + const points = graphicsData.points; + + if (points.length === 0) return; + + const verts = webGLData.points; + const length = points.length / 2; + + // sort color + const color = hex2rgb(graphicsData.lineColor); + const alpha = graphicsData.lineAlpha; + const r = color[0] * alpha; + const g = color[1] * alpha; + const b = color[2] * alpha; + + for (i = 1; i < length; i++) + { + const p1x = points[(i - 1) * 2]; + const p1y = points[((i - 1) * 2) + 1]; + + const p2x = points[i * 2]; + const p2y = points[(i * 2) + 1]; + + verts.push(p1x, p1y); + verts.push(r, g, b, alpha); + + verts.push(p2x, p2y); + verts.push(r, g, b, alpha); + } +} diff --git a/src/core/graphics/webgl/utils/buildRoundedRectangle.js b/src/core/graphics/webgl/utils/buildRoundedRectangle.js index 9abc807..a6c8461 100644 --- a/src/core/graphics/webgl/utils/buildRoundedRectangle.js +++ b/src/core/graphics/webgl/utils/buildRoundedRectangle.js @@ -77,6 +77,26 @@ } /** + * Calculate a single point for a quadratic bezier curve. + * Utility function used by quadraticBezierCurve. + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @private + * @param {number} n1 - first number + * @param {number} n2 - second number + * @param {number} perc - percentage + * @return {number} the result + * + */ +function getPt(n1, n2, perc) +{ + const diff = n2 - n1; + + return n1 + (diff * perc); +} + +/** * Calculate the points for a quadratic bezier curve. (helper function..) * Based on: https://stackoverflow.com/questions/785097/how-do-i-implement-a-bezier-curve-in-c * @@ -105,13 +125,6 @@ let x = 0; let y = 0; - function getPt(n1, n2, perc) - { - const diff = n2 - n1; - - return n1 + (diff * perc); - } - for (let i = 0, j = 0; i <= n; ++i) { j = i / n; diff --git a/src/core/index.js b/src/core/index.js index f7a817c..4f414e8 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -30,6 +30,7 @@ export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; export { default as CanvasGraphicsRenderer } from './graphics/canvas/CanvasGraphicsRenderer'; +export { default as Spritesheet } from './textures/Spritesheet'; export { default as Texture } from './textures/Texture'; export { default as BaseTexture } from './textures/BaseTexture'; export { default as RenderTexture } from './textures/RenderTexture'; diff --git a/src/core/math/Matrix.js b/src/core/math/Matrix.js index 0b77477..3679860 100644 --- a/src/core/math/Matrix.js +++ b/src/core/math/Matrix.js @@ -13,45 +13,50 @@ export default class Matrix { /** - * + * @param {number} [a=1] - x scale + * @param {number} [b=0] - y skew + * @param {number} [c=0] - x skew + * @param {number} [d=1] - y scale + * @param {number} [tx=0] - x translation + * @param {number} [ty=0] - y translation */ - constructor() + constructor(a = 1, b = 0, c = 0, d = 1, tx = 0, ty = 0) { /** * @member {number} * @default 1 */ - this.a = 1; + this.a = a; /** * @member {number} * @default 0 */ - this.b = 0; + this.b = b; /** * @member {number} * @default 0 */ - this.c = 0; + this.c = c; /** * @member {number} * @default 1 */ - this.d = 1; + this.d = d; /** * @member {number} * @default 0 */ - this.tx = 0; + this.tx = tx; /** * @member {number} * @default 0 */ - this.ty = 0; + this.ty = ty; this.array = null; } diff --git a/src/core/renderers/SystemRenderer.js b/src/core/renderers/SystemRenderer.js index fa116a6..a24c775 100644 --- a/src/core/renderers/SystemRenderer.js +++ b/src/core/renderers/SystemRenderer.js @@ -1,5 +1,5 @@ import { sayHello, hex2string, hex2rgb } from '../utils'; -import { Matrix } from '../math'; +import { Matrix, Rectangle } from '../math'; import { RENDERER_TYPE } from '../const'; import settings from '../settings'; import Container from '../display/Container'; @@ -21,8 +21,8 @@ { /** * @param {string} system - The name of the system this renderer is for. - * @param {number} [width=800] - the width of the canvas view - * @param {number} [height=600] - the height of the canvas view + * @param {number} [screenWidth=800] - the width of the screen + * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false @@ -37,7 +37,7 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(system, width, height, options) + constructor(system, screenWidth, screenHeight, options) { super(); @@ -69,20 +69,13 @@ this.type = RENDERER_TYPE.UNKNOWN; /** - * The width of the canvas view + * Measurements of the screen. (0, 0, screenWidth, screenHeight) * - * @member {number} - * @default 800 - */ - this.width = width || 800; - - /** - * The height of the canvas view + * Its safe to use as filterArea or hitArea for whole stage * - * @member {number} - * @default 600 + * @member {PIXI.Rectangle} */ - this.height = height || 600; + this.screen = new Rectangle(0, 0, screenWidth || 800, screenHeight || 600); /** * The canvas element that everything is drawn to @@ -107,7 +100,7 @@ this.transparent = options.transparent; /** - * Whether the render view should be resized automatically + * Whether css dimensions of canvas view should be resized to screen dimensions automatically * * @member {boolean} */ @@ -192,23 +185,48 @@ } /** - * Resizes the canvas view to the specified width and height + * Same as view.width, actual number of pixels in the canvas by horizontal * - * @param {number} width - the new width of the canvas view - * @param {number} height - the new height of the canvas view + * @member {number} + * @readonly + * @default 800 */ - resize(width, height) + get width() { - this.width = width * this.resolution; - this.height = height * this.resolution; + return this.view.width; + } - this.view.width = this.width; - this.view.height = this.height; + /** + * Same as view.height, actual number of pixels in the canvas by vertical + * + * @member {number} + * @readonly + * @default 600 + */ + get height() + { + return this.view.height; + } + + /** + * Resizes the screen and canvas to the specified width and height + * Canvas dimensions are multiplied by resolution + * + * @param {number} screenWidth - the new width of the screen + * @param {number} screenHeight - the new height of the screen + */ + resize(screenWidth, screenHeight) + { + this.screen.width = screenWidth; + this.screen.height = screenHeight; + + this.view.width = screenWidth * this.resolution; + this.view.height = screenHeight * this.resolution; if (this.autoResize) { - this.view.style.width = `${this.width / this.resolution}px`; - this.view.style.height = `${this.height / this.resolution}px`; + this.view.style.width = `${screenWidth}px`; + this.view.style.height = `${screenHeight}px`; } } @@ -249,11 +267,10 @@ this.type = RENDERER_TYPE.UNKNOWN; - this.width = 0; - this.height = 0; - this.view = null; + this.screen = null; + this.resolution = 0; this.transparent = false; diff --git a/src/core/renderers/canvas/CanvasRenderer.js b/src/core/renderers/canvas/CanvasRenderer.js index 1bd2d5e..e84e49c 100644 --- a/src/core/renderers/canvas/CanvasRenderer.js +++ b/src/core/renderers/canvas/CanvasRenderer.js @@ -18,8 +18,8 @@ export default class CanvasRenderer extends SystemRenderer { /** - * @param {number} [width=800] - the width of the canvas view - * @param {number} [height=600] - the height of the canvas view + * @param {number} [screenWidth=800] - the width of the screen + * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false @@ -34,9 +34,9 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(width, height, options = {}) + constructor(screenWidth, screenHeight, options = {}) { - super('Canvas', width, height, options); + super('Canvas', screenWidth, screenHeight, options); this.type = RENDERER_TYPE.CANVAS; @@ -96,7 +96,7 @@ this.context = null; this.renderingToScreen = false; - this.resize(width, height); + this.resize(screenWidth, screenHeight); } /** @@ -283,12 +283,12 @@ * * @extends PIXI.SystemRenderer#resize * - * @param {number} width - The new width of the canvas view - * @param {number} height - The new height of the canvas view + * @param {number} screenWidth - the new width of the screen + * @param {number} screenHeight - the new height of the screen */ - resize(width, height) + resize(screenWidth, screenHeight) { - super.resize(width, height); + super.resize(screenWidth, screenHeight); // reset the scale mode.. oddly this seems to be reset when the canvas is resized. // surely a browser bug?? Let pixi fix that for you.. diff --git a/src/core/renderers/webgl/TextureManager.js b/src/core/renderers/webgl/TextureManager.js index b685c1a..8227289 100644 --- a/src/core/renderers/webgl/TextureManager.js +++ b/src/core/renderers/webgl/TextureManager.js @@ -61,7 +61,7 @@ * Updates and/or Creates a WebGL texture for the renderer's context. * * @param {PIXI.BaseTexture|PIXI.Texture} texture - the texture to update - * @param {Number} location - the location the texture will be bound to. + * @param {number} location - the location the texture will be bound to. * @return {GLTexture} The gl texture. */ updateTexture(texture, location) diff --git a/src/core/renderers/webgl/WebGLRenderer.js b/src/core/renderers/webgl/WebGLRenderer.js index e4cd8a0..830b382 100644 --- a/src/core/renderers/webgl/WebGLRenderer.js +++ b/src/core/renderers/webgl/WebGLRenderer.js @@ -31,8 +31,8 @@ { /** * - * @param {number} [width=0] - the width of the canvas view - * @param {number} [height=0] - the height of the canvas view + * @param {number} [screenWidth=800] - the width of the screen + * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false @@ -53,9 +53,9 @@ * @param {boolean} [options.legacy=false] - If true Pixi will aim to ensure compatibility * with older / less advanced devices. If you experiance unexplained flickering try setting this to true. */ - constructor(width, height, options = {}) + constructor(screenWidth, screenHeight, options = {}) { - super('WebGL', width, height, options); + super('WebGL', screenWidth, screenHeight, options); this.legacy = !!options.legacy; @@ -245,7 +245,7 @@ glCore._testingContext = gl; // setup the width/height properties and gl viewport - this.resize(this.width, this.height); + this.resize(this.screen.width, this.screen.height); } /** @@ -343,16 +343,16 @@ /** * Resizes the webGL view to the specified width and height. * - * @param {number} width - the new width of the webGL view - * @param {number} height - the new height of the webGL view + * @param {number} screenWidth - the new width of the screen + * @param {number} screenHeight - the new height of the screen */ - resize(width, height) + resize(screenWidth, screenHeight) { // if(width * this.resolution === this.width && height * this.resolution === this.height)return; - SystemRenderer.prototype.resize.call(this, width, height); + SystemRenderer.prototype.resize.call(this, screenWidth, screenHeight); - this.rootRenderTarget.resize(width, height); + this.rootRenderTarget.resize(screenWidth, screenHeight); if (this._activeRenderTarget === this.rootRenderTarget) { @@ -396,6 +396,26 @@ } /** + * Erases the render texture and fills the drawing area with a colour + * + * @param {PIXI.RenderTexture} renderTexture - The render texture to clear + * @param {number} [clearColor] - The colour + * @return {PIXI.WebGLRenderer} Returns itself. + */ + clearRenderTexture(renderTexture, clearColor) + { + const baseTexture = renderTexture.baseTexture; + const renderTarget = baseTexture._glRenderTargets[this.CONTEXT_UID]; + + if (renderTarget) + { + renderTarget.clear(clearColor); + } + + return this; + } + + /** * Binds a render texture for rendering * * @param {PIXI.RenderTexture} renderTexture - The render texture to render @@ -474,9 +494,10 @@ * Changes the current GLShader to the one given in parameter * * @param {PIXI.glCore.Shader} shader - the new glShader + * @param {boolean} [autoProject=true] - Whether automatically set the projection matrix * @return {PIXI.WebGLRenderer} Returns itself. */ - _bindGLShader(shader) + _bindGLShader(shader, autoProject) { // TODO cache if (this._activeShader !== shader) @@ -484,8 +505,14 @@ this._activeShader = shader; shader.bind(); - // automatically set the projection matrix - shader.uniforms.projectionMatrix = this._activeRenderTarget.projectionMatrix.toArray(true); + // `autoProject` normally would be a default parameter set to true + // but because of how Babel transpiles default parameters + // it hinders the performance of this method. + if (autoProject !== false) + { + // automatically set the projection matrix + shader.uniforms.projectionMatrix = this._activeRenderTarget.projectionMatrix.toArray(true); + } } return this; diff --git a/src/core/renderers/webgl/WebGLState.js b/src/core/renderers/webgl/WebGLState.js index ba6ecc5..6c846d5 100755 --- a/src/core/renderers/webgl/WebGLState.js +++ b/src/core/renderers/webgl/WebGLState.js @@ -82,18 +82,20 @@ push() { // next state.. - let state = this.stack[++this.stackIndex]; + let state = this.stack[this.stackIndex]; if (!state) { state = this.stack[this.stackIndex] = new Uint8Array(16); } + ++this.stackIndex; + // copy state.. // set active state so we can force overrides of gl state for (let i = 0; i < this.activeState.length; i++) { - this.activeState[i] = state[i]; + state[i] = this.activeState[i]; } } diff --git a/src/core/renderers/webgl/filters/Filter.js b/src/core/renderers/webgl/filters/Filter.js index 2c3eaad..4eb6128 100644 --- a/src/core/renderers/webgl/filters/Filter.js +++ b/src/core/renderers/webgl/filters/Filter.js @@ -9,7 +9,7 @@ * @memberof PIXI * @extends PIXI.Shader */ -class Filter extends Shader +export default class Filter extends Shader { /** * @param {string} [vertexSrc] - The source of the vertex shader. @@ -56,15 +56,16 @@ * @param {PIXI.RenderTarget} input - The input render target. * @param {PIXI.RenderTarget} output - The target to output to. * @param {boolean} clear - Should the output be cleared before rendering to it + * @param {object} [currentState] - It's current state of filter. + * There are some useful properties in the currentState : + * target, filters, sourceFrame, destinationFrame, renderTarget, resolution */ - apply(filterManager, input, output, clear) + apply(filterManager, input, output, clear, currentState) // eslint-disable-line no-unused-vars { // do as you please! - filterManager.applyFilter(this, input, output, clear); + filterManager.applyFilter(this, input, output, clear, currentState); // or just do a regular render.. } } - -export default Filter; diff --git a/src/core/renderers/webgl/filters/extractUniformsFromSrc.js b/src/core/renderers/webgl/filters/extractUniformsFromSrc.js index d78a0b1..4893214 100644 --- a/src/core/renderers/webgl/filters/extractUniformsFromSrc.js +++ b/src/core/renderers/webgl/filters/extractUniformsFromSrc.js @@ -12,7 +12,7 @@ function extractUniformsFromString(string) { - const maskRegex = new RegExp('^(projectionMatrix|uSampler|filterArea)$'); + const maskRegex = new RegExp('^(projectionMatrix|uSampler|filterArea|filterClamp)$'); const uniforms = {}; let nameSplit; diff --git a/src/core/renderers/webgl/managers/FilterManager.js b/src/core/renderers/webgl/managers/FilterManager.js index 43fc869..6c7422d 100644 --- a/src/core/renderers/webgl/managers/FilterManager.js +++ b/src/core/renderers/webgl/managers/FilterManager.js @@ -2,6 +2,7 @@ import RenderTarget from '../utils/RenderTarget'; import Quad from '../utils/Quad'; import { Rectangle } from '../../../math'; +import Shader from '../../../Shader'; import * as filterTransforms from '../filters/filterTransforms'; import bitTwiddle from 'bit-twiddle'; @@ -22,8 +23,6 @@ this.filters = []; this.target = null; this.resolution = 1; - - this.firstRun = true; } } @@ -155,7 +154,7 @@ if (filters.length === 1) { - filters[0].apply(this, currentState.renderTarget, lastState.renderTarget, false); + filters[0].apply(this, currentState.renderTarget, lastState.renderTarget, false, currentState); this.freePotRenderTarget(currentState.renderTarget); } else @@ -177,7 +176,7 @@ for (i = 0; i < filters.length - 1; ++i) { - filters[i].apply(this, flip, flop, true); + filters[i].apply(this, flip, flop, true, currentState); const t = flip; @@ -185,7 +184,7 @@ flop = t; } - filters[i].apply(this, flip, lastState.renderTarget, false); + filters[i].apply(this, flip, lastState.renderTarget, false, currentState); this.freePotRenderTarget(flip); this.freePotRenderTarget(flop); @@ -212,16 +211,28 @@ const renderer = this.renderer; const gl = renderer.gl; - renderer.bindShader(filter, true); + let shader = filter.glShaders[renderer.CONTEXT_UID]; - // TODO theres a better way! - // quad can be a mesh and we then should be able to pull this off without accessing the shader directly - const shader = renderer.shaderManager.getGLShader(); - - // TODO change quad to mesh.. - if (!this.firstRun) + // cacheing.. + if (!shader) { - this.firstRun = true; + if (filter.glShaderKey) + { + shader = this.shaderCache[filter.glShaderKey]; + + if (!shader) + { + shader = new Shader(this.gl, filter.vertexSrc, filter.fragmentSrc); + + filter.glShaders[renderer.CONTEXT_UID] = this.shaderCache[filter.glShaderKey] = shader; + } + } + else + { + shader = filter.glShaders[renderer.CONTEXT_UID] = new Shader(this.gl, filter.vertexSrc, filter.fragmentSrc); + } + + // TODO - this only needs to be done once? renderer.bindVao(null); this.quad.initVao(shader); @@ -244,58 +255,146 @@ renderer.maskManager.pushScissorMask(null, renderer.maskManager.scissorData); } - // renderer._bindGLShader(shader); + renderer.bindShader(shader); - let currentState; - const uniforms = filter.uniforms; + // free unit 0 for us, doesn't matter what was there + // don't try to restore it, because syncUniforms can upload it to another slot + // and it'll be a problem + const tex = this.renderer.emptyTextures[0]; - if (uniforms.filterArea) - { - currentState = this.filterData.stack[this.filterData.index]; - - const filterArea = uniforms.filterArea; - - filterArea[0] = currentState.renderTarget.size.width; - filterArea[1] = currentState.renderTarget.size.height; - filterArea[2] = currentState.sourceFrame.x; - filterArea[3] = currentState.sourceFrame.y; - - filter.uniforms.filterArea = filterArea; - } - - // use this to clamp displaced texture coords so they belong to filterArea - // see displacementFilter fragment shader for an example - if (uniforms.filterClamp) - { - currentState = this.filterData.stack[this.filterData.index]; - - const filterClamp = uniforms.filterClamp; - - filterClamp[0] = 0; - filterClamp[1] = 0; - filterClamp[2] = (currentState.sourceFrame.width - 1) / currentState.renderTarget.size.width; - filterClamp[3] = (currentState.sourceFrame.height - 1) / currentState.renderTarget.size.height; - - filter.uniforms.filterClamp = filterClamp; - } - - renderer.shaderManager.setUniforms(filter.uniforms); + this.renderer.boundTextures[0] = tex; + // this syncs the pixi filters uniforms with glsl uniforms + this.syncUniforms(shader, filter); renderer.state.setBlendMode(filter.blendMode); - // temporary bypass cache.. - const tex = this.renderer.boundTextures[0]; - gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, input.texture.texture); this.quad.vao.draw(this.renderer.gl.TRIANGLES, 6, 0); - // restore cache. gl.bindTexture(gl.TEXTURE_2D, tex._glTextures[this.renderer.CONTEXT_UID].texture); } /** + * Uploads the uniforms of the filter. + * + * @param {GLShader} shader - The underlying gl shader. + * @param {PIXI.Filter} filter - The filter we are synchronizing. + */ + syncUniforms(shader, filter) + { + const uniformData = filter.uniformData; + const uniforms = filter.uniforms; + + // 0 is reserved for the pixi texture so we start at 1! + let textureCount = 1; + let currentState; + + // filterArea and filterClamp that are handled by FilterManager directly + // they must not appear in uniformData + + if (shader.uniforms.filterArea) + { + currentState = this.filterData.stack[this.filterData.index]; + + const filterArea = shader.uniforms.filterArea; + + filterArea[0] = currentState.renderTarget.size.width; + filterArea[1] = currentState.renderTarget.size.height; + filterArea[2] = currentState.sourceFrame.x; + filterArea[3] = currentState.sourceFrame.y; + + shader.uniforms.filterArea = filterArea; + } + + // use this to clamp displaced texture coords so they belong to filterArea + // see displacementFilter fragment shader for an example + if (shader.uniforms.filterClamp) + { + currentState = currentState || this.filterData.stack[this.filterData.index]; + + const filterClamp = shader.uniforms.filterClamp; + + filterClamp[0] = 0; + filterClamp[1] = 0; + filterClamp[2] = (currentState.sourceFrame.width - 1) / currentState.renderTarget.size.width; + filterClamp[3] = (currentState.sourceFrame.height - 1) / currentState.renderTarget.size.height; + + shader.uniforms.filterClamp = filterClamp; + } + + // TODO Cacheing layer.. + for (const i in uniformData) + { + if (uniformData[i].type === 'sampler2D' && uniforms[i] !== 0) + { + if (uniforms[i].baseTexture) + { + shader.uniforms[i] = this.renderer.bindTexture(uniforms[i].baseTexture, textureCount); + } + else + { + shader.uniforms[i] = textureCount; + + // TODO + // this is helpful as renderTargets can also be set. + // Although thinking about it, we could probably + // make the filter texture cache return a RenderTexture + // rather than a renderTarget + const gl = this.renderer.gl; + + this.renderer.boundTextures[textureCount] = this.renderer.emptyTextures[textureCount]; + gl.activeTexture(gl.TEXTURE0 + textureCount); + + uniforms[i].texture.bind(); + } + + textureCount++; + } + else if (uniformData[i].type === 'mat3') + { + // check if its pixi matrix.. + if (uniforms[i].a !== undefined) + { + shader.uniforms[i] = uniforms[i].toArray(true); + } + else + { + shader.uniforms[i] = uniforms[i]; + } + } + else if (uniformData[i].type === 'vec2') + { + // check if its a point.. + if (uniforms[i].x !== undefined) + { + const val = shader.uniforms[i] || new Float32Array(2); + + val[0] = uniforms[i].x; + val[1] = uniforms[i].y; + shader.uniforms[i] = val; + } + else + { + shader.uniforms[i] = uniforms[i]; + } + } + else if (uniformData[i].type === 'float') + { + if (shader.uniforms.data[i].value !== uniformData[i]) + { + shader.uniforms[i] = uniforms[i]; + } + } + else + { + shader.uniforms[i] = uniforms[i]; + } + } + } + + /** * Gets a render target from the pool, or creates a new one. * * @param {boolean} clear - Should we clear the render texture when we get it? @@ -442,7 +541,6 @@ renderTarget.resolution = resolution; renderTarget.defaultFrame.width = renderTarget.size.width = minWidth / resolution; renderTarget.defaultFrame.height = renderTarget.size.height = minHeight / resolution; - renderTarget.attachStencilBuffer(); return renderTarget; } diff --git a/package.json b/package.json index 6a366ba..71ade15 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pixi.js", - "version": "4.3.4", + "version": "4.3.5", "description": "Pixi.js is a fast lightweight 2D library that works across all devices.", "author": "Mat Groves", "contributors": [ @@ -61,7 +61,7 @@ "ismobilejs": "^0.4.0", "object-assign": "^4.0.1", "pixi-gl-core": "^1.0.3", - "resource-loader": "^2.0.4" + "resource-loader": "^2.0.6" }, "devDependencies": { "babel-cli": "^6.18.0", @@ -70,9 +70,9 @@ "babel-preset-es2015": "^6.14.0", "babelify": "^7.3.0", "del": "^2.2.0", - "electron-prebuilt": "^1.3.2", + "electron": "^1.4.15", "eslint": "^3.5.0", - "floss": "^1.2.0", + "floss": "^2.0.1", "gh-pages": "^0.11.0", "jaguarjs-jsdoc": "^1.0.1", "js-md5": "^0.4.1", diff --git a/src/accessibility/AccessibilityManager.js b/src/accessibility/AccessibilityManager.js index df2716a..f16bcb1 100644 --- a/src/accessibility/AccessibilityManager.js +++ b/src/accessibility/AccessibilityManager.js @@ -3,7 +3,7 @@ import accessibleTarget from './accessibleTarget'; // add some extra variables to the container.. -Object.assign( +core.utils.mixins.delayMixin( core.DisplayObject.prototype, accessibleTarget ); diff --git a/src/core/Application.js b/src/core/Application.js index ea0f058..5340578 100644 --- a/src/core/Application.js +++ b/src/core/Application.js @@ -1,6 +1,6 @@ import { autoDetectRenderer } from './autoDetectRenderer'; import Container from './display/Container'; -import Ticker from './ticker/Ticker'; +import { shared, Ticker } from './ticker'; /** * Convenience class to create a new PIXI application. @@ -35,9 +35,9 @@ * @param {boolean} [noWebGL=false] - prevents selection of WebGL renderer, even if such is present * @param {boolean} [options.legacy=false] - If true Pixi will aim to ensure compatibility * with older / less advanced devices. If you experience unexplained flickering try setting this to true. - + * @param {boolean} [useSharedTicker=false] - `true` to use PIXI.ticker.shared, `false` to create new ticker. */ - constructor(width, height, options, noWebGL) + constructor(width, height, options, noWebGL, useSharedTicker = false) { /** * WebGL renderer if available, otherwise CanvasRenderer @@ -46,23 +46,46 @@ this.renderer = autoDetectRenderer(width, height, options, noWebGL); /** - * The root display container that's renderered. + * The root display container that's rendered. * @member {PIXI.Container} */ this.stage = new Container(); /** + * Internal reference to the ticker + * @member {PIXI.ticker.Ticker} + * @private + */ + this._ticker = null; + + /** * Ticker for doing render updates. * @member {PIXI.ticker.Ticker} + * @default PIXI.ticker.shared */ - this.ticker = new Ticker(); - - this.ticker.add(this.render, this); + this.ticker = useSharedTicker ? shared : new Ticker(); // Start the rendering this.start(); } + set ticker(ticker) // eslint-disable-line require-jsdoc + { + if (this._ticker) + { + this._ticker.remove(this.render, this); + } + this._ticker = ticker; + if (ticker) + { + ticker.add(this.render, this); + } + } + get ticker() // eslint-disable-line require-jsdoc + { + return this._ticker; + } + /** * Render the current stage. */ @@ -76,7 +99,7 @@ */ stop() { - this.ticker.stop(); + this._ticker.stop(); } /** @@ -84,7 +107,7 @@ */ start() { - this.ticker.start(); + this._ticker.start(); } /** @@ -98,13 +121,22 @@ } /** + * Reference to the renderer's screen rectangle. Its safe to use as filterArea or hitArea for whole screen + * @member {PIXI.Rectangle} + * @readonly + */ + get screen() + { + return this.renderer.screen; + } + + /** * Destroy and don't use after this. * @param {Boolean} [removeView=false] Automatically remove canvas from DOM. */ destroy(removeView) { this.stop(); - this.ticker.remove(this.render, this); this.ticker = null; this.stage.destroy(); diff --git a/src/core/Shader.js b/src/core/Shader.js new file mode 100644 index 0000000..78b17e6 --- /dev/null +++ b/src/core/Shader.js @@ -0,0 +1,46 @@ +import { GLShader } from 'pixi-gl-core'; +import settings from './settings'; + +function checkPrecision(src, def) +{ + if (src instanceof Array) + { + if (src[0].substring(0, 9) !== 'precision') + { + const copy = src.slice(0); + + copy.unshift(`precision ${def} float;`); + + return copy; + } + } + else if (src.substring(0, 9) !== 'precision') + { + return `precision ${def} float;\n${src}`; + } + + return src; +} + +/** + * Wrapper class, webGL Shader for Pixi. + * Adds precision string if vertexSrc or fragmentSrc have no mention of it. + * + * @class + * @extends GLShader + * @memberof PIXI + */ +export default class Shader extends GLShader +{ + /** + * + * @param {WebGLRenderingContext} gl - The current WebGL rendering context + * @param {string|string[]} vertexSrc - The vertex shader source as an array of strings. + * @param {string|string[]} fragmentSrc - The fragment shader source as an array of strings. + */ + constructor(gl, vertexSrc, fragmentSrc) + { + super(gl, checkPrecision(vertexSrc, settings.PRECISION_VERTEX), + checkPrecision(fragmentSrc, settings.PRECISION_FRAGMENT)); + } +} diff --git a/src/core/const.js b/src/core/const.js index ef6f662..3de0efe 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -188,7 +188,7 @@ * The gc modes that are supported by pixi. * * The {@link PIXI.settings.GC_MODE} Garbage Collection mode for pixi textures is AUTO - * If set to GC_MODE, the renderer will occasianally check textures usage. If they are not + * If set to GC_MODE, the renderer will occasionally check textures usage. If they are not * used for a specified period of time they will be removed from the GPU. They will of course * be uploaded again when they are required. This is a silent behind the scenes process that * should ensure that the GPU does not get filled up. diff --git a/src/core/display/Container.js b/src/core/display/Container.js index ed8cd5f..17fde8d 100644 --- a/src/core/display/Container.js +++ b/src/core/display/Container.js @@ -73,11 +73,12 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.push(child); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -108,11 +109,12 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.splice(index, 0, child); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -225,10 +227,11 @@ if (index === -1) return null; child.parent = null; + // ensure child transform will be recalculated + child.transform._parentID = -1; removeItems(this.children, index, 1); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -249,11 +252,12 @@ { const child = this.getChildAt(index); + // ensure child transform will be recalculated.. child.parent = null; + child.transform._parentID = -1; removeItems(this.children, index, 1); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -284,13 +288,12 @@ for (let i = 0; i < removed.length; ++i) { removed[i].parent = null; + if (removed[i].transform) + { + removed[i].transform._parentID = -1; + } } - // ensure a transform will be recalculated.. - if (this.transform) - { - this.transform._parentID = -1; - } this._boundsID++; this.onChildrenChange(beginIndex); diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index f37f2f5..be4c4fb 100644 --- a/src/core/graphics/Graphics.js +++ b/src/core/graphics/Graphics.js @@ -28,8 +28,9 @@ { /** * + * @param {boolean} [nativeLines=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP */ - constructor() + constructor(nativeLines = false) { super(); @@ -50,6 +51,13 @@ this.lineWidth = 0; /** + * If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * + * @member {boolean} + */ + this.nativeLines = nativeLines; + + /** * The color of any lines drawn. * * @member {string} @@ -757,28 +765,14 @@ if (!this._spriteRect) { - if (!Graphics._SPRITE_TEXTURE) - { - Graphics._SPRITE_TEXTURE = RenderTexture.create(10, 10); - - const canvas = document.createElement('canvas'); - - canvas.width = 10; - canvas.height = 10; - - const context = canvas.getContext('2d'); - - context.fillStyle = 'white'; - context.fillRect(0, 0, 10, 10); - - Graphics._SPRITE_TEXTURE = Texture.fromCanvas(canvas); - } - - this._spriteRect = new Sprite(Graphics._SPRITE_TEXTURE); + this._spriteRect = new Sprite(new Texture(Texture.WHITE)); } + + const sprite = this._spriteRect; + if (this.tint === 0xffffff) { - this._spriteRect.tint = this.graphicsData[0].fillColor; + sprite.tint = this.graphicsData[0].fillColor; } else { @@ -792,20 +786,21 @@ t1[1] *= t2[1]; t1[2] *= t2[2]; - this._spriteRect.tint = rgb2hex(t1); + sprite.tint = rgb2hex(t1); } - this._spriteRect.alpha = this.graphicsData[0].fillAlpha; - this._spriteRect.worldAlpha = this.worldAlpha * this._spriteRect.alpha; + sprite.alpha = this.graphicsData[0].fillAlpha; + sprite.worldAlpha = this.worldAlpha * sprite.alpha; + sprite.blendMode = this.blendMode; - Graphics._SPRITE_TEXTURE._frame.width = rect.width; - Graphics._SPRITE_TEXTURE._frame.height = rect.height; + sprite.texture._frame.width = rect.width; + sprite.texture._frame.height = rect.height; - this._spriteRect.transform.worldTransform = this.transform.worldTransform; + sprite.transform.worldTransform = this.transform.worldTransform; - this._spriteRect.anchor.set(-rect.x / rect.width, -rect.y / rect.height); - this._spriteRect._onAnchorUpdate(); + sprite.anchor.set(-rect.x / rect.width, -rect.y / rect.height); + sprite._onAnchorUpdate(); - this._spriteRect._renderWebGL(renderer); + sprite._renderWebGL(renderer); } /** @@ -1031,6 +1026,7 @@ this.fillColor, this.fillAlpha, this.filling, + this.nativeLines, shape ); diff --git a/src/core/graphics/GraphicsData.js b/src/core/graphics/GraphicsData.js index f5a87ae..b6f03c0 100644 --- a/src/core/graphics/GraphicsData.js +++ b/src/core/graphics/GraphicsData.js @@ -14,14 +14,19 @@ * @param {number} fillColor - the color of the fill * @param {number} fillAlpha - the alpha of the fill * @param {boolean} fill - whether or not the shape is filled with a colour + * @param {boolean} nativeLines - the method for drawing lines * @param {PIXI.Circle|PIXI.Rectangle|PIXI.Ellipse|PIXI.Polygon} shape - The shape object to draw. */ - constructor(lineWidth, lineColor, lineAlpha, fillColor, fillAlpha, fill, shape) + constructor(lineWidth, lineColor, lineAlpha, fillColor, fillAlpha, fill, nativeLines, shape) { /** * @member {number} the width of the line to draw */ this.lineWidth = lineWidth; + /** + * @member {boolean} if true the liens will be draw using LINES instead of TRIANGLE_STRIP + */ + this.nativeLines = nativeLines; /** * @member {number} the color of the line to draw @@ -85,6 +90,7 @@ this.fillColor, this.fillAlpha, this.fill, + this.nativeLines, this.shape ); } diff --git a/src/core/graphics/webgl/GraphicsRenderer.js b/src/core/graphics/webgl/GraphicsRenderer.js index 14083fe..67f5e92 100644 --- a/src/core/graphics/webgl/GraphicsRenderer.js +++ b/src/core/graphics/webgl/GraphicsRenderer.js @@ -102,7 +102,15 @@ shaderTemp.uniforms.alpha = graphics.worldAlpha; renderer.bindVao(webGLData.vao); - webGLData.vao.draw(gl.TRIANGLE_STRIP, webGLData.indices.length); + + if (graphics.nativeLines) + { + gl.drawArrays(gl.LINES, 0, webGLData.points.length / 6); + } + else + { + webGLData.vao.draw(gl.TRIANGLE_STRIP, webGLData.indices.length); + } } } diff --git a/src/core/graphics/webgl/utils/buildLine.js b/src/core/graphics/webgl/utils/buildLine.js index ecf43f7..4d6862e 100644 --- a/src/core/graphics/webgl/utils/buildLine.js +++ b/src/core/graphics/webgl/utils/buildLine.js @@ -11,7 +11,29 @@ * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties * @param {object} webGLData - an object containing all the webGL-specific information to create this shape */ -export default function buildLine(graphicsData, webGLData) +export default function (graphicsData, webGLData) +{ + if (graphicsData.nativeLines) + { + buildNativeLine(graphicsData, webGLData); + } + else + { + buildLine(graphicsData, webGLData); + } +} + +/** + * Builds a line to draw using the poligon method. + * + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @private + * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties + * @param {object} webGLData - an object containing all the webGL-specific information to create this shape + */ +function buildLine(graphicsData, webGLData) { // TODO OPTIMISE! let points = graphicsData.points; @@ -224,3 +246,46 @@ indices.push(indexStart - 1); } + +/** + * Builds a line to draw using the gl.drawArrays(gl.LINES) method + * + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @private + * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties + * @param {object} webGLData - an object containing all the webGL-specific information to create this shape + */ +function buildNativeLine(graphicsData, webGLData) +{ + let i = 0; + const points = graphicsData.points; + + if (points.length === 0) return; + + const verts = webGLData.points; + const length = points.length / 2; + + // sort color + const color = hex2rgb(graphicsData.lineColor); + const alpha = graphicsData.lineAlpha; + const r = color[0] * alpha; + const g = color[1] * alpha; + const b = color[2] * alpha; + + for (i = 1; i < length; i++) + { + const p1x = points[(i - 1) * 2]; + const p1y = points[((i - 1) * 2) + 1]; + + const p2x = points[i * 2]; + const p2y = points[(i * 2) + 1]; + + verts.push(p1x, p1y); + verts.push(r, g, b, alpha); + + verts.push(p2x, p2y); + verts.push(r, g, b, alpha); + } +} diff --git a/src/core/graphics/webgl/utils/buildRoundedRectangle.js b/src/core/graphics/webgl/utils/buildRoundedRectangle.js index 9abc807..a6c8461 100644 --- a/src/core/graphics/webgl/utils/buildRoundedRectangle.js +++ b/src/core/graphics/webgl/utils/buildRoundedRectangle.js @@ -77,6 +77,26 @@ } /** + * Calculate a single point for a quadratic bezier curve. + * Utility function used by quadraticBezierCurve. + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @private + * @param {number} n1 - first number + * @param {number} n2 - second number + * @param {number} perc - percentage + * @return {number} the result + * + */ +function getPt(n1, n2, perc) +{ + const diff = n2 - n1; + + return n1 + (diff * perc); +} + +/** * Calculate the points for a quadratic bezier curve. (helper function..) * Based on: https://stackoverflow.com/questions/785097/how-do-i-implement-a-bezier-curve-in-c * @@ -105,13 +125,6 @@ let x = 0; let y = 0; - function getPt(n1, n2, perc) - { - const diff = n2 - n1; - - return n1 + (diff * perc); - } - for (let i = 0, j = 0; i <= n; ++i) { j = i / n; diff --git a/src/core/index.js b/src/core/index.js index f7a817c..4f414e8 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -30,6 +30,7 @@ export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; export { default as CanvasGraphicsRenderer } from './graphics/canvas/CanvasGraphicsRenderer'; +export { default as Spritesheet } from './textures/Spritesheet'; export { default as Texture } from './textures/Texture'; export { default as BaseTexture } from './textures/BaseTexture'; export { default as RenderTexture } from './textures/RenderTexture'; diff --git a/src/core/math/Matrix.js b/src/core/math/Matrix.js index 0b77477..3679860 100644 --- a/src/core/math/Matrix.js +++ b/src/core/math/Matrix.js @@ -13,45 +13,50 @@ export default class Matrix { /** - * + * @param {number} [a=1] - x scale + * @param {number} [b=0] - y skew + * @param {number} [c=0] - x skew + * @param {number} [d=1] - y scale + * @param {number} [tx=0] - x translation + * @param {number} [ty=0] - y translation */ - constructor() + constructor(a = 1, b = 0, c = 0, d = 1, tx = 0, ty = 0) { /** * @member {number} * @default 1 */ - this.a = 1; + this.a = a; /** * @member {number} * @default 0 */ - this.b = 0; + this.b = b; /** * @member {number} * @default 0 */ - this.c = 0; + this.c = c; /** * @member {number} * @default 1 */ - this.d = 1; + this.d = d; /** * @member {number} * @default 0 */ - this.tx = 0; + this.tx = tx; /** * @member {number} * @default 0 */ - this.ty = 0; + this.ty = ty; this.array = null; } diff --git a/src/core/renderers/SystemRenderer.js b/src/core/renderers/SystemRenderer.js index fa116a6..a24c775 100644 --- a/src/core/renderers/SystemRenderer.js +++ b/src/core/renderers/SystemRenderer.js @@ -1,5 +1,5 @@ import { sayHello, hex2string, hex2rgb } from '../utils'; -import { Matrix } from '../math'; +import { Matrix, Rectangle } from '../math'; import { RENDERER_TYPE } from '../const'; import settings from '../settings'; import Container from '../display/Container'; @@ -21,8 +21,8 @@ { /** * @param {string} system - The name of the system this renderer is for. - * @param {number} [width=800] - the width of the canvas view - * @param {number} [height=600] - the height of the canvas view + * @param {number} [screenWidth=800] - the width of the screen + * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false @@ -37,7 +37,7 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(system, width, height, options) + constructor(system, screenWidth, screenHeight, options) { super(); @@ -69,20 +69,13 @@ this.type = RENDERER_TYPE.UNKNOWN; /** - * The width of the canvas view + * Measurements of the screen. (0, 0, screenWidth, screenHeight) * - * @member {number} - * @default 800 - */ - this.width = width || 800; - - /** - * The height of the canvas view + * Its safe to use as filterArea or hitArea for whole stage * - * @member {number} - * @default 600 + * @member {PIXI.Rectangle} */ - this.height = height || 600; + this.screen = new Rectangle(0, 0, screenWidth || 800, screenHeight || 600); /** * The canvas element that everything is drawn to @@ -107,7 +100,7 @@ this.transparent = options.transparent; /** - * Whether the render view should be resized automatically + * Whether css dimensions of canvas view should be resized to screen dimensions automatically * * @member {boolean} */ @@ -192,23 +185,48 @@ } /** - * Resizes the canvas view to the specified width and height + * Same as view.width, actual number of pixels in the canvas by horizontal * - * @param {number} width - the new width of the canvas view - * @param {number} height - the new height of the canvas view + * @member {number} + * @readonly + * @default 800 */ - resize(width, height) + get width() { - this.width = width * this.resolution; - this.height = height * this.resolution; + return this.view.width; + } - this.view.width = this.width; - this.view.height = this.height; + /** + * Same as view.height, actual number of pixels in the canvas by vertical + * + * @member {number} + * @readonly + * @default 600 + */ + get height() + { + return this.view.height; + } + + /** + * Resizes the screen and canvas to the specified width and height + * Canvas dimensions are multiplied by resolution + * + * @param {number} screenWidth - the new width of the screen + * @param {number} screenHeight - the new height of the screen + */ + resize(screenWidth, screenHeight) + { + this.screen.width = screenWidth; + this.screen.height = screenHeight; + + this.view.width = screenWidth * this.resolution; + this.view.height = screenHeight * this.resolution; if (this.autoResize) { - this.view.style.width = `${this.width / this.resolution}px`; - this.view.style.height = `${this.height / this.resolution}px`; + this.view.style.width = `${screenWidth}px`; + this.view.style.height = `${screenHeight}px`; } } @@ -249,11 +267,10 @@ this.type = RENDERER_TYPE.UNKNOWN; - this.width = 0; - this.height = 0; - this.view = null; + this.screen = null; + this.resolution = 0; this.transparent = false; diff --git a/src/core/renderers/canvas/CanvasRenderer.js b/src/core/renderers/canvas/CanvasRenderer.js index 1bd2d5e..e84e49c 100644 --- a/src/core/renderers/canvas/CanvasRenderer.js +++ b/src/core/renderers/canvas/CanvasRenderer.js @@ -18,8 +18,8 @@ export default class CanvasRenderer extends SystemRenderer { /** - * @param {number} [width=800] - the width of the canvas view - * @param {number} [height=600] - the height of the canvas view + * @param {number} [screenWidth=800] - the width of the screen + * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false @@ -34,9 +34,9 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(width, height, options = {}) + constructor(screenWidth, screenHeight, options = {}) { - super('Canvas', width, height, options); + super('Canvas', screenWidth, screenHeight, options); this.type = RENDERER_TYPE.CANVAS; @@ -96,7 +96,7 @@ this.context = null; this.renderingToScreen = false; - this.resize(width, height); + this.resize(screenWidth, screenHeight); } /** @@ -283,12 +283,12 @@ * * @extends PIXI.SystemRenderer#resize * - * @param {number} width - The new width of the canvas view - * @param {number} height - The new height of the canvas view + * @param {number} screenWidth - the new width of the screen + * @param {number} screenHeight - the new height of the screen */ - resize(width, height) + resize(screenWidth, screenHeight) { - super.resize(width, height); + super.resize(screenWidth, screenHeight); // reset the scale mode.. oddly this seems to be reset when the canvas is resized. // surely a browser bug?? Let pixi fix that for you.. diff --git a/src/core/renderers/webgl/TextureManager.js b/src/core/renderers/webgl/TextureManager.js index b685c1a..8227289 100644 --- a/src/core/renderers/webgl/TextureManager.js +++ b/src/core/renderers/webgl/TextureManager.js @@ -61,7 +61,7 @@ * Updates and/or Creates a WebGL texture for the renderer's context. * * @param {PIXI.BaseTexture|PIXI.Texture} texture - the texture to update - * @param {Number} location - the location the texture will be bound to. + * @param {number} location - the location the texture will be bound to. * @return {GLTexture} The gl texture. */ updateTexture(texture, location) diff --git a/src/core/renderers/webgl/WebGLRenderer.js b/src/core/renderers/webgl/WebGLRenderer.js index e4cd8a0..830b382 100644 --- a/src/core/renderers/webgl/WebGLRenderer.js +++ b/src/core/renderers/webgl/WebGLRenderer.js @@ -31,8 +31,8 @@ { /** * - * @param {number} [width=0] - the width of the canvas view - * @param {number} [height=0] - the height of the canvas view + * @param {number} [screenWidth=800] - the width of the screen + * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false @@ -53,9 +53,9 @@ * @param {boolean} [options.legacy=false] - If true Pixi will aim to ensure compatibility * with older / less advanced devices. If you experiance unexplained flickering try setting this to true. */ - constructor(width, height, options = {}) + constructor(screenWidth, screenHeight, options = {}) { - super('WebGL', width, height, options); + super('WebGL', screenWidth, screenHeight, options); this.legacy = !!options.legacy; @@ -245,7 +245,7 @@ glCore._testingContext = gl; // setup the width/height properties and gl viewport - this.resize(this.width, this.height); + this.resize(this.screen.width, this.screen.height); } /** @@ -343,16 +343,16 @@ /** * Resizes the webGL view to the specified width and height. * - * @param {number} width - the new width of the webGL view - * @param {number} height - the new height of the webGL view + * @param {number} screenWidth - the new width of the screen + * @param {number} screenHeight - the new height of the screen */ - resize(width, height) + resize(screenWidth, screenHeight) { // if(width * this.resolution === this.width && height * this.resolution === this.height)return; - SystemRenderer.prototype.resize.call(this, width, height); + SystemRenderer.prototype.resize.call(this, screenWidth, screenHeight); - this.rootRenderTarget.resize(width, height); + this.rootRenderTarget.resize(screenWidth, screenHeight); if (this._activeRenderTarget === this.rootRenderTarget) { @@ -396,6 +396,26 @@ } /** + * Erases the render texture and fills the drawing area with a colour + * + * @param {PIXI.RenderTexture} renderTexture - The render texture to clear + * @param {number} [clearColor] - The colour + * @return {PIXI.WebGLRenderer} Returns itself. + */ + clearRenderTexture(renderTexture, clearColor) + { + const baseTexture = renderTexture.baseTexture; + const renderTarget = baseTexture._glRenderTargets[this.CONTEXT_UID]; + + if (renderTarget) + { + renderTarget.clear(clearColor); + } + + return this; + } + + /** * Binds a render texture for rendering * * @param {PIXI.RenderTexture} renderTexture - The render texture to render @@ -474,9 +494,10 @@ * Changes the current GLShader to the one given in parameter * * @param {PIXI.glCore.Shader} shader - the new glShader + * @param {boolean} [autoProject=true] - Whether automatically set the projection matrix * @return {PIXI.WebGLRenderer} Returns itself. */ - _bindGLShader(shader) + _bindGLShader(shader, autoProject) { // TODO cache if (this._activeShader !== shader) @@ -484,8 +505,14 @@ this._activeShader = shader; shader.bind(); - // automatically set the projection matrix - shader.uniforms.projectionMatrix = this._activeRenderTarget.projectionMatrix.toArray(true); + // `autoProject` normally would be a default parameter set to true + // but because of how Babel transpiles default parameters + // it hinders the performance of this method. + if (autoProject !== false) + { + // automatically set the projection matrix + shader.uniforms.projectionMatrix = this._activeRenderTarget.projectionMatrix.toArray(true); + } } return this; diff --git a/src/core/renderers/webgl/WebGLState.js b/src/core/renderers/webgl/WebGLState.js index ba6ecc5..6c846d5 100755 --- a/src/core/renderers/webgl/WebGLState.js +++ b/src/core/renderers/webgl/WebGLState.js @@ -82,18 +82,20 @@ push() { // next state.. - let state = this.stack[++this.stackIndex]; + let state = this.stack[this.stackIndex]; if (!state) { state = this.stack[this.stackIndex] = new Uint8Array(16); } + ++this.stackIndex; + // copy state.. // set active state so we can force overrides of gl state for (let i = 0; i < this.activeState.length; i++) { - this.activeState[i] = state[i]; + state[i] = this.activeState[i]; } } diff --git a/src/core/renderers/webgl/filters/Filter.js b/src/core/renderers/webgl/filters/Filter.js index 2c3eaad..4eb6128 100644 --- a/src/core/renderers/webgl/filters/Filter.js +++ b/src/core/renderers/webgl/filters/Filter.js @@ -9,7 +9,7 @@ * @memberof PIXI * @extends PIXI.Shader */ -class Filter extends Shader +export default class Filter extends Shader { /** * @param {string} [vertexSrc] - The source of the vertex shader. @@ -56,15 +56,16 @@ * @param {PIXI.RenderTarget} input - The input render target. * @param {PIXI.RenderTarget} output - The target to output to. * @param {boolean} clear - Should the output be cleared before rendering to it + * @param {object} [currentState] - It's current state of filter. + * There are some useful properties in the currentState : + * target, filters, sourceFrame, destinationFrame, renderTarget, resolution */ - apply(filterManager, input, output, clear) + apply(filterManager, input, output, clear, currentState) // eslint-disable-line no-unused-vars { // do as you please! - filterManager.applyFilter(this, input, output, clear); + filterManager.applyFilter(this, input, output, clear, currentState); // or just do a regular render.. } } - -export default Filter; diff --git a/src/core/renderers/webgl/filters/extractUniformsFromSrc.js b/src/core/renderers/webgl/filters/extractUniformsFromSrc.js index d78a0b1..4893214 100644 --- a/src/core/renderers/webgl/filters/extractUniformsFromSrc.js +++ b/src/core/renderers/webgl/filters/extractUniformsFromSrc.js @@ -12,7 +12,7 @@ function extractUniformsFromString(string) { - const maskRegex = new RegExp('^(projectionMatrix|uSampler|filterArea)$'); + const maskRegex = new RegExp('^(projectionMatrix|uSampler|filterArea|filterClamp)$'); const uniforms = {}; let nameSplit; diff --git a/src/core/renderers/webgl/managers/FilterManager.js b/src/core/renderers/webgl/managers/FilterManager.js index 43fc869..6c7422d 100644 --- a/src/core/renderers/webgl/managers/FilterManager.js +++ b/src/core/renderers/webgl/managers/FilterManager.js @@ -2,6 +2,7 @@ import RenderTarget from '../utils/RenderTarget'; import Quad from '../utils/Quad'; import { Rectangle } from '../../../math'; +import Shader from '../../../Shader'; import * as filterTransforms from '../filters/filterTransforms'; import bitTwiddle from 'bit-twiddle'; @@ -22,8 +23,6 @@ this.filters = []; this.target = null; this.resolution = 1; - - this.firstRun = true; } } @@ -155,7 +154,7 @@ if (filters.length === 1) { - filters[0].apply(this, currentState.renderTarget, lastState.renderTarget, false); + filters[0].apply(this, currentState.renderTarget, lastState.renderTarget, false, currentState); this.freePotRenderTarget(currentState.renderTarget); } else @@ -177,7 +176,7 @@ for (i = 0; i < filters.length - 1; ++i) { - filters[i].apply(this, flip, flop, true); + filters[i].apply(this, flip, flop, true, currentState); const t = flip; @@ -185,7 +184,7 @@ flop = t; } - filters[i].apply(this, flip, lastState.renderTarget, false); + filters[i].apply(this, flip, lastState.renderTarget, false, currentState); this.freePotRenderTarget(flip); this.freePotRenderTarget(flop); @@ -212,16 +211,28 @@ const renderer = this.renderer; const gl = renderer.gl; - renderer.bindShader(filter, true); + let shader = filter.glShaders[renderer.CONTEXT_UID]; - // TODO theres a better way! - // quad can be a mesh and we then should be able to pull this off without accessing the shader directly - const shader = renderer.shaderManager.getGLShader(); - - // TODO change quad to mesh.. - if (!this.firstRun) + // cacheing.. + if (!shader) { - this.firstRun = true; + if (filter.glShaderKey) + { + shader = this.shaderCache[filter.glShaderKey]; + + if (!shader) + { + shader = new Shader(this.gl, filter.vertexSrc, filter.fragmentSrc); + + filter.glShaders[renderer.CONTEXT_UID] = this.shaderCache[filter.glShaderKey] = shader; + } + } + else + { + shader = filter.glShaders[renderer.CONTEXT_UID] = new Shader(this.gl, filter.vertexSrc, filter.fragmentSrc); + } + + // TODO - this only needs to be done once? renderer.bindVao(null); this.quad.initVao(shader); @@ -244,58 +255,146 @@ renderer.maskManager.pushScissorMask(null, renderer.maskManager.scissorData); } - // renderer._bindGLShader(shader); + renderer.bindShader(shader); - let currentState; - const uniforms = filter.uniforms; + // free unit 0 for us, doesn't matter what was there + // don't try to restore it, because syncUniforms can upload it to another slot + // and it'll be a problem + const tex = this.renderer.emptyTextures[0]; - if (uniforms.filterArea) - { - currentState = this.filterData.stack[this.filterData.index]; - - const filterArea = uniforms.filterArea; - - filterArea[0] = currentState.renderTarget.size.width; - filterArea[1] = currentState.renderTarget.size.height; - filterArea[2] = currentState.sourceFrame.x; - filterArea[3] = currentState.sourceFrame.y; - - filter.uniforms.filterArea = filterArea; - } - - // use this to clamp displaced texture coords so they belong to filterArea - // see displacementFilter fragment shader for an example - if (uniforms.filterClamp) - { - currentState = this.filterData.stack[this.filterData.index]; - - const filterClamp = uniforms.filterClamp; - - filterClamp[0] = 0; - filterClamp[1] = 0; - filterClamp[2] = (currentState.sourceFrame.width - 1) / currentState.renderTarget.size.width; - filterClamp[3] = (currentState.sourceFrame.height - 1) / currentState.renderTarget.size.height; - - filter.uniforms.filterClamp = filterClamp; - } - - renderer.shaderManager.setUniforms(filter.uniforms); + this.renderer.boundTextures[0] = tex; + // this syncs the pixi filters uniforms with glsl uniforms + this.syncUniforms(shader, filter); renderer.state.setBlendMode(filter.blendMode); - // temporary bypass cache.. - const tex = this.renderer.boundTextures[0]; - gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, input.texture.texture); this.quad.vao.draw(this.renderer.gl.TRIANGLES, 6, 0); - // restore cache. gl.bindTexture(gl.TEXTURE_2D, tex._glTextures[this.renderer.CONTEXT_UID].texture); } /** + * Uploads the uniforms of the filter. + * + * @param {GLShader} shader - The underlying gl shader. + * @param {PIXI.Filter} filter - The filter we are synchronizing. + */ + syncUniforms(shader, filter) + { + const uniformData = filter.uniformData; + const uniforms = filter.uniforms; + + // 0 is reserved for the pixi texture so we start at 1! + let textureCount = 1; + let currentState; + + // filterArea and filterClamp that are handled by FilterManager directly + // they must not appear in uniformData + + if (shader.uniforms.filterArea) + { + currentState = this.filterData.stack[this.filterData.index]; + + const filterArea = shader.uniforms.filterArea; + + filterArea[0] = currentState.renderTarget.size.width; + filterArea[1] = currentState.renderTarget.size.height; + filterArea[2] = currentState.sourceFrame.x; + filterArea[3] = currentState.sourceFrame.y; + + shader.uniforms.filterArea = filterArea; + } + + // use this to clamp displaced texture coords so they belong to filterArea + // see displacementFilter fragment shader for an example + if (shader.uniforms.filterClamp) + { + currentState = currentState || this.filterData.stack[this.filterData.index]; + + const filterClamp = shader.uniforms.filterClamp; + + filterClamp[0] = 0; + filterClamp[1] = 0; + filterClamp[2] = (currentState.sourceFrame.width - 1) / currentState.renderTarget.size.width; + filterClamp[3] = (currentState.sourceFrame.height - 1) / currentState.renderTarget.size.height; + + shader.uniforms.filterClamp = filterClamp; + } + + // TODO Cacheing layer.. + for (const i in uniformData) + { + if (uniformData[i].type === 'sampler2D' && uniforms[i] !== 0) + { + if (uniforms[i].baseTexture) + { + shader.uniforms[i] = this.renderer.bindTexture(uniforms[i].baseTexture, textureCount); + } + else + { + shader.uniforms[i] = textureCount; + + // TODO + // this is helpful as renderTargets can also be set. + // Although thinking about it, we could probably + // make the filter texture cache return a RenderTexture + // rather than a renderTarget + const gl = this.renderer.gl; + + this.renderer.boundTextures[textureCount] = this.renderer.emptyTextures[textureCount]; + gl.activeTexture(gl.TEXTURE0 + textureCount); + + uniforms[i].texture.bind(); + } + + textureCount++; + } + else if (uniformData[i].type === 'mat3') + { + // check if its pixi matrix.. + if (uniforms[i].a !== undefined) + { + shader.uniforms[i] = uniforms[i].toArray(true); + } + else + { + shader.uniforms[i] = uniforms[i]; + } + } + else if (uniformData[i].type === 'vec2') + { + // check if its a point.. + if (uniforms[i].x !== undefined) + { + const val = shader.uniforms[i] || new Float32Array(2); + + val[0] = uniforms[i].x; + val[1] = uniforms[i].y; + shader.uniforms[i] = val; + } + else + { + shader.uniforms[i] = uniforms[i]; + } + } + else if (uniformData[i].type === 'float') + { + if (shader.uniforms.data[i].value !== uniformData[i]) + { + shader.uniforms[i] = uniforms[i]; + } + } + else + { + shader.uniforms[i] = uniforms[i]; + } + } + } + + /** * Gets a render target from the pool, or creates a new one. * * @param {boolean} clear - Should we clear the render texture when we get it? @@ -442,7 +541,6 @@ renderTarget.resolution = resolution; renderTarget.defaultFrame.width = renderTarget.size.width = minWidth / resolution; renderTarget.defaultFrame.height = renderTarget.size.height = minHeight / resolution; - renderTarget.attachStencilBuffer(); return renderTarget; } diff --git a/src/core/settings.js b/src/core/settings.js index bb08cb0..f4dd087 100644 --- a/src/core/settings.js +++ b/src/core/settings.js @@ -77,11 +77,11 @@ * * @static * @memberof PIXI.settings - * @type {RegExp|string} + * @type {RegExp} * @example `@2x` - * @default /@(.+)x/ + * @default /@([0-9\.]+)x/ */ - RETINA_PREFIX: /@(.+)x/, + RETINA_PREFIX: /@([0-9\.]+)x/, /** * The default render options if none are supplied to {@link PIXI.WebGLRenderer} @@ -175,14 +175,24 @@ SCALE_MODE: 0, /** - * Default specify float precision in shaders. + * Default specify float precision in vertex shader. + * + * @static + * @memberof PIXI.settings + * @type {PIXI.PRECISION} + * @default PIXI.PRECISION.HIGH + */ + PRECISION_VERTEX: 'highp', + + /** + * Default specify float precision in fragment shader. * * @static * @memberof PIXI.settings * @type {PIXI.PRECISION} * @default PIXI.PRECISION.MEDIUM */ - PRECISION: 'mediump', + PRECISION_FRAGMENT: 'mediump', /** * Can we upload the same buffer in a single frame? diff --git a/package.json b/package.json index 6a366ba..71ade15 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pixi.js", - "version": "4.3.4", + "version": "4.3.5", "description": "Pixi.js is a fast lightweight 2D library that works across all devices.", "author": "Mat Groves", "contributors": [ @@ -61,7 +61,7 @@ "ismobilejs": "^0.4.0", "object-assign": "^4.0.1", "pixi-gl-core": "^1.0.3", - "resource-loader": "^2.0.4" + "resource-loader": "^2.0.6" }, "devDependencies": { "babel-cli": "^6.18.0", @@ -70,9 +70,9 @@ "babel-preset-es2015": "^6.14.0", "babelify": "^7.3.0", "del": "^2.2.0", - "electron-prebuilt": "^1.3.2", + "electron": "^1.4.15", "eslint": "^3.5.0", - "floss": "^1.2.0", + "floss": "^2.0.1", "gh-pages": "^0.11.0", "jaguarjs-jsdoc": "^1.0.1", "js-md5": "^0.4.1", diff --git a/src/accessibility/AccessibilityManager.js b/src/accessibility/AccessibilityManager.js index df2716a..f16bcb1 100644 --- a/src/accessibility/AccessibilityManager.js +++ b/src/accessibility/AccessibilityManager.js @@ -3,7 +3,7 @@ import accessibleTarget from './accessibleTarget'; // add some extra variables to the container.. -Object.assign( +core.utils.mixins.delayMixin( core.DisplayObject.prototype, accessibleTarget ); diff --git a/src/core/Application.js b/src/core/Application.js index ea0f058..5340578 100644 --- a/src/core/Application.js +++ b/src/core/Application.js @@ -1,6 +1,6 @@ import { autoDetectRenderer } from './autoDetectRenderer'; import Container from './display/Container'; -import Ticker from './ticker/Ticker'; +import { shared, Ticker } from './ticker'; /** * Convenience class to create a new PIXI application. @@ -35,9 +35,9 @@ * @param {boolean} [noWebGL=false] - prevents selection of WebGL renderer, even if such is present * @param {boolean} [options.legacy=false] - If true Pixi will aim to ensure compatibility * with older / less advanced devices. If you experience unexplained flickering try setting this to true. - + * @param {boolean} [useSharedTicker=false] - `true` to use PIXI.ticker.shared, `false` to create new ticker. */ - constructor(width, height, options, noWebGL) + constructor(width, height, options, noWebGL, useSharedTicker = false) { /** * WebGL renderer if available, otherwise CanvasRenderer @@ -46,23 +46,46 @@ this.renderer = autoDetectRenderer(width, height, options, noWebGL); /** - * The root display container that's renderered. + * The root display container that's rendered. * @member {PIXI.Container} */ this.stage = new Container(); /** + * Internal reference to the ticker + * @member {PIXI.ticker.Ticker} + * @private + */ + this._ticker = null; + + /** * Ticker for doing render updates. * @member {PIXI.ticker.Ticker} + * @default PIXI.ticker.shared */ - this.ticker = new Ticker(); - - this.ticker.add(this.render, this); + this.ticker = useSharedTicker ? shared : new Ticker(); // Start the rendering this.start(); } + set ticker(ticker) // eslint-disable-line require-jsdoc + { + if (this._ticker) + { + this._ticker.remove(this.render, this); + } + this._ticker = ticker; + if (ticker) + { + ticker.add(this.render, this); + } + } + get ticker() // eslint-disable-line require-jsdoc + { + return this._ticker; + } + /** * Render the current stage. */ @@ -76,7 +99,7 @@ */ stop() { - this.ticker.stop(); + this._ticker.stop(); } /** @@ -84,7 +107,7 @@ */ start() { - this.ticker.start(); + this._ticker.start(); } /** @@ -98,13 +121,22 @@ } /** + * Reference to the renderer's screen rectangle. Its safe to use as filterArea or hitArea for whole screen + * @member {PIXI.Rectangle} + * @readonly + */ + get screen() + { + return this.renderer.screen; + } + + /** * Destroy and don't use after this. * @param {Boolean} [removeView=false] Automatically remove canvas from DOM. */ destroy(removeView) { this.stop(); - this.ticker.remove(this.render, this); this.ticker = null; this.stage.destroy(); diff --git a/src/core/Shader.js b/src/core/Shader.js new file mode 100644 index 0000000..78b17e6 --- /dev/null +++ b/src/core/Shader.js @@ -0,0 +1,46 @@ +import { GLShader } from 'pixi-gl-core'; +import settings from './settings'; + +function checkPrecision(src, def) +{ + if (src instanceof Array) + { + if (src[0].substring(0, 9) !== 'precision') + { + const copy = src.slice(0); + + copy.unshift(`precision ${def} float;`); + + return copy; + } + } + else if (src.substring(0, 9) !== 'precision') + { + return `precision ${def} float;\n${src}`; + } + + return src; +} + +/** + * Wrapper class, webGL Shader for Pixi. + * Adds precision string if vertexSrc or fragmentSrc have no mention of it. + * + * @class + * @extends GLShader + * @memberof PIXI + */ +export default class Shader extends GLShader +{ + /** + * + * @param {WebGLRenderingContext} gl - The current WebGL rendering context + * @param {string|string[]} vertexSrc - The vertex shader source as an array of strings. + * @param {string|string[]} fragmentSrc - The fragment shader source as an array of strings. + */ + constructor(gl, vertexSrc, fragmentSrc) + { + super(gl, checkPrecision(vertexSrc, settings.PRECISION_VERTEX), + checkPrecision(fragmentSrc, settings.PRECISION_FRAGMENT)); + } +} diff --git a/src/core/const.js b/src/core/const.js index ef6f662..3de0efe 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -188,7 +188,7 @@ * The gc modes that are supported by pixi. * * The {@link PIXI.settings.GC_MODE} Garbage Collection mode for pixi textures is AUTO - * If set to GC_MODE, the renderer will occasianally check textures usage. If they are not + * If set to GC_MODE, the renderer will occasionally check textures usage. If they are not * used for a specified period of time they will be removed from the GPU. They will of course * be uploaded again when they are required. This is a silent behind the scenes process that * should ensure that the GPU does not get filled up. diff --git a/src/core/display/Container.js b/src/core/display/Container.js index ed8cd5f..17fde8d 100644 --- a/src/core/display/Container.js +++ b/src/core/display/Container.js @@ -73,11 +73,12 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.push(child); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -108,11 +109,12 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.splice(index, 0, child); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -225,10 +227,11 @@ if (index === -1) return null; child.parent = null; + // ensure child transform will be recalculated + child.transform._parentID = -1; removeItems(this.children, index, 1); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -249,11 +252,12 @@ { const child = this.getChildAt(index); + // ensure child transform will be recalculated.. child.parent = null; + child.transform._parentID = -1; removeItems(this.children, index, 1); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -284,13 +288,12 @@ for (let i = 0; i < removed.length; ++i) { removed[i].parent = null; + if (removed[i].transform) + { + removed[i].transform._parentID = -1; + } } - // ensure a transform will be recalculated.. - if (this.transform) - { - this.transform._parentID = -1; - } this._boundsID++; this.onChildrenChange(beginIndex); diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index f37f2f5..be4c4fb 100644 --- a/src/core/graphics/Graphics.js +++ b/src/core/graphics/Graphics.js @@ -28,8 +28,9 @@ { /** * + * @param {boolean} [nativeLines=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP */ - constructor() + constructor(nativeLines = false) { super(); @@ -50,6 +51,13 @@ this.lineWidth = 0; /** + * If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * + * @member {boolean} + */ + this.nativeLines = nativeLines; + + /** * The color of any lines drawn. * * @member {string} @@ -757,28 +765,14 @@ if (!this._spriteRect) { - if (!Graphics._SPRITE_TEXTURE) - { - Graphics._SPRITE_TEXTURE = RenderTexture.create(10, 10); - - const canvas = document.createElement('canvas'); - - canvas.width = 10; - canvas.height = 10; - - const context = canvas.getContext('2d'); - - context.fillStyle = 'white'; - context.fillRect(0, 0, 10, 10); - - Graphics._SPRITE_TEXTURE = Texture.fromCanvas(canvas); - } - - this._spriteRect = new Sprite(Graphics._SPRITE_TEXTURE); + this._spriteRect = new Sprite(new Texture(Texture.WHITE)); } + + const sprite = this._spriteRect; + if (this.tint === 0xffffff) { - this._spriteRect.tint = this.graphicsData[0].fillColor; + sprite.tint = this.graphicsData[0].fillColor; } else { @@ -792,20 +786,21 @@ t1[1] *= t2[1]; t1[2] *= t2[2]; - this._spriteRect.tint = rgb2hex(t1); + sprite.tint = rgb2hex(t1); } - this._spriteRect.alpha = this.graphicsData[0].fillAlpha; - this._spriteRect.worldAlpha = this.worldAlpha * this._spriteRect.alpha; + sprite.alpha = this.graphicsData[0].fillAlpha; + sprite.worldAlpha = this.worldAlpha * sprite.alpha; + sprite.blendMode = this.blendMode; - Graphics._SPRITE_TEXTURE._frame.width = rect.width; - Graphics._SPRITE_TEXTURE._frame.height = rect.height; + sprite.texture._frame.width = rect.width; + sprite.texture._frame.height = rect.height; - this._spriteRect.transform.worldTransform = this.transform.worldTransform; + sprite.transform.worldTransform = this.transform.worldTransform; - this._spriteRect.anchor.set(-rect.x / rect.width, -rect.y / rect.height); - this._spriteRect._onAnchorUpdate(); + sprite.anchor.set(-rect.x / rect.width, -rect.y / rect.height); + sprite._onAnchorUpdate(); - this._spriteRect._renderWebGL(renderer); + sprite._renderWebGL(renderer); } /** @@ -1031,6 +1026,7 @@ this.fillColor, this.fillAlpha, this.filling, + this.nativeLines, shape ); diff --git a/src/core/graphics/GraphicsData.js b/src/core/graphics/GraphicsData.js index f5a87ae..b6f03c0 100644 --- a/src/core/graphics/GraphicsData.js +++ b/src/core/graphics/GraphicsData.js @@ -14,14 +14,19 @@ * @param {number} fillColor - the color of the fill * @param {number} fillAlpha - the alpha of the fill * @param {boolean} fill - whether or not the shape is filled with a colour + * @param {boolean} nativeLines - the method for drawing lines * @param {PIXI.Circle|PIXI.Rectangle|PIXI.Ellipse|PIXI.Polygon} shape - The shape object to draw. */ - constructor(lineWidth, lineColor, lineAlpha, fillColor, fillAlpha, fill, shape) + constructor(lineWidth, lineColor, lineAlpha, fillColor, fillAlpha, fill, nativeLines, shape) { /** * @member {number} the width of the line to draw */ this.lineWidth = lineWidth; + /** + * @member {boolean} if true the liens will be draw using LINES instead of TRIANGLE_STRIP + */ + this.nativeLines = nativeLines; /** * @member {number} the color of the line to draw @@ -85,6 +90,7 @@ this.fillColor, this.fillAlpha, this.fill, + this.nativeLines, this.shape ); } diff --git a/src/core/graphics/webgl/GraphicsRenderer.js b/src/core/graphics/webgl/GraphicsRenderer.js index 14083fe..67f5e92 100644 --- a/src/core/graphics/webgl/GraphicsRenderer.js +++ b/src/core/graphics/webgl/GraphicsRenderer.js @@ -102,7 +102,15 @@ shaderTemp.uniforms.alpha = graphics.worldAlpha; renderer.bindVao(webGLData.vao); - webGLData.vao.draw(gl.TRIANGLE_STRIP, webGLData.indices.length); + + if (graphics.nativeLines) + { + gl.drawArrays(gl.LINES, 0, webGLData.points.length / 6); + } + else + { + webGLData.vao.draw(gl.TRIANGLE_STRIP, webGLData.indices.length); + } } } diff --git a/src/core/graphics/webgl/utils/buildLine.js b/src/core/graphics/webgl/utils/buildLine.js index ecf43f7..4d6862e 100644 --- a/src/core/graphics/webgl/utils/buildLine.js +++ b/src/core/graphics/webgl/utils/buildLine.js @@ -11,7 +11,29 @@ * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties * @param {object} webGLData - an object containing all the webGL-specific information to create this shape */ -export default function buildLine(graphicsData, webGLData) +export default function (graphicsData, webGLData) +{ + if (graphicsData.nativeLines) + { + buildNativeLine(graphicsData, webGLData); + } + else + { + buildLine(graphicsData, webGLData); + } +} + +/** + * Builds a line to draw using the poligon method. + * + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @private + * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties + * @param {object} webGLData - an object containing all the webGL-specific information to create this shape + */ +function buildLine(graphicsData, webGLData) { // TODO OPTIMISE! let points = graphicsData.points; @@ -224,3 +246,46 @@ indices.push(indexStart - 1); } + +/** + * Builds a line to draw using the gl.drawArrays(gl.LINES) method + * + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @private + * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties + * @param {object} webGLData - an object containing all the webGL-specific information to create this shape + */ +function buildNativeLine(graphicsData, webGLData) +{ + let i = 0; + const points = graphicsData.points; + + if (points.length === 0) return; + + const verts = webGLData.points; + const length = points.length / 2; + + // sort color + const color = hex2rgb(graphicsData.lineColor); + const alpha = graphicsData.lineAlpha; + const r = color[0] * alpha; + const g = color[1] * alpha; + const b = color[2] * alpha; + + for (i = 1; i < length; i++) + { + const p1x = points[(i - 1) * 2]; + const p1y = points[((i - 1) * 2) + 1]; + + const p2x = points[i * 2]; + const p2y = points[(i * 2) + 1]; + + verts.push(p1x, p1y); + verts.push(r, g, b, alpha); + + verts.push(p2x, p2y); + verts.push(r, g, b, alpha); + } +} diff --git a/src/core/graphics/webgl/utils/buildRoundedRectangle.js b/src/core/graphics/webgl/utils/buildRoundedRectangle.js index 9abc807..a6c8461 100644 --- a/src/core/graphics/webgl/utils/buildRoundedRectangle.js +++ b/src/core/graphics/webgl/utils/buildRoundedRectangle.js @@ -77,6 +77,26 @@ } /** + * Calculate a single point for a quadratic bezier curve. + * Utility function used by quadraticBezierCurve. + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @private + * @param {number} n1 - first number + * @param {number} n2 - second number + * @param {number} perc - percentage + * @return {number} the result + * + */ +function getPt(n1, n2, perc) +{ + const diff = n2 - n1; + + return n1 + (diff * perc); +} + +/** * Calculate the points for a quadratic bezier curve. (helper function..) * Based on: https://stackoverflow.com/questions/785097/how-do-i-implement-a-bezier-curve-in-c * @@ -105,13 +125,6 @@ let x = 0; let y = 0; - function getPt(n1, n2, perc) - { - const diff = n2 - n1; - - return n1 + (diff * perc); - } - for (let i = 0, j = 0; i <= n; ++i) { j = i / n; diff --git a/src/core/index.js b/src/core/index.js index f7a817c..4f414e8 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -30,6 +30,7 @@ export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; export { default as CanvasGraphicsRenderer } from './graphics/canvas/CanvasGraphicsRenderer'; +export { default as Spritesheet } from './textures/Spritesheet'; export { default as Texture } from './textures/Texture'; export { default as BaseTexture } from './textures/BaseTexture'; export { default as RenderTexture } from './textures/RenderTexture'; diff --git a/src/core/math/Matrix.js b/src/core/math/Matrix.js index 0b77477..3679860 100644 --- a/src/core/math/Matrix.js +++ b/src/core/math/Matrix.js @@ -13,45 +13,50 @@ export default class Matrix { /** - * + * @param {number} [a=1] - x scale + * @param {number} [b=0] - y skew + * @param {number} [c=0] - x skew + * @param {number} [d=1] - y scale + * @param {number} [tx=0] - x translation + * @param {number} [ty=0] - y translation */ - constructor() + constructor(a = 1, b = 0, c = 0, d = 1, tx = 0, ty = 0) { /** * @member {number} * @default 1 */ - this.a = 1; + this.a = a; /** * @member {number} * @default 0 */ - this.b = 0; + this.b = b; /** * @member {number} * @default 0 */ - this.c = 0; + this.c = c; /** * @member {number} * @default 1 */ - this.d = 1; + this.d = d; /** * @member {number} * @default 0 */ - this.tx = 0; + this.tx = tx; /** * @member {number} * @default 0 */ - this.ty = 0; + this.ty = ty; this.array = null; } diff --git a/src/core/renderers/SystemRenderer.js b/src/core/renderers/SystemRenderer.js index fa116a6..a24c775 100644 --- a/src/core/renderers/SystemRenderer.js +++ b/src/core/renderers/SystemRenderer.js @@ -1,5 +1,5 @@ import { sayHello, hex2string, hex2rgb } from '../utils'; -import { Matrix } from '../math'; +import { Matrix, Rectangle } from '../math'; import { RENDERER_TYPE } from '../const'; import settings from '../settings'; import Container from '../display/Container'; @@ -21,8 +21,8 @@ { /** * @param {string} system - The name of the system this renderer is for. - * @param {number} [width=800] - the width of the canvas view - * @param {number} [height=600] - the height of the canvas view + * @param {number} [screenWidth=800] - the width of the screen + * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false @@ -37,7 +37,7 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(system, width, height, options) + constructor(system, screenWidth, screenHeight, options) { super(); @@ -69,20 +69,13 @@ this.type = RENDERER_TYPE.UNKNOWN; /** - * The width of the canvas view + * Measurements of the screen. (0, 0, screenWidth, screenHeight) * - * @member {number} - * @default 800 - */ - this.width = width || 800; - - /** - * The height of the canvas view + * Its safe to use as filterArea or hitArea for whole stage * - * @member {number} - * @default 600 + * @member {PIXI.Rectangle} */ - this.height = height || 600; + this.screen = new Rectangle(0, 0, screenWidth || 800, screenHeight || 600); /** * The canvas element that everything is drawn to @@ -107,7 +100,7 @@ this.transparent = options.transparent; /** - * Whether the render view should be resized automatically + * Whether css dimensions of canvas view should be resized to screen dimensions automatically * * @member {boolean} */ @@ -192,23 +185,48 @@ } /** - * Resizes the canvas view to the specified width and height + * Same as view.width, actual number of pixels in the canvas by horizontal * - * @param {number} width - the new width of the canvas view - * @param {number} height - the new height of the canvas view + * @member {number} + * @readonly + * @default 800 */ - resize(width, height) + get width() { - this.width = width * this.resolution; - this.height = height * this.resolution; + return this.view.width; + } - this.view.width = this.width; - this.view.height = this.height; + /** + * Same as view.height, actual number of pixels in the canvas by vertical + * + * @member {number} + * @readonly + * @default 600 + */ + get height() + { + return this.view.height; + } + + /** + * Resizes the screen and canvas to the specified width and height + * Canvas dimensions are multiplied by resolution + * + * @param {number} screenWidth - the new width of the screen + * @param {number} screenHeight - the new height of the screen + */ + resize(screenWidth, screenHeight) + { + this.screen.width = screenWidth; + this.screen.height = screenHeight; + + this.view.width = screenWidth * this.resolution; + this.view.height = screenHeight * this.resolution; if (this.autoResize) { - this.view.style.width = `${this.width / this.resolution}px`; - this.view.style.height = `${this.height / this.resolution}px`; + this.view.style.width = `${screenWidth}px`; + this.view.style.height = `${screenHeight}px`; } } @@ -249,11 +267,10 @@ this.type = RENDERER_TYPE.UNKNOWN; - this.width = 0; - this.height = 0; - this.view = null; + this.screen = null; + this.resolution = 0; this.transparent = false; diff --git a/src/core/renderers/canvas/CanvasRenderer.js b/src/core/renderers/canvas/CanvasRenderer.js index 1bd2d5e..e84e49c 100644 --- a/src/core/renderers/canvas/CanvasRenderer.js +++ b/src/core/renderers/canvas/CanvasRenderer.js @@ -18,8 +18,8 @@ export default class CanvasRenderer extends SystemRenderer { /** - * @param {number} [width=800] - the width of the canvas view - * @param {number} [height=600] - the height of the canvas view + * @param {number} [screenWidth=800] - the width of the screen + * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false @@ -34,9 +34,9 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(width, height, options = {}) + constructor(screenWidth, screenHeight, options = {}) { - super('Canvas', width, height, options); + super('Canvas', screenWidth, screenHeight, options); this.type = RENDERER_TYPE.CANVAS; @@ -96,7 +96,7 @@ this.context = null; this.renderingToScreen = false; - this.resize(width, height); + this.resize(screenWidth, screenHeight); } /** @@ -283,12 +283,12 @@ * * @extends PIXI.SystemRenderer#resize * - * @param {number} width - The new width of the canvas view - * @param {number} height - The new height of the canvas view + * @param {number} screenWidth - the new width of the screen + * @param {number} screenHeight - the new height of the screen */ - resize(width, height) + resize(screenWidth, screenHeight) { - super.resize(width, height); + super.resize(screenWidth, screenHeight); // reset the scale mode.. oddly this seems to be reset when the canvas is resized. // surely a browser bug?? Let pixi fix that for you.. diff --git a/src/core/renderers/webgl/TextureManager.js b/src/core/renderers/webgl/TextureManager.js index b685c1a..8227289 100644 --- a/src/core/renderers/webgl/TextureManager.js +++ b/src/core/renderers/webgl/TextureManager.js @@ -61,7 +61,7 @@ * Updates and/or Creates a WebGL texture for the renderer's context. * * @param {PIXI.BaseTexture|PIXI.Texture} texture - the texture to update - * @param {Number} location - the location the texture will be bound to. + * @param {number} location - the location the texture will be bound to. * @return {GLTexture} The gl texture. */ updateTexture(texture, location) diff --git a/src/core/renderers/webgl/WebGLRenderer.js b/src/core/renderers/webgl/WebGLRenderer.js index e4cd8a0..830b382 100644 --- a/src/core/renderers/webgl/WebGLRenderer.js +++ b/src/core/renderers/webgl/WebGLRenderer.js @@ -31,8 +31,8 @@ { /** * - * @param {number} [width=0] - the width of the canvas view - * @param {number} [height=0] - the height of the canvas view + * @param {number} [screenWidth=800] - the width of the screen + * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false @@ -53,9 +53,9 @@ * @param {boolean} [options.legacy=false] - If true Pixi will aim to ensure compatibility * with older / less advanced devices. If you experiance unexplained flickering try setting this to true. */ - constructor(width, height, options = {}) + constructor(screenWidth, screenHeight, options = {}) { - super('WebGL', width, height, options); + super('WebGL', screenWidth, screenHeight, options); this.legacy = !!options.legacy; @@ -245,7 +245,7 @@ glCore._testingContext = gl; // setup the width/height properties and gl viewport - this.resize(this.width, this.height); + this.resize(this.screen.width, this.screen.height); } /** @@ -343,16 +343,16 @@ /** * Resizes the webGL view to the specified width and height. * - * @param {number} width - the new width of the webGL view - * @param {number} height - the new height of the webGL view + * @param {number} screenWidth - the new width of the screen + * @param {number} screenHeight - the new height of the screen */ - resize(width, height) + resize(screenWidth, screenHeight) { // if(width * this.resolution === this.width && height * this.resolution === this.height)return; - SystemRenderer.prototype.resize.call(this, width, height); + SystemRenderer.prototype.resize.call(this, screenWidth, screenHeight); - this.rootRenderTarget.resize(width, height); + this.rootRenderTarget.resize(screenWidth, screenHeight); if (this._activeRenderTarget === this.rootRenderTarget) { @@ -396,6 +396,26 @@ } /** + * Erases the render texture and fills the drawing area with a colour + * + * @param {PIXI.RenderTexture} renderTexture - The render texture to clear + * @param {number} [clearColor] - The colour + * @return {PIXI.WebGLRenderer} Returns itself. + */ + clearRenderTexture(renderTexture, clearColor) + { + const baseTexture = renderTexture.baseTexture; + const renderTarget = baseTexture._glRenderTargets[this.CONTEXT_UID]; + + if (renderTarget) + { + renderTarget.clear(clearColor); + } + + return this; + } + + /** * Binds a render texture for rendering * * @param {PIXI.RenderTexture} renderTexture - The render texture to render @@ -474,9 +494,10 @@ * Changes the current GLShader to the one given in parameter * * @param {PIXI.glCore.Shader} shader - the new glShader + * @param {boolean} [autoProject=true] - Whether automatically set the projection matrix * @return {PIXI.WebGLRenderer} Returns itself. */ - _bindGLShader(shader) + _bindGLShader(shader, autoProject) { // TODO cache if (this._activeShader !== shader) @@ -484,8 +505,14 @@ this._activeShader = shader; shader.bind(); - // automatically set the projection matrix - shader.uniforms.projectionMatrix = this._activeRenderTarget.projectionMatrix.toArray(true); + // `autoProject` normally would be a default parameter set to true + // but because of how Babel transpiles default parameters + // it hinders the performance of this method. + if (autoProject !== false) + { + // automatically set the projection matrix + shader.uniforms.projectionMatrix = this._activeRenderTarget.projectionMatrix.toArray(true); + } } return this; diff --git a/src/core/renderers/webgl/WebGLState.js b/src/core/renderers/webgl/WebGLState.js index ba6ecc5..6c846d5 100755 --- a/src/core/renderers/webgl/WebGLState.js +++ b/src/core/renderers/webgl/WebGLState.js @@ -82,18 +82,20 @@ push() { // next state.. - let state = this.stack[++this.stackIndex]; + let state = this.stack[this.stackIndex]; if (!state) { state = this.stack[this.stackIndex] = new Uint8Array(16); } + ++this.stackIndex; + // copy state.. // set active state so we can force overrides of gl state for (let i = 0; i < this.activeState.length; i++) { - this.activeState[i] = state[i]; + state[i] = this.activeState[i]; } } diff --git a/src/core/renderers/webgl/filters/Filter.js b/src/core/renderers/webgl/filters/Filter.js index 2c3eaad..4eb6128 100644 --- a/src/core/renderers/webgl/filters/Filter.js +++ b/src/core/renderers/webgl/filters/Filter.js @@ -9,7 +9,7 @@ * @memberof PIXI * @extends PIXI.Shader */ -class Filter extends Shader +export default class Filter extends Shader { /** * @param {string} [vertexSrc] - The source of the vertex shader. @@ -56,15 +56,16 @@ * @param {PIXI.RenderTarget} input - The input render target. * @param {PIXI.RenderTarget} output - The target to output to. * @param {boolean} clear - Should the output be cleared before rendering to it + * @param {object} [currentState] - It's current state of filter. + * There are some useful properties in the currentState : + * target, filters, sourceFrame, destinationFrame, renderTarget, resolution */ - apply(filterManager, input, output, clear) + apply(filterManager, input, output, clear, currentState) // eslint-disable-line no-unused-vars { // do as you please! - filterManager.applyFilter(this, input, output, clear); + filterManager.applyFilter(this, input, output, clear, currentState); // or just do a regular render.. } } - -export default Filter; diff --git a/src/core/renderers/webgl/filters/extractUniformsFromSrc.js b/src/core/renderers/webgl/filters/extractUniformsFromSrc.js index d78a0b1..4893214 100644 --- a/src/core/renderers/webgl/filters/extractUniformsFromSrc.js +++ b/src/core/renderers/webgl/filters/extractUniformsFromSrc.js @@ -12,7 +12,7 @@ function extractUniformsFromString(string) { - const maskRegex = new RegExp('^(projectionMatrix|uSampler|filterArea)$'); + const maskRegex = new RegExp('^(projectionMatrix|uSampler|filterArea|filterClamp)$'); const uniforms = {}; let nameSplit; diff --git a/src/core/renderers/webgl/managers/FilterManager.js b/src/core/renderers/webgl/managers/FilterManager.js index 43fc869..6c7422d 100644 --- a/src/core/renderers/webgl/managers/FilterManager.js +++ b/src/core/renderers/webgl/managers/FilterManager.js @@ -2,6 +2,7 @@ import RenderTarget from '../utils/RenderTarget'; import Quad from '../utils/Quad'; import { Rectangle } from '../../../math'; +import Shader from '../../../Shader'; import * as filterTransforms from '../filters/filterTransforms'; import bitTwiddle from 'bit-twiddle'; @@ -22,8 +23,6 @@ this.filters = []; this.target = null; this.resolution = 1; - - this.firstRun = true; } } @@ -155,7 +154,7 @@ if (filters.length === 1) { - filters[0].apply(this, currentState.renderTarget, lastState.renderTarget, false); + filters[0].apply(this, currentState.renderTarget, lastState.renderTarget, false, currentState); this.freePotRenderTarget(currentState.renderTarget); } else @@ -177,7 +176,7 @@ for (i = 0; i < filters.length - 1; ++i) { - filters[i].apply(this, flip, flop, true); + filters[i].apply(this, flip, flop, true, currentState); const t = flip; @@ -185,7 +184,7 @@ flop = t; } - filters[i].apply(this, flip, lastState.renderTarget, false); + filters[i].apply(this, flip, lastState.renderTarget, false, currentState); this.freePotRenderTarget(flip); this.freePotRenderTarget(flop); @@ -212,16 +211,28 @@ const renderer = this.renderer; const gl = renderer.gl; - renderer.bindShader(filter, true); + let shader = filter.glShaders[renderer.CONTEXT_UID]; - // TODO theres a better way! - // quad can be a mesh and we then should be able to pull this off without accessing the shader directly - const shader = renderer.shaderManager.getGLShader(); - - // TODO change quad to mesh.. - if (!this.firstRun) + // cacheing.. + if (!shader) { - this.firstRun = true; + if (filter.glShaderKey) + { + shader = this.shaderCache[filter.glShaderKey]; + + if (!shader) + { + shader = new Shader(this.gl, filter.vertexSrc, filter.fragmentSrc); + + filter.glShaders[renderer.CONTEXT_UID] = this.shaderCache[filter.glShaderKey] = shader; + } + } + else + { + shader = filter.glShaders[renderer.CONTEXT_UID] = new Shader(this.gl, filter.vertexSrc, filter.fragmentSrc); + } + + // TODO - this only needs to be done once? renderer.bindVao(null); this.quad.initVao(shader); @@ -244,58 +255,146 @@ renderer.maskManager.pushScissorMask(null, renderer.maskManager.scissorData); } - // renderer._bindGLShader(shader); + renderer.bindShader(shader); - let currentState; - const uniforms = filter.uniforms; + // free unit 0 for us, doesn't matter what was there + // don't try to restore it, because syncUniforms can upload it to another slot + // and it'll be a problem + const tex = this.renderer.emptyTextures[0]; - if (uniforms.filterArea) - { - currentState = this.filterData.stack[this.filterData.index]; - - const filterArea = uniforms.filterArea; - - filterArea[0] = currentState.renderTarget.size.width; - filterArea[1] = currentState.renderTarget.size.height; - filterArea[2] = currentState.sourceFrame.x; - filterArea[3] = currentState.sourceFrame.y; - - filter.uniforms.filterArea = filterArea; - } - - // use this to clamp displaced texture coords so they belong to filterArea - // see displacementFilter fragment shader for an example - if (uniforms.filterClamp) - { - currentState = this.filterData.stack[this.filterData.index]; - - const filterClamp = uniforms.filterClamp; - - filterClamp[0] = 0; - filterClamp[1] = 0; - filterClamp[2] = (currentState.sourceFrame.width - 1) / currentState.renderTarget.size.width; - filterClamp[3] = (currentState.sourceFrame.height - 1) / currentState.renderTarget.size.height; - - filter.uniforms.filterClamp = filterClamp; - } - - renderer.shaderManager.setUniforms(filter.uniforms); + this.renderer.boundTextures[0] = tex; + // this syncs the pixi filters uniforms with glsl uniforms + this.syncUniforms(shader, filter); renderer.state.setBlendMode(filter.blendMode); - // temporary bypass cache.. - const tex = this.renderer.boundTextures[0]; - gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, input.texture.texture); this.quad.vao.draw(this.renderer.gl.TRIANGLES, 6, 0); - // restore cache. gl.bindTexture(gl.TEXTURE_2D, tex._glTextures[this.renderer.CONTEXT_UID].texture); } /** + * Uploads the uniforms of the filter. + * + * @param {GLShader} shader - The underlying gl shader. + * @param {PIXI.Filter} filter - The filter we are synchronizing. + */ + syncUniforms(shader, filter) + { + const uniformData = filter.uniformData; + const uniforms = filter.uniforms; + + // 0 is reserved for the pixi texture so we start at 1! + let textureCount = 1; + let currentState; + + // filterArea and filterClamp that are handled by FilterManager directly + // they must not appear in uniformData + + if (shader.uniforms.filterArea) + { + currentState = this.filterData.stack[this.filterData.index]; + + const filterArea = shader.uniforms.filterArea; + + filterArea[0] = currentState.renderTarget.size.width; + filterArea[1] = currentState.renderTarget.size.height; + filterArea[2] = currentState.sourceFrame.x; + filterArea[3] = currentState.sourceFrame.y; + + shader.uniforms.filterArea = filterArea; + } + + // use this to clamp displaced texture coords so they belong to filterArea + // see displacementFilter fragment shader for an example + if (shader.uniforms.filterClamp) + { + currentState = currentState || this.filterData.stack[this.filterData.index]; + + const filterClamp = shader.uniforms.filterClamp; + + filterClamp[0] = 0; + filterClamp[1] = 0; + filterClamp[2] = (currentState.sourceFrame.width - 1) / currentState.renderTarget.size.width; + filterClamp[3] = (currentState.sourceFrame.height - 1) / currentState.renderTarget.size.height; + + shader.uniforms.filterClamp = filterClamp; + } + + // TODO Cacheing layer.. + for (const i in uniformData) + { + if (uniformData[i].type === 'sampler2D' && uniforms[i] !== 0) + { + if (uniforms[i].baseTexture) + { + shader.uniforms[i] = this.renderer.bindTexture(uniforms[i].baseTexture, textureCount); + } + else + { + shader.uniforms[i] = textureCount; + + // TODO + // this is helpful as renderTargets can also be set. + // Although thinking about it, we could probably + // make the filter texture cache return a RenderTexture + // rather than a renderTarget + const gl = this.renderer.gl; + + this.renderer.boundTextures[textureCount] = this.renderer.emptyTextures[textureCount]; + gl.activeTexture(gl.TEXTURE0 + textureCount); + + uniforms[i].texture.bind(); + } + + textureCount++; + } + else if (uniformData[i].type === 'mat3') + { + // check if its pixi matrix.. + if (uniforms[i].a !== undefined) + { + shader.uniforms[i] = uniforms[i].toArray(true); + } + else + { + shader.uniforms[i] = uniforms[i]; + } + } + else if (uniformData[i].type === 'vec2') + { + // check if its a point.. + if (uniforms[i].x !== undefined) + { + const val = shader.uniforms[i] || new Float32Array(2); + + val[0] = uniforms[i].x; + val[1] = uniforms[i].y; + shader.uniforms[i] = val; + } + else + { + shader.uniforms[i] = uniforms[i]; + } + } + else if (uniformData[i].type === 'float') + { + if (shader.uniforms.data[i].value !== uniformData[i]) + { + shader.uniforms[i] = uniforms[i]; + } + } + else + { + shader.uniforms[i] = uniforms[i]; + } + } + } + + /** * Gets a render target from the pool, or creates a new one. * * @param {boolean} clear - Should we clear the render texture when we get it? @@ -442,7 +541,6 @@ renderTarget.resolution = resolution; renderTarget.defaultFrame.width = renderTarget.size.width = minWidth / resolution; renderTarget.defaultFrame.height = renderTarget.size.height = minHeight / resolution; - renderTarget.attachStencilBuffer(); return renderTarget; } diff --git a/src/core/settings.js b/src/core/settings.js index bb08cb0..f4dd087 100644 --- a/src/core/settings.js +++ b/src/core/settings.js @@ -77,11 +77,11 @@ * * @static * @memberof PIXI.settings - * @type {RegExp|string} + * @type {RegExp} * @example `@2x` - * @default /@(.+)x/ + * @default /@([0-9\.]+)x/ */ - RETINA_PREFIX: /@(.+)x/, + RETINA_PREFIX: /@([0-9\.]+)x/, /** * The default render options if none are supplied to {@link PIXI.WebGLRenderer} @@ -175,14 +175,24 @@ SCALE_MODE: 0, /** - * Default specify float precision in shaders. + * Default specify float precision in vertex shader. + * + * @static + * @memberof PIXI.settings + * @type {PIXI.PRECISION} + * @default PIXI.PRECISION.HIGH + */ + PRECISION_VERTEX: 'highp', + + /** + * Default specify float precision in fragment shader. * * @static * @memberof PIXI.settings * @type {PIXI.PRECISION} * @default PIXI.PRECISION.MEDIUM */ - PRECISION: 'mediump', + PRECISION_FRAGMENT: 'mediump', /** * Can we upload the same buffer in a single frame? diff --git a/src/core/sprites/Sprite.js b/src/core/sprites/Sprite.js index 28b0b18..121bf58 100644 --- a/src/core/sprites/Sprite.js +++ b/src/core/sprites/Sprite.js @@ -212,11 +212,11 @@ } else { - w0 = orig.width * (1 - anchor._x); - w1 = orig.width * -anchor._x; + w1 = -anchor._x * orig.width; + w0 = w1 + orig.width; - h0 = orig.height * (1 - anchor._y); - h1 = orig.height * -anchor._y; + h1 = -anchor._y * orig.height; + h0 = h1 + orig.height; } // xy @@ -269,11 +269,11 @@ const tx = wt.tx; const ty = wt.ty; - const w0 = (orig.width) * (1 - anchor._x); - const w1 = (orig.width) * -anchor._x; + const w1 = -anchor._x * orig.width; + const w0 = w1 + orig.width; - const h0 = orig.height * (1 - anchor._y); - const h1 = orig.height * -anchor._y; + const h1 = -anchor._y * orig.height; + const h0 = h1 + orig.height; // xy vertexData[0] = (a * w1) + (c * h1) + tx; @@ -440,7 +440,7 @@ * * @static * @param {number|string|PIXI.BaseTexture|HTMLCanvasElement|HTMLVideoElement} source Source to create texture from - * @return {PIXI.Sprite} The newly created texture + * @return {PIXI.Sprite} The newly created sprite */ static from(source) { @@ -538,8 +538,8 @@ } /** - * The tint applied to the sprite. This is a hex value. A value of - * 0xFFFFFF will remove any tint effect. + * The tint applied to the sprite. This is a hex value. + * A value of 0xFFFFFF will remove any tint effect. * * @member {number} * @default 0xFFFFFF diff --git a/package.json b/package.json index 6a366ba..71ade15 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pixi.js", - "version": "4.3.4", + "version": "4.3.5", "description": "Pixi.js is a fast lightweight 2D library that works across all devices.", "author": "Mat Groves", "contributors": [ @@ -61,7 +61,7 @@ "ismobilejs": "^0.4.0", "object-assign": "^4.0.1", "pixi-gl-core": "^1.0.3", - "resource-loader": "^2.0.4" + "resource-loader": "^2.0.6" }, "devDependencies": { "babel-cli": "^6.18.0", @@ -70,9 +70,9 @@ "babel-preset-es2015": "^6.14.0", "babelify": "^7.3.0", "del": "^2.2.0", - "electron-prebuilt": "^1.3.2", + "electron": "^1.4.15", "eslint": "^3.5.0", - "floss": "^1.2.0", + "floss": "^2.0.1", "gh-pages": "^0.11.0", "jaguarjs-jsdoc": "^1.0.1", "js-md5": "^0.4.1", diff --git a/src/accessibility/AccessibilityManager.js b/src/accessibility/AccessibilityManager.js index df2716a..f16bcb1 100644 --- a/src/accessibility/AccessibilityManager.js +++ b/src/accessibility/AccessibilityManager.js @@ -3,7 +3,7 @@ import accessibleTarget from './accessibleTarget'; // add some extra variables to the container.. -Object.assign( +core.utils.mixins.delayMixin( core.DisplayObject.prototype, accessibleTarget ); diff --git a/src/core/Application.js b/src/core/Application.js index ea0f058..5340578 100644 --- a/src/core/Application.js +++ b/src/core/Application.js @@ -1,6 +1,6 @@ import { autoDetectRenderer } from './autoDetectRenderer'; import Container from './display/Container'; -import Ticker from './ticker/Ticker'; +import { shared, Ticker } from './ticker'; /** * Convenience class to create a new PIXI application. @@ -35,9 +35,9 @@ * @param {boolean} [noWebGL=false] - prevents selection of WebGL renderer, even if such is present * @param {boolean} [options.legacy=false] - If true Pixi will aim to ensure compatibility * with older / less advanced devices. If you experience unexplained flickering try setting this to true. - + * @param {boolean} [useSharedTicker=false] - `true` to use PIXI.ticker.shared, `false` to create new ticker. */ - constructor(width, height, options, noWebGL) + constructor(width, height, options, noWebGL, useSharedTicker = false) { /** * WebGL renderer if available, otherwise CanvasRenderer @@ -46,23 +46,46 @@ this.renderer = autoDetectRenderer(width, height, options, noWebGL); /** - * The root display container that's renderered. + * The root display container that's rendered. * @member {PIXI.Container} */ this.stage = new Container(); /** + * Internal reference to the ticker + * @member {PIXI.ticker.Ticker} + * @private + */ + this._ticker = null; + + /** * Ticker for doing render updates. * @member {PIXI.ticker.Ticker} + * @default PIXI.ticker.shared */ - this.ticker = new Ticker(); - - this.ticker.add(this.render, this); + this.ticker = useSharedTicker ? shared : new Ticker(); // Start the rendering this.start(); } + set ticker(ticker) // eslint-disable-line require-jsdoc + { + if (this._ticker) + { + this._ticker.remove(this.render, this); + } + this._ticker = ticker; + if (ticker) + { + ticker.add(this.render, this); + } + } + get ticker() // eslint-disable-line require-jsdoc + { + return this._ticker; + } + /** * Render the current stage. */ @@ -76,7 +99,7 @@ */ stop() { - this.ticker.stop(); + this._ticker.stop(); } /** @@ -84,7 +107,7 @@ */ start() { - this.ticker.start(); + this._ticker.start(); } /** @@ -98,13 +121,22 @@ } /** + * Reference to the renderer's screen rectangle. Its safe to use as filterArea or hitArea for whole screen + * @member {PIXI.Rectangle} + * @readonly + */ + get screen() + { + return this.renderer.screen; + } + + /** * Destroy and don't use after this. * @param {Boolean} [removeView=false] Automatically remove canvas from DOM. */ destroy(removeView) { this.stop(); - this.ticker.remove(this.render, this); this.ticker = null; this.stage.destroy(); diff --git a/src/core/Shader.js b/src/core/Shader.js new file mode 100644 index 0000000..78b17e6 --- /dev/null +++ b/src/core/Shader.js @@ -0,0 +1,46 @@ +import { GLShader } from 'pixi-gl-core'; +import settings from './settings'; + +function checkPrecision(src, def) +{ + if (src instanceof Array) + { + if (src[0].substring(0, 9) !== 'precision') + { + const copy = src.slice(0); + + copy.unshift(`precision ${def} float;`); + + return copy; + } + } + else if (src.substring(0, 9) !== 'precision') + { + return `precision ${def} float;\n${src}`; + } + + return src; +} + +/** + * Wrapper class, webGL Shader for Pixi. + * Adds precision string if vertexSrc or fragmentSrc have no mention of it. + * + * @class + * @extends GLShader + * @memberof PIXI + */ +export default class Shader extends GLShader +{ + /** + * + * @param {WebGLRenderingContext} gl - The current WebGL rendering context + * @param {string|string[]} vertexSrc - The vertex shader source as an array of strings. + * @param {string|string[]} fragmentSrc - The fragment shader source as an array of strings. + */ + constructor(gl, vertexSrc, fragmentSrc) + { + super(gl, checkPrecision(vertexSrc, settings.PRECISION_VERTEX), + checkPrecision(fragmentSrc, settings.PRECISION_FRAGMENT)); + } +} diff --git a/src/core/const.js b/src/core/const.js index ef6f662..3de0efe 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -188,7 +188,7 @@ * The gc modes that are supported by pixi. * * The {@link PIXI.settings.GC_MODE} Garbage Collection mode for pixi textures is AUTO - * If set to GC_MODE, the renderer will occasianally check textures usage. If they are not + * If set to GC_MODE, the renderer will occasionally check textures usage. If they are not * used for a specified period of time they will be removed from the GPU. They will of course * be uploaded again when they are required. This is a silent behind the scenes process that * should ensure that the GPU does not get filled up. diff --git a/src/core/display/Container.js b/src/core/display/Container.js index ed8cd5f..17fde8d 100644 --- a/src/core/display/Container.js +++ b/src/core/display/Container.js @@ -73,11 +73,12 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.push(child); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -108,11 +109,12 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.splice(index, 0, child); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -225,10 +227,11 @@ if (index === -1) return null; child.parent = null; + // ensure child transform will be recalculated + child.transform._parentID = -1; removeItems(this.children, index, 1); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -249,11 +252,12 @@ { const child = this.getChildAt(index); + // ensure child transform will be recalculated.. child.parent = null; + child.transform._parentID = -1; removeItems(this.children, index, 1); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -284,13 +288,12 @@ for (let i = 0; i < removed.length; ++i) { removed[i].parent = null; + if (removed[i].transform) + { + removed[i].transform._parentID = -1; + } } - // ensure a transform will be recalculated.. - if (this.transform) - { - this.transform._parentID = -1; - } this._boundsID++; this.onChildrenChange(beginIndex); diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index f37f2f5..be4c4fb 100644 --- a/src/core/graphics/Graphics.js +++ b/src/core/graphics/Graphics.js @@ -28,8 +28,9 @@ { /** * + * @param {boolean} [nativeLines=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP */ - constructor() + constructor(nativeLines = false) { super(); @@ -50,6 +51,13 @@ this.lineWidth = 0; /** + * If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * + * @member {boolean} + */ + this.nativeLines = nativeLines; + + /** * The color of any lines drawn. * * @member {string} @@ -757,28 +765,14 @@ if (!this._spriteRect) { - if (!Graphics._SPRITE_TEXTURE) - { - Graphics._SPRITE_TEXTURE = RenderTexture.create(10, 10); - - const canvas = document.createElement('canvas'); - - canvas.width = 10; - canvas.height = 10; - - const context = canvas.getContext('2d'); - - context.fillStyle = 'white'; - context.fillRect(0, 0, 10, 10); - - Graphics._SPRITE_TEXTURE = Texture.fromCanvas(canvas); - } - - this._spriteRect = new Sprite(Graphics._SPRITE_TEXTURE); + this._spriteRect = new Sprite(new Texture(Texture.WHITE)); } + + const sprite = this._spriteRect; + if (this.tint === 0xffffff) { - this._spriteRect.tint = this.graphicsData[0].fillColor; + sprite.tint = this.graphicsData[0].fillColor; } else { @@ -792,20 +786,21 @@ t1[1] *= t2[1]; t1[2] *= t2[2]; - this._spriteRect.tint = rgb2hex(t1); + sprite.tint = rgb2hex(t1); } - this._spriteRect.alpha = this.graphicsData[0].fillAlpha; - this._spriteRect.worldAlpha = this.worldAlpha * this._spriteRect.alpha; + sprite.alpha = this.graphicsData[0].fillAlpha; + sprite.worldAlpha = this.worldAlpha * sprite.alpha; + sprite.blendMode = this.blendMode; - Graphics._SPRITE_TEXTURE._frame.width = rect.width; - Graphics._SPRITE_TEXTURE._frame.height = rect.height; + sprite.texture._frame.width = rect.width; + sprite.texture._frame.height = rect.height; - this._spriteRect.transform.worldTransform = this.transform.worldTransform; + sprite.transform.worldTransform = this.transform.worldTransform; - this._spriteRect.anchor.set(-rect.x / rect.width, -rect.y / rect.height); - this._spriteRect._onAnchorUpdate(); + sprite.anchor.set(-rect.x / rect.width, -rect.y / rect.height); + sprite._onAnchorUpdate(); - this._spriteRect._renderWebGL(renderer); + sprite._renderWebGL(renderer); } /** @@ -1031,6 +1026,7 @@ this.fillColor, this.fillAlpha, this.filling, + this.nativeLines, shape ); diff --git a/src/core/graphics/GraphicsData.js b/src/core/graphics/GraphicsData.js index f5a87ae..b6f03c0 100644 --- a/src/core/graphics/GraphicsData.js +++ b/src/core/graphics/GraphicsData.js @@ -14,14 +14,19 @@ * @param {number} fillColor - the color of the fill * @param {number} fillAlpha - the alpha of the fill * @param {boolean} fill - whether or not the shape is filled with a colour + * @param {boolean} nativeLines - the method for drawing lines * @param {PIXI.Circle|PIXI.Rectangle|PIXI.Ellipse|PIXI.Polygon} shape - The shape object to draw. */ - constructor(lineWidth, lineColor, lineAlpha, fillColor, fillAlpha, fill, shape) + constructor(lineWidth, lineColor, lineAlpha, fillColor, fillAlpha, fill, nativeLines, shape) { /** * @member {number} the width of the line to draw */ this.lineWidth = lineWidth; + /** + * @member {boolean} if true the liens will be draw using LINES instead of TRIANGLE_STRIP + */ + this.nativeLines = nativeLines; /** * @member {number} the color of the line to draw @@ -85,6 +90,7 @@ this.fillColor, this.fillAlpha, this.fill, + this.nativeLines, this.shape ); } diff --git a/src/core/graphics/webgl/GraphicsRenderer.js b/src/core/graphics/webgl/GraphicsRenderer.js index 14083fe..67f5e92 100644 --- a/src/core/graphics/webgl/GraphicsRenderer.js +++ b/src/core/graphics/webgl/GraphicsRenderer.js @@ -102,7 +102,15 @@ shaderTemp.uniforms.alpha = graphics.worldAlpha; renderer.bindVao(webGLData.vao); - webGLData.vao.draw(gl.TRIANGLE_STRIP, webGLData.indices.length); + + if (graphics.nativeLines) + { + gl.drawArrays(gl.LINES, 0, webGLData.points.length / 6); + } + else + { + webGLData.vao.draw(gl.TRIANGLE_STRIP, webGLData.indices.length); + } } } diff --git a/src/core/graphics/webgl/utils/buildLine.js b/src/core/graphics/webgl/utils/buildLine.js index ecf43f7..4d6862e 100644 --- a/src/core/graphics/webgl/utils/buildLine.js +++ b/src/core/graphics/webgl/utils/buildLine.js @@ -11,7 +11,29 @@ * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties * @param {object} webGLData - an object containing all the webGL-specific information to create this shape */ -export default function buildLine(graphicsData, webGLData) +export default function (graphicsData, webGLData) +{ + if (graphicsData.nativeLines) + { + buildNativeLine(graphicsData, webGLData); + } + else + { + buildLine(graphicsData, webGLData); + } +} + +/** + * Builds a line to draw using the poligon method. + * + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @private + * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties + * @param {object} webGLData - an object containing all the webGL-specific information to create this shape + */ +function buildLine(graphicsData, webGLData) { // TODO OPTIMISE! let points = graphicsData.points; @@ -224,3 +246,46 @@ indices.push(indexStart - 1); } + +/** + * Builds a line to draw using the gl.drawArrays(gl.LINES) method + * + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @private + * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties + * @param {object} webGLData - an object containing all the webGL-specific information to create this shape + */ +function buildNativeLine(graphicsData, webGLData) +{ + let i = 0; + const points = graphicsData.points; + + if (points.length === 0) return; + + const verts = webGLData.points; + const length = points.length / 2; + + // sort color + const color = hex2rgb(graphicsData.lineColor); + const alpha = graphicsData.lineAlpha; + const r = color[0] * alpha; + const g = color[1] * alpha; + const b = color[2] * alpha; + + for (i = 1; i < length; i++) + { + const p1x = points[(i - 1) * 2]; + const p1y = points[((i - 1) * 2) + 1]; + + const p2x = points[i * 2]; + const p2y = points[(i * 2) + 1]; + + verts.push(p1x, p1y); + verts.push(r, g, b, alpha); + + verts.push(p2x, p2y); + verts.push(r, g, b, alpha); + } +} diff --git a/src/core/graphics/webgl/utils/buildRoundedRectangle.js b/src/core/graphics/webgl/utils/buildRoundedRectangle.js index 9abc807..a6c8461 100644 --- a/src/core/graphics/webgl/utils/buildRoundedRectangle.js +++ b/src/core/graphics/webgl/utils/buildRoundedRectangle.js @@ -77,6 +77,26 @@ } /** + * Calculate a single point for a quadratic bezier curve. + * Utility function used by quadraticBezierCurve. + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @private + * @param {number} n1 - first number + * @param {number} n2 - second number + * @param {number} perc - percentage + * @return {number} the result + * + */ +function getPt(n1, n2, perc) +{ + const diff = n2 - n1; + + return n1 + (diff * perc); +} + +/** * Calculate the points for a quadratic bezier curve. (helper function..) * Based on: https://stackoverflow.com/questions/785097/how-do-i-implement-a-bezier-curve-in-c * @@ -105,13 +125,6 @@ let x = 0; let y = 0; - function getPt(n1, n2, perc) - { - const diff = n2 - n1; - - return n1 + (diff * perc); - } - for (let i = 0, j = 0; i <= n; ++i) { j = i / n; diff --git a/src/core/index.js b/src/core/index.js index f7a817c..4f414e8 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -30,6 +30,7 @@ export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; export { default as CanvasGraphicsRenderer } from './graphics/canvas/CanvasGraphicsRenderer'; +export { default as Spritesheet } from './textures/Spritesheet'; export { default as Texture } from './textures/Texture'; export { default as BaseTexture } from './textures/BaseTexture'; export { default as RenderTexture } from './textures/RenderTexture'; diff --git a/src/core/math/Matrix.js b/src/core/math/Matrix.js index 0b77477..3679860 100644 --- a/src/core/math/Matrix.js +++ b/src/core/math/Matrix.js @@ -13,45 +13,50 @@ export default class Matrix { /** - * + * @param {number} [a=1] - x scale + * @param {number} [b=0] - y skew + * @param {number} [c=0] - x skew + * @param {number} [d=1] - y scale + * @param {number} [tx=0] - x translation + * @param {number} [ty=0] - y translation */ - constructor() + constructor(a = 1, b = 0, c = 0, d = 1, tx = 0, ty = 0) { /** * @member {number} * @default 1 */ - this.a = 1; + this.a = a; /** * @member {number} * @default 0 */ - this.b = 0; + this.b = b; /** * @member {number} * @default 0 */ - this.c = 0; + this.c = c; /** * @member {number} * @default 1 */ - this.d = 1; + this.d = d; /** * @member {number} * @default 0 */ - this.tx = 0; + this.tx = tx; /** * @member {number} * @default 0 */ - this.ty = 0; + this.ty = ty; this.array = null; } diff --git a/src/core/renderers/SystemRenderer.js b/src/core/renderers/SystemRenderer.js index fa116a6..a24c775 100644 --- a/src/core/renderers/SystemRenderer.js +++ b/src/core/renderers/SystemRenderer.js @@ -1,5 +1,5 @@ import { sayHello, hex2string, hex2rgb } from '../utils'; -import { Matrix } from '../math'; +import { Matrix, Rectangle } from '../math'; import { RENDERER_TYPE } from '../const'; import settings from '../settings'; import Container from '../display/Container'; @@ -21,8 +21,8 @@ { /** * @param {string} system - The name of the system this renderer is for. - * @param {number} [width=800] - the width of the canvas view - * @param {number} [height=600] - the height of the canvas view + * @param {number} [screenWidth=800] - the width of the screen + * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false @@ -37,7 +37,7 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(system, width, height, options) + constructor(system, screenWidth, screenHeight, options) { super(); @@ -69,20 +69,13 @@ this.type = RENDERER_TYPE.UNKNOWN; /** - * The width of the canvas view + * Measurements of the screen. (0, 0, screenWidth, screenHeight) * - * @member {number} - * @default 800 - */ - this.width = width || 800; - - /** - * The height of the canvas view + * Its safe to use as filterArea or hitArea for whole stage * - * @member {number} - * @default 600 + * @member {PIXI.Rectangle} */ - this.height = height || 600; + this.screen = new Rectangle(0, 0, screenWidth || 800, screenHeight || 600); /** * The canvas element that everything is drawn to @@ -107,7 +100,7 @@ this.transparent = options.transparent; /** - * Whether the render view should be resized automatically + * Whether css dimensions of canvas view should be resized to screen dimensions automatically * * @member {boolean} */ @@ -192,23 +185,48 @@ } /** - * Resizes the canvas view to the specified width and height + * Same as view.width, actual number of pixels in the canvas by horizontal * - * @param {number} width - the new width of the canvas view - * @param {number} height - the new height of the canvas view + * @member {number} + * @readonly + * @default 800 */ - resize(width, height) + get width() { - this.width = width * this.resolution; - this.height = height * this.resolution; + return this.view.width; + } - this.view.width = this.width; - this.view.height = this.height; + /** + * Same as view.height, actual number of pixels in the canvas by vertical + * + * @member {number} + * @readonly + * @default 600 + */ + get height() + { + return this.view.height; + } + + /** + * Resizes the screen and canvas to the specified width and height + * Canvas dimensions are multiplied by resolution + * + * @param {number} screenWidth - the new width of the screen + * @param {number} screenHeight - the new height of the screen + */ + resize(screenWidth, screenHeight) + { + this.screen.width = screenWidth; + this.screen.height = screenHeight; + + this.view.width = screenWidth * this.resolution; + this.view.height = screenHeight * this.resolution; if (this.autoResize) { - this.view.style.width = `${this.width / this.resolution}px`; - this.view.style.height = `${this.height / this.resolution}px`; + this.view.style.width = `${screenWidth}px`; + this.view.style.height = `${screenHeight}px`; } } @@ -249,11 +267,10 @@ this.type = RENDERER_TYPE.UNKNOWN; - this.width = 0; - this.height = 0; - this.view = null; + this.screen = null; + this.resolution = 0; this.transparent = false; diff --git a/src/core/renderers/canvas/CanvasRenderer.js b/src/core/renderers/canvas/CanvasRenderer.js index 1bd2d5e..e84e49c 100644 --- a/src/core/renderers/canvas/CanvasRenderer.js +++ b/src/core/renderers/canvas/CanvasRenderer.js @@ -18,8 +18,8 @@ export default class CanvasRenderer extends SystemRenderer { /** - * @param {number} [width=800] - the width of the canvas view - * @param {number} [height=600] - the height of the canvas view + * @param {number} [screenWidth=800] - the width of the screen + * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false @@ -34,9 +34,9 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(width, height, options = {}) + constructor(screenWidth, screenHeight, options = {}) { - super('Canvas', width, height, options); + super('Canvas', screenWidth, screenHeight, options); this.type = RENDERER_TYPE.CANVAS; @@ -96,7 +96,7 @@ this.context = null; this.renderingToScreen = false; - this.resize(width, height); + this.resize(screenWidth, screenHeight); } /** @@ -283,12 +283,12 @@ * * @extends PIXI.SystemRenderer#resize * - * @param {number} width - The new width of the canvas view - * @param {number} height - The new height of the canvas view + * @param {number} screenWidth - the new width of the screen + * @param {number} screenHeight - the new height of the screen */ - resize(width, height) + resize(screenWidth, screenHeight) { - super.resize(width, height); + super.resize(screenWidth, screenHeight); // reset the scale mode.. oddly this seems to be reset when the canvas is resized. // surely a browser bug?? Let pixi fix that for you.. diff --git a/src/core/renderers/webgl/TextureManager.js b/src/core/renderers/webgl/TextureManager.js index b685c1a..8227289 100644 --- a/src/core/renderers/webgl/TextureManager.js +++ b/src/core/renderers/webgl/TextureManager.js @@ -61,7 +61,7 @@ * Updates and/or Creates a WebGL texture for the renderer's context. * * @param {PIXI.BaseTexture|PIXI.Texture} texture - the texture to update - * @param {Number} location - the location the texture will be bound to. + * @param {number} location - the location the texture will be bound to. * @return {GLTexture} The gl texture. */ updateTexture(texture, location) diff --git a/src/core/renderers/webgl/WebGLRenderer.js b/src/core/renderers/webgl/WebGLRenderer.js index e4cd8a0..830b382 100644 --- a/src/core/renderers/webgl/WebGLRenderer.js +++ b/src/core/renderers/webgl/WebGLRenderer.js @@ -31,8 +31,8 @@ { /** * - * @param {number} [width=0] - the width of the canvas view - * @param {number} [height=0] - the height of the canvas view + * @param {number} [screenWidth=800] - the width of the screen + * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false @@ -53,9 +53,9 @@ * @param {boolean} [options.legacy=false] - If true Pixi will aim to ensure compatibility * with older / less advanced devices. If you experiance unexplained flickering try setting this to true. */ - constructor(width, height, options = {}) + constructor(screenWidth, screenHeight, options = {}) { - super('WebGL', width, height, options); + super('WebGL', screenWidth, screenHeight, options); this.legacy = !!options.legacy; @@ -245,7 +245,7 @@ glCore._testingContext = gl; // setup the width/height properties and gl viewport - this.resize(this.width, this.height); + this.resize(this.screen.width, this.screen.height); } /** @@ -343,16 +343,16 @@ /** * Resizes the webGL view to the specified width and height. * - * @param {number} width - the new width of the webGL view - * @param {number} height - the new height of the webGL view + * @param {number} screenWidth - the new width of the screen + * @param {number} screenHeight - the new height of the screen */ - resize(width, height) + resize(screenWidth, screenHeight) { // if(width * this.resolution === this.width && height * this.resolution === this.height)return; - SystemRenderer.prototype.resize.call(this, width, height); + SystemRenderer.prototype.resize.call(this, screenWidth, screenHeight); - this.rootRenderTarget.resize(width, height); + this.rootRenderTarget.resize(screenWidth, screenHeight); if (this._activeRenderTarget === this.rootRenderTarget) { @@ -396,6 +396,26 @@ } /** + * Erases the render texture and fills the drawing area with a colour + * + * @param {PIXI.RenderTexture} renderTexture - The render texture to clear + * @param {number} [clearColor] - The colour + * @return {PIXI.WebGLRenderer} Returns itself. + */ + clearRenderTexture(renderTexture, clearColor) + { + const baseTexture = renderTexture.baseTexture; + const renderTarget = baseTexture._glRenderTargets[this.CONTEXT_UID]; + + if (renderTarget) + { + renderTarget.clear(clearColor); + } + + return this; + } + + /** * Binds a render texture for rendering * * @param {PIXI.RenderTexture} renderTexture - The render texture to render @@ -474,9 +494,10 @@ * Changes the current GLShader to the one given in parameter * * @param {PIXI.glCore.Shader} shader - the new glShader + * @param {boolean} [autoProject=true] - Whether automatically set the projection matrix * @return {PIXI.WebGLRenderer} Returns itself. */ - _bindGLShader(shader) + _bindGLShader(shader, autoProject) { // TODO cache if (this._activeShader !== shader) @@ -484,8 +505,14 @@ this._activeShader = shader; shader.bind(); - // automatically set the projection matrix - shader.uniforms.projectionMatrix = this._activeRenderTarget.projectionMatrix.toArray(true); + // `autoProject` normally would be a default parameter set to true + // but because of how Babel transpiles default parameters + // it hinders the performance of this method. + if (autoProject !== false) + { + // automatically set the projection matrix + shader.uniforms.projectionMatrix = this._activeRenderTarget.projectionMatrix.toArray(true); + } } return this; diff --git a/src/core/renderers/webgl/WebGLState.js b/src/core/renderers/webgl/WebGLState.js index ba6ecc5..6c846d5 100755 --- a/src/core/renderers/webgl/WebGLState.js +++ b/src/core/renderers/webgl/WebGLState.js @@ -82,18 +82,20 @@ push() { // next state.. - let state = this.stack[++this.stackIndex]; + let state = this.stack[this.stackIndex]; if (!state) { state = this.stack[this.stackIndex] = new Uint8Array(16); } + ++this.stackIndex; + // copy state.. // set active state so we can force overrides of gl state for (let i = 0; i < this.activeState.length; i++) { - this.activeState[i] = state[i]; + state[i] = this.activeState[i]; } } diff --git a/src/core/renderers/webgl/filters/Filter.js b/src/core/renderers/webgl/filters/Filter.js index 2c3eaad..4eb6128 100644 --- a/src/core/renderers/webgl/filters/Filter.js +++ b/src/core/renderers/webgl/filters/Filter.js @@ -9,7 +9,7 @@ * @memberof PIXI * @extends PIXI.Shader */ -class Filter extends Shader +export default class Filter extends Shader { /** * @param {string} [vertexSrc] - The source of the vertex shader. @@ -56,15 +56,16 @@ * @param {PIXI.RenderTarget} input - The input render target. * @param {PIXI.RenderTarget} output - The target to output to. * @param {boolean} clear - Should the output be cleared before rendering to it + * @param {object} [currentState] - It's current state of filter. + * There are some useful properties in the currentState : + * target, filters, sourceFrame, destinationFrame, renderTarget, resolution */ - apply(filterManager, input, output, clear) + apply(filterManager, input, output, clear, currentState) // eslint-disable-line no-unused-vars { // do as you please! - filterManager.applyFilter(this, input, output, clear); + filterManager.applyFilter(this, input, output, clear, currentState); // or just do a regular render.. } } - -export default Filter; diff --git a/src/core/renderers/webgl/filters/extractUniformsFromSrc.js b/src/core/renderers/webgl/filters/extractUniformsFromSrc.js index d78a0b1..4893214 100644 --- a/src/core/renderers/webgl/filters/extractUniformsFromSrc.js +++ b/src/core/renderers/webgl/filters/extractUniformsFromSrc.js @@ -12,7 +12,7 @@ function extractUniformsFromString(string) { - const maskRegex = new RegExp('^(projectionMatrix|uSampler|filterArea)$'); + const maskRegex = new RegExp('^(projectionMatrix|uSampler|filterArea|filterClamp)$'); const uniforms = {}; let nameSplit; diff --git a/src/core/renderers/webgl/managers/FilterManager.js b/src/core/renderers/webgl/managers/FilterManager.js index 43fc869..6c7422d 100644 --- a/src/core/renderers/webgl/managers/FilterManager.js +++ b/src/core/renderers/webgl/managers/FilterManager.js @@ -2,6 +2,7 @@ import RenderTarget from '../utils/RenderTarget'; import Quad from '../utils/Quad'; import { Rectangle } from '../../../math'; +import Shader from '../../../Shader'; import * as filterTransforms from '../filters/filterTransforms'; import bitTwiddle from 'bit-twiddle'; @@ -22,8 +23,6 @@ this.filters = []; this.target = null; this.resolution = 1; - - this.firstRun = true; } } @@ -155,7 +154,7 @@ if (filters.length === 1) { - filters[0].apply(this, currentState.renderTarget, lastState.renderTarget, false); + filters[0].apply(this, currentState.renderTarget, lastState.renderTarget, false, currentState); this.freePotRenderTarget(currentState.renderTarget); } else @@ -177,7 +176,7 @@ for (i = 0; i < filters.length - 1; ++i) { - filters[i].apply(this, flip, flop, true); + filters[i].apply(this, flip, flop, true, currentState); const t = flip; @@ -185,7 +184,7 @@ flop = t; } - filters[i].apply(this, flip, lastState.renderTarget, false); + filters[i].apply(this, flip, lastState.renderTarget, false, currentState); this.freePotRenderTarget(flip); this.freePotRenderTarget(flop); @@ -212,16 +211,28 @@ const renderer = this.renderer; const gl = renderer.gl; - renderer.bindShader(filter, true); + let shader = filter.glShaders[renderer.CONTEXT_UID]; - // TODO theres a better way! - // quad can be a mesh and we then should be able to pull this off without accessing the shader directly - const shader = renderer.shaderManager.getGLShader(); - - // TODO change quad to mesh.. - if (!this.firstRun) + // cacheing.. + if (!shader) { - this.firstRun = true; + if (filter.glShaderKey) + { + shader = this.shaderCache[filter.glShaderKey]; + + if (!shader) + { + shader = new Shader(this.gl, filter.vertexSrc, filter.fragmentSrc); + + filter.glShaders[renderer.CONTEXT_UID] = this.shaderCache[filter.glShaderKey] = shader; + } + } + else + { + shader = filter.glShaders[renderer.CONTEXT_UID] = new Shader(this.gl, filter.vertexSrc, filter.fragmentSrc); + } + + // TODO - this only needs to be done once? renderer.bindVao(null); this.quad.initVao(shader); @@ -244,58 +255,146 @@ renderer.maskManager.pushScissorMask(null, renderer.maskManager.scissorData); } - // renderer._bindGLShader(shader); + renderer.bindShader(shader); - let currentState; - const uniforms = filter.uniforms; + // free unit 0 for us, doesn't matter what was there + // don't try to restore it, because syncUniforms can upload it to another slot + // and it'll be a problem + const tex = this.renderer.emptyTextures[0]; - if (uniforms.filterArea) - { - currentState = this.filterData.stack[this.filterData.index]; - - const filterArea = uniforms.filterArea; - - filterArea[0] = currentState.renderTarget.size.width; - filterArea[1] = currentState.renderTarget.size.height; - filterArea[2] = currentState.sourceFrame.x; - filterArea[3] = currentState.sourceFrame.y; - - filter.uniforms.filterArea = filterArea; - } - - // use this to clamp displaced texture coords so they belong to filterArea - // see displacementFilter fragment shader for an example - if (uniforms.filterClamp) - { - currentState = this.filterData.stack[this.filterData.index]; - - const filterClamp = uniforms.filterClamp; - - filterClamp[0] = 0; - filterClamp[1] = 0; - filterClamp[2] = (currentState.sourceFrame.width - 1) / currentState.renderTarget.size.width; - filterClamp[3] = (currentState.sourceFrame.height - 1) / currentState.renderTarget.size.height; - - filter.uniforms.filterClamp = filterClamp; - } - - renderer.shaderManager.setUniforms(filter.uniforms); + this.renderer.boundTextures[0] = tex; + // this syncs the pixi filters uniforms with glsl uniforms + this.syncUniforms(shader, filter); renderer.state.setBlendMode(filter.blendMode); - // temporary bypass cache.. - const tex = this.renderer.boundTextures[0]; - gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, input.texture.texture); this.quad.vao.draw(this.renderer.gl.TRIANGLES, 6, 0); - // restore cache. gl.bindTexture(gl.TEXTURE_2D, tex._glTextures[this.renderer.CONTEXT_UID].texture); } /** + * Uploads the uniforms of the filter. + * + * @param {GLShader} shader - The underlying gl shader. + * @param {PIXI.Filter} filter - The filter we are synchronizing. + */ + syncUniforms(shader, filter) + { + const uniformData = filter.uniformData; + const uniforms = filter.uniforms; + + // 0 is reserved for the pixi texture so we start at 1! + let textureCount = 1; + let currentState; + + // filterArea and filterClamp that are handled by FilterManager directly + // they must not appear in uniformData + + if (shader.uniforms.filterArea) + { + currentState = this.filterData.stack[this.filterData.index]; + + const filterArea = shader.uniforms.filterArea; + + filterArea[0] = currentState.renderTarget.size.width; + filterArea[1] = currentState.renderTarget.size.height; + filterArea[2] = currentState.sourceFrame.x; + filterArea[3] = currentState.sourceFrame.y; + + shader.uniforms.filterArea = filterArea; + } + + // use this to clamp displaced texture coords so they belong to filterArea + // see displacementFilter fragment shader for an example + if (shader.uniforms.filterClamp) + { + currentState = currentState || this.filterData.stack[this.filterData.index]; + + const filterClamp = shader.uniforms.filterClamp; + + filterClamp[0] = 0; + filterClamp[1] = 0; + filterClamp[2] = (currentState.sourceFrame.width - 1) / currentState.renderTarget.size.width; + filterClamp[3] = (currentState.sourceFrame.height - 1) / currentState.renderTarget.size.height; + + shader.uniforms.filterClamp = filterClamp; + } + + // TODO Cacheing layer.. + for (const i in uniformData) + { + if (uniformData[i].type === 'sampler2D' && uniforms[i] !== 0) + { + if (uniforms[i].baseTexture) + { + shader.uniforms[i] = this.renderer.bindTexture(uniforms[i].baseTexture, textureCount); + } + else + { + shader.uniforms[i] = textureCount; + + // TODO + // this is helpful as renderTargets can also be set. + // Although thinking about it, we could probably + // make the filter texture cache return a RenderTexture + // rather than a renderTarget + const gl = this.renderer.gl; + + this.renderer.boundTextures[textureCount] = this.renderer.emptyTextures[textureCount]; + gl.activeTexture(gl.TEXTURE0 + textureCount); + + uniforms[i].texture.bind(); + } + + textureCount++; + } + else if (uniformData[i].type === 'mat3') + { + // check if its pixi matrix.. + if (uniforms[i].a !== undefined) + { + shader.uniforms[i] = uniforms[i].toArray(true); + } + else + { + shader.uniforms[i] = uniforms[i]; + } + } + else if (uniformData[i].type === 'vec2') + { + // check if its a point.. + if (uniforms[i].x !== undefined) + { + const val = shader.uniforms[i] || new Float32Array(2); + + val[0] = uniforms[i].x; + val[1] = uniforms[i].y; + shader.uniforms[i] = val; + } + else + { + shader.uniforms[i] = uniforms[i]; + } + } + else if (uniformData[i].type === 'float') + { + if (shader.uniforms.data[i].value !== uniformData[i]) + { + shader.uniforms[i] = uniforms[i]; + } + } + else + { + shader.uniforms[i] = uniforms[i]; + } + } + } + + /** * Gets a render target from the pool, or creates a new one. * * @param {boolean} clear - Should we clear the render texture when we get it? @@ -442,7 +541,6 @@ renderTarget.resolution = resolution; renderTarget.defaultFrame.width = renderTarget.size.width = minWidth / resolution; renderTarget.defaultFrame.height = renderTarget.size.height = minHeight / resolution; - renderTarget.attachStencilBuffer(); return renderTarget; } diff --git a/src/core/settings.js b/src/core/settings.js index bb08cb0..f4dd087 100644 --- a/src/core/settings.js +++ b/src/core/settings.js @@ -77,11 +77,11 @@ * * @static * @memberof PIXI.settings - * @type {RegExp|string} + * @type {RegExp} * @example `@2x` - * @default /@(.+)x/ + * @default /@([0-9\.]+)x/ */ - RETINA_PREFIX: /@(.+)x/, + RETINA_PREFIX: /@([0-9\.]+)x/, /** * The default render options if none are supplied to {@link PIXI.WebGLRenderer} @@ -175,14 +175,24 @@ SCALE_MODE: 0, /** - * Default specify float precision in shaders. + * Default specify float precision in vertex shader. + * + * @static + * @memberof PIXI.settings + * @type {PIXI.PRECISION} + * @default PIXI.PRECISION.HIGH + */ + PRECISION_VERTEX: 'highp', + + /** + * Default specify float precision in fragment shader. * * @static * @memberof PIXI.settings * @type {PIXI.PRECISION} * @default PIXI.PRECISION.MEDIUM */ - PRECISION: 'mediump', + PRECISION_FRAGMENT: 'mediump', /** * Can we upload the same buffer in a single frame? diff --git a/src/core/sprites/Sprite.js b/src/core/sprites/Sprite.js index 28b0b18..121bf58 100644 --- a/src/core/sprites/Sprite.js +++ b/src/core/sprites/Sprite.js @@ -212,11 +212,11 @@ } else { - w0 = orig.width * (1 - anchor._x); - w1 = orig.width * -anchor._x; + w1 = -anchor._x * orig.width; + w0 = w1 + orig.width; - h0 = orig.height * (1 - anchor._y); - h1 = orig.height * -anchor._y; + h1 = -anchor._y * orig.height; + h0 = h1 + orig.height; } // xy @@ -269,11 +269,11 @@ const tx = wt.tx; const ty = wt.ty; - const w0 = (orig.width) * (1 - anchor._x); - const w1 = (orig.width) * -anchor._x; + const w1 = -anchor._x * orig.width; + const w0 = w1 + orig.width; - const h0 = orig.height * (1 - anchor._y); - const h1 = orig.height * -anchor._y; + const h1 = -anchor._y * orig.height; + const h0 = h1 + orig.height; // xy vertexData[0] = (a * w1) + (c * h1) + tx; @@ -440,7 +440,7 @@ * * @static * @param {number|string|PIXI.BaseTexture|HTMLCanvasElement|HTMLVideoElement} source Source to create texture from - * @return {PIXI.Sprite} The newly created texture + * @return {PIXI.Sprite} The newly created sprite */ static from(source) { @@ -538,8 +538,8 @@ } /** - * The tint applied to the sprite. This is a hex value. A value of - * 0xFFFFFF will remove any tint effect. + * The tint applied to the sprite. This is a hex value. + * A value of 0xFFFFFF will remove any tint effect. * * @member {number} * @default 0xFFFFFF diff --git a/src/core/sprites/canvas/CanvasTinter.js b/src/core/sprites/canvas/CanvasTinter.js index 1cf316b..27e5e0c 100644 --- a/src/core/sprites/canvas/CanvasTinter.js +++ b/src/core/sprites/canvas/CanvasTinter.js @@ -75,8 +75,8 @@ crop.width *= resolution; crop.height *= resolution; - canvas.width = crop.width; - canvas.height = crop.height; + canvas.width = Math.ceil(crop.width); + canvas.height = Math.ceil(crop.height); context.fillStyle = `#${(`00000${(color | 0).toString(16)}`).substr(-6)}`; @@ -130,8 +130,8 @@ crop.width *= resolution; crop.height *= resolution; - canvas.width = crop.width; - canvas.height = crop.height; + canvas.width = Math.ceil(crop.width); + canvas.height = Math.ceil(crop.height); context.globalCompositeOperation = 'copy'; context.fillStyle = `#${(`00000${(color | 0).toString(16)}`).substr(-6)}`; @@ -172,8 +172,8 @@ crop.width *= resolution; crop.height *= resolution; - canvas.width = crop.width; - canvas.height = crop.height; + canvas.width = Math.ceil(crop.width); + canvas.height = Math.ceil(crop.height); context.globalCompositeOperation = 'copy'; context.drawImage( diff --git a/package.json b/package.json index 6a366ba..71ade15 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pixi.js", - "version": "4.3.4", + "version": "4.3.5", "description": "Pixi.js is a fast lightweight 2D library that works across all devices.", "author": "Mat Groves", "contributors": [ @@ -61,7 +61,7 @@ "ismobilejs": "^0.4.0", "object-assign": "^4.0.1", "pixi-gl-core": "^1.0.3", - "resource-loader": "^2.0.4" + "resource-loader": "^2.0.6" }, "devDependencies": { "babel-cli": "^6.18.0", @@ -70,9 +70,9 @@ "babel-preset-es2015": "^6.14.0", "babelify": "^7.3.0", "del": "^2.2.0", - "electron-prebuilt": "^1.3.2", + "electron": "^1.4.15", "eslint": "^3.5.0", - "floss": "^1.2.0", + "floss": "^2.0.1", "gh-pages": "^0.11.0", "jaguarjs-jsdoc": "^1.0.1", "js-md5": "^0.4.1", diff --git a/src/accessibility/AccessibilityManager.js b/src/accessibility/AccessibilityManager.js index df2716a..f16bcb1 100644 --- a/src/accessibility/AccessibilityManager.js +++ b/src/accessibility/AccessibilityManager.js @@ -3,7 +3,7 @@ import accessibleTarget from './accessibleTarget'; // add some extra variables to the container.. -Object.assign( +core.utils.mixins.delayMixin( core.DisplayObject.prototype, accessibleTarget ); diff --git a/src/core/Application.js b/src/core/Application.js index ea0f058..5340578 100644 --- a/src/core/Application.js +++ b/src/core/Application.js @@ -1,6 +1,6 @@ import { autoDetectRenderer } from './autoDetectRenderer'; import Container from './display/Container'; -import Ticker from './ticker/Ticker'; +import { shared, Ticker } from './ticker'; /** * Convenience class to create a new PIXI application. @@ -35,9 +35,9 @@ * @param {boolean} [noWebGL=false] - prevents selection of WebGL renderer, even if such is present * @param {boolean} [options.legacy=false] - If true Pixi will aim to ensure compatibility * with older / less advanced devices. If you experience unexplained flickering try setting this to true. - + * @param {boolean} [useSharedTicker=false] - `true` to use PIXI.ticker.shared, `false` to create new ticker. */ - constructor(width, height, options, noWebGL) + constructor(width, height, options, noWebGL, useSharedTicker = false) { /** * WebGL renderer if available, otherwise CanvasRenderer @@ -46,23 +46,46 @@ this.renderer = autoDetectRenderer(width, height, options, noWebGL); /** - * The root display container that's renderered. + * The root display container that's rendered. * @member {PIXI.Container} */ this.stage = new Container(); /** + * Internal reference to the ticker + * @member {PIXI.ticker.Ticker} + * @private + */ + this._ticker = null; + + /** * Ticker for doing render updates. * @member {PIXI.ticker.Ticker} + * @default PIXI.ticker.shared */ - this.ticker = new Ticker(); - - this.ticker.add(this.render, this); + this.ticker = useSharedTicker ? shared : new Ticker(); // Start the rendering this.start(); } + set ticker(ticker) // eslint-disable-line require-jsdoc + { + if (this._ticker) + { + this._ticker.remove(this.render, this); + } + this._ticker = ticker; + if (ticker) + { + ticker.add(this.render, this); + } + } + get ticker() // eslint-disable-line require-jsdoc + { + return this._ticker; + } + /** * Render the current stage. */ @@ -76,7 +99,7 @@ */ stop() { - this.ticker.stop(); + this._ticker.stop(); } /** @@ -84,7 +107,7 @@ */ start() { - this.ticker.start(); + this._ticker.start(); } /** @@ -98,13 +121,22 @@ } /** + * Reference to the renderer's screen rectangle. Its safe to use as filterArea or hitArea for whole screen + * @member {PIXI.Rectangle} + * @readonly + */ + get screen() + { + return this.renderer.screen; + } + + /** * Destroy and don't use after this. * @param {Boolean} [removeView=false] Automatically remove canvas from DOM. */ destroy(removeView) { this.stop(); - this.ticker.remove(this.render, this); this.ticker = null; this.stage.destroy(); diff --git a/src/core/Shader.js b/src/core/Shader.js new file mode 100644 index 0000000..78b17e6 --- /dev/null +++ b/src/core/Shader.js @@ -0,0 +1,46 @@ +import { GLShader } from 'pixi-gl-core'; +import settings from './settings'; + +function checkPrecision(src, def) +{ + if (src instanceof Array) + { + if (src[0].substring(0, 9) !== 'precision') + { + const copy = src.slice(0); + + copy.unshift(`precision ${def} float;`); + + return copy; + } + } + else if (src.substring(0, 9) !== 'precision') + { + return `precision ${def} float;\n${src}`; + } + + return src; +} + +/** + * Wrapper class, webGL Shader for Pixi. + * Adds precision string if vertexSrc or fragmentSrc have no mention of it. + * + * @class + * @extends GLShader + * @memberof PIXI + */ +export default class Shader extends GLShader +{ + /** + * + * @param {WebGLRenderingContext} gl - The current WebGL rendering context + * @param {string|string[]} vertexSrc - The vertex shader source as an array of strings. + * @param {string|string[]} fragmentSrc - The fragment shader source as an array of strings. + */ + constructor(gl, vertexSrc, fragmentSrc) + { + super(gl, checkPrecision(vertexSrc, settings.PRECISION_VERTEX), + checkPrecision(fragmentSrc, settings.PRECISION_FRAGMENT)); + } +} diff --git a/src/core/const.js b/src/core/const.js index ef6f662..3de0efe 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -188,7 +188,7 @@ * The gc modes that are supported by pixi. * * The {@link PIXI.settings.GC_MODE} Garbage Collection mode for pixi textures is AUTO - * If set to GC_MODE, the renderer will occasianally check textures usage. If they are not + * If set to GC_MODE, the renderer will occasionally check textures usage. If they are not * used for a specified period of time they will be removed from the GPU. They will of course * be uploaded again when they are required. This is a silent behind the scenes process that * should ensure that the GPU does not get filled up. diff --git a/src/core/display/Container.js b/src/core/display/Container.js index ed8cd5f..17fde8d 100644 --- a/src/core/display/Container.js +++ b/src/core/display/Container.js @@ -73,11 +73,12 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.push(child); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -108,11 +109,12 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.splice(index, 0, child); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -225,10 +227,11 @@ if (index === -1) return null; child.parent = null; + // ensure child transform will be recalculated + child.transform._parentID = -1; removeItems(this.children, index, 1); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -249,11 +252,12 @@ { const child = this.getChildAt(index); + // ensure child transform will be recalculated.. child.parent = null; + child.transform._parentID = -1; removeItems(this.children, index, 1); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -284,13 +288,12 @@ for (let i = 0; i < removed.length; ++i) { removed[i].parent = null; + if (removed[i].transform) + { + removed[i].transform._parentID = -1; + } } - // ensure a transform will be recalculated.. - if (this.transform) - { - this.transform._parentID = -1; - } this._boundsID++; this.onChildrenChange(beginIndex); diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index f37f2f5..be4c4fb 100644 --- a/src/core/graphics/Graphics.js +++ b/src/core/graphics/Graphics.js @@ -28,8 +28,9 @@ { /** * + * @param {boolean} [nativeLines=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP */ - constructor() + constructor(nativeLines = false) { super(); @@ -50,6 +51,13 @@ this.lineWidth = 0; /** + * If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * + * @member {boolean} + */ + this.nativeLines = nativeLines; + + /** * The color of any lines drawn. * * @member {string} @@ -757,28 +765,14 @@ if (!this._spriteRect) { - if (!Graphics._SPRITE_TEXTURE) - { - Graphics._SPRITE_TEXTURE = RenderTexture.create(10, 10); - - const canvas = document.createElement('canvas'); - - canvas.width = 10; - canvas.height = 10; - - const context = canvas.getContext('2d'); - - context.fillStyle = 'white'; - context.fillRect(0, 0, 10, 10); - - Graphics._SPRITE_TEXTURE = Texture.fromCanvas(canvas); - } - - this._spriteRect = new Sprite(Graphics._SPRITE_TEXTURE); + this._spriteRect = new Sprite(new Texture(Texture.WHITE)); } + + const sprite = this._spriteRect; + if (this.tint === 0xffffff) { - this._spriteRect.tint = this.graphicsData[0].fillColor; + sprite.tint = this.graphicsData[0].fillColor; } else { @@ -792,20 +786,21 @@ t1[1] *= t2[1]; t1[2] *= t2[2]; - this._spriteRect.tint = rgb2hex(t1); + sprite.tint = rgb2hex(t1); } - this._spriteRect.alpha = this.graphicsData[0].fillAlpha; - this._spriteRect.worldAlpha = this.worldAlpha * this._spriteRect.alpha; + sprite.alpha = this.graphicsData[0].fillAlpha; + sprite.worldAlpha = this.worldAlpha * sprite.alpha; + sprite.blendMode = this.blendMode; - Graphics._SPRITE_TEXTURE._frame.width = rect.width; - Graphics._SPRITE_TEXTURE._frame.height = rect.height; + sprite.texture._frame.width = rect.width; + sprite.texture._frame.height = rect.height; - this._spriteRect.transform.worldTransform = this.transform.worldTransform; + sprite.transform.worldTransform = this.transform.worldTransform; - this._spriteRect.anchor.set(-rect.x / rect.width, -rect.y / rect.height); - this._spriteRect._onAnchorUpdate(); + sprite.anchor.set(-rect.x / rect.width, -rect.y / rect.height); + sprite._onAnchorUpdate(); - this._spriteRect._renderWebGL(renderer); + sprite._renderWebGL(renderer); } /** @@ -1031,6 +1026,7 @@ this.fillColor, this.fillAlpha, this.filling, + this.nativeLines, shape ); diff --git a/src/core/graphics/GraphicsData.js b/src/core/graphics/GraphicsData.js index f5a87ae..b6f03c0 100644 --- a/src/core/graphics/GraphicsData.js +++ b/src/core/graphics/GraphicsData.js @@ -14,14 +14,19 @@ * @param {number} fillColor - the color of the fill * @param {number} fillAlpha - the alpha of the fill * @param {boolean} fill - whether or not the shape is filled with a colour + * @param {boolean} nativeLines - the method for drawing lines * @param {PIXI.Circle|PIXI.Rectangle|PIXI.Ellipse|PIXI.Polygon} shape - The shape object to draw. */ - constructor(lineWidth, lineColor, lineAlpha, fillColor, fillAlpha, fill, shape) + constructor(lineWidth, lineColor, lineAlpha, fillColor, fillAlpha, fill, nativeLines, shape) { /** * @member {number} the width of the line to draw */ this.lineWidth = lineWidth; + /** + * @member {boolean} if true the liens will be draw using LINES instead of TRIANGLE_STRIP + */ + this.nativeLines = nativeLines; /** * @member {number} the color of the line to draw @@ -85,6 +90,7 @@ this.fillColor, this.fillAlpha, this.fill, + this.nativeLines, this.shape ); } diff --git a/src/core/graphics/webgl/GraphicsRenderer.js b/src/core/graphics/webgl/GraphicsRenderer.js index 14083fe..67f5e92 100644 --- a/src/core/graphics/webgl/GraphicsRenderer.js +++ b/src/core/graphics/webgl/GraphicsRenderer.js @@ -102,7 +102,15 @@ shaderTemp.uniforms.alpha = graphics.worldAlpha; renderer.bindVao(webGLData.vao); - webGLData.vao.draw(gl.TRIANGLE_STRIP, webGLData.indices.length); + + if (graphics.nativeLines) + { + gl.drawArrays(gl.LINES, 0, webGLData.points.length / 6); + } + else + { + webGLData.vao.draw(gl.TRIANGLE_STRIP, webGLData.indices.length); + } } } diff --git a/src/core/graphics/webgl/utils/buildLine.js b/src/core/graphics/webgl/utils/buildLine.js index ecf43f7..4d6862e 100644 --- a/src/core/graphics/webgl/utils/buildLine.js +++ b/src/core/graphics/webgl/utils/buildLine.js @@ -11,7 +11,29 @@ * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties * @param {object} webGLData - an object containing all the webGL-specific information to create this shape */ -export default function buildLine(graphicsData, webGLData) +export default function (graphicsData, webGLData) +{ + if (graphicsData.nativeLines) + { + buildNativeLine(graphicsData, webGLData); + } + else + { + buildLine(graphicsData, webGLData); + } +} + +/** + * Builds a line to draw using the poligon method. + * + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @private + * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties + * @param {object} webGLData - an object containing all the webGL-specific information to create this shape + */ +function buildLine(graphicsData, webGLData) { // TODO OPTIMISE! let points = graphicsData.points; @@ -224,3 +246,46 @@ indices.push(indexStart - 1); } + +/** + * Builds a line to draw using the gl.drawArrays(gl.LINES) method + * + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @private + * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties + * @param {object} webGLData - an object containing all the webGL-specific information to create this shape + */ +function buildNativeLine(graphicsData, webGLData) +{ + let i = 0; + const points = graphicsData.points; + + if (points.length === 0) return; + + const verts = webGLData.points; + const length = points.length / 2; + + // sort color + const color = hex2rgb(graphicsData.lineColor); + const alpha = graphicsData.lineAlpha; + const r = color[0] * alpha; + const g = color[1] * alpha; + const b = color[2] * alpha; + + for (i = 1; i < length; i++) + { + const p1x = points[(i - 1) * 2]; + const p1y = points[((i - 1) * 2) + 1]; + + const p2x = points[i * 2]; + const p2y = points[(i * 2) + 1]; + + verts.push(p1x, p1y); + verts.push(r, g, b, alpha); + + verts.push(p2x, p2y); + verts.push(r, g, b, alpha); + } +} diff --git a/src/core/graphics/webgl/utils/buildRoundedRectangle.js b/src/core/graphics/webgl/utils/buildRoundedRectangle.js index 9abc807..a6c8461 100644 --- a/src/core/graphics/webgl/utils/buildRoundedRectangle.js +++ b/src/core/graphics/webgl/utils/buildRoundedRectangle.js @@ -77,6 +77,26 @@ } /** + * Calculate a single point for a quadratic bezier curve. + * Utility function used by quadraticBezierCurve. + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @private + * @param {number} n1 - first number + * @param {number} n2 - second number + * @param {number} perc - percentage + * @return {number} the result + * + */ +function getPt(n1, n2, perc) +{ + const diff = n2 - n1; + + return n1 + (diff * perc); +} + +/** * Calculate the points for a quadratic bezier curve. (helper function..) * Based on: https://stackoverflow.com/questions/785097/how-do-i-implement-a-bezier-curve-in-c * @@ -105,13 +125,6 @@ let x = 0; let y = 0; - function getPt(n1, n2, perc) - { - const diff = n2 - n1; - - return n1 + (diff * perc); - } - for (let i = 0, j = 0; i <= n; ++i) { j = i / n; diff --git a/src/core/index.js b/src/core/index.js index f7a817c..4f414e8 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -30,6 +30,7 @@ export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; export { default as CanvasGraphicsRenderer } from './graphics/canvas/CanvasGraphicsRenderer'; +export { default as Spritesheet } from './textures/Spritesheet'; export { default as Texture } from './textures/Texture'; export { default as BaseTexture } from './textures/BaseTexture'; export { default as RenderTexture } from './textures/RenderTexture'; diff --git a/src/core/math/Matrix.js b/src/core/math/Matrix.js index 0b77477..3679860 100644 --- a/src/core/math/Matrix.js +++ b/src/core/math/Matrix.js @@ -13,45 +13,50 @@ export default class Matrix { /** - * + * @param {number} [a=1] - x scale + * @param {number} [b=0] - y skew + * @param {number} [c=0] - x skew + * @param {number} [d=1] - y scale + * @param {number} [tx=0] - x translation + * @param {number} [ty=0] - y translation */ - constructor() + constructor(a = 1, b = 0, c = 0, d = 1, tx = 0, ty = 0) { /** * @member {number} * @default 1 */ - this.a = 1; + this.a = a; /** * @member {number} * @default 0 */ - this.b = 0; + this.b = b; /** * @member {number} * @default 0 */ - this.c = 0; + this.c = c; /** * @member {number} * @default 1 */ - this.d = 1; + this.d = d; /** * @member {number} * @default 0 */ - this.tx = 0; + this.tx = tx; /** * @member {number} * @default 0 */ - this.ty = 0; + this.ty = ty; this.array = null; } diff --git a/src/core/renderers/SystemRenderer.js b/src/core/renderers/SystemRenderer.js index fa116a6..a24c775 100644 --- a/src/core/renderers/SystemRenderer.js +++ b/src/core/renderers/SystemRenderer.js @@ -1,5 +1,5 @@ import { sayHello, hex2string, hex2rgb } from '../utils'; -import { Matrix } from '../math'; +import { Matrix, Rectangle } from '../math'; import { RENDERER_TYPE } from '../const'; import settings from '../settings'; import Container from '../display/Container'; @@ -21,8 +21,8 @@ { /** * @param {string} system - The name of the system this renderer is for. - * @param {number} [width=800] - the width of the canvas view - * @param {number} [height=600] - the height of the canvas view + * @param {number} [screenWidth=800] - the width of the screen + * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false @@ -37,7 +37,7 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(system, width, height, options) + constructor(system, screenWidth, screenHeight, options) { super(); @@ -69,20 +69,13 @@ this.type = RENDERER_TYPE.UNKNOWN; /** - * The width of the canvas view + * Measurements of the screen. (0, 0, screenWidth, screenHeight) * - * @member {number} - * @default 800 - */ - this.width = width || 800; - - /** - * The height of the canvas view + * Its safe to use as filterArea or hitArea for whole stage * - * @member {number} - * @default 600 + * @member {PIXI.Rectangle} */ - this.height = height || 600; + this.screen = new Rectangle(0, 0, screenWidth || 800, screenHeight || 600); /** * The canvas element that everything is drawn to @@ -107,7 +100,7 @@ this.transparent = options.transparent; /** - * Whether the render view should be resized automatically + * Whether css dimensions of canvas view should be resized to screen dimensions automatically * * @member {boolean} */ @@ -192,23 +185,48 @@ } /** - * Resizes the canvas view to the specified width and height + * Same as view.width, actual number of pixels in the canvas by horizontal * - * @param {number} width - the new width of the canvas view - * @param {number} height - the new height of the canvas view + * @member {number} + * @readonly + * @default 800 */ - resize(width, height) + get width() { - this.width = width * this.resolution; - this.height = height * this.resolution; + return this.view.width; + } - this.view.width = this.width; - this.view.height = this.height; + /** + * Same as view.height, actual number of pixels in the canvas by vertical + * + * @member {number} + * @readonly + * @default 600 + */ + get height() + { + return this.view.height; + } + + /** + * Resizes the screen and canvas to the specified width and height + * Canvas dimensions are multiplied by resolution + * + * @param {number} screenWidth - the new width of the screen + * @param {number} screenHeight - the new height of the screen + */ + resize(screenWidth, screenHeight) + { + this.screen.width = screenWidth; + this.screen.height = screenHeight; + + this.view.width = screenWidth * this.resolution; + this.view.height = screenHeight * this.resolution; if (this.autoResize) { - this.view.style.width = `${this.width / this.resolution}px`; - this.view.style.height = `${this.height / this.resolution}px`; + this.view.style.width = `${screenWidth}px`; + this.view.style.height = `${screenHeight}px`; } } @@ -249,11 +267,10 @@ this.type = RENDERER_TYPE.UNKNOWN; - this.width = 0; - this.height = 0; - this.view = null; + this.screen = null; + this.resolution = 0; this.transparent = false; diff --git a/src/core/renderers/canvas/CanvasRenderer.js b/src/core/renderers/canvas/CanvasRenderer.js index 1bd2d5e..e84e49c 100644 --- a/src/core/renderers/canvas/CanvasRenderer.js +++ b/src/core/renderers/canvas/CanvasRenderer.js @@ -18,8 +18,8 @@ export default class CanvasRenderer extends SystemRenderer { /** - * @param {number} [width=800] - the width of the canvas view - * @param {number} [height=600] - the height of the canvas view + * @param {number} [screenWidth=800] - the width of the screen + * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false @@ -34,9 +34,9 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(width, height, options = {}) + constructor(screenWidth, screenHeight, options = {}) { - super('Canvas', width, height, options); + super('Canvas', screenWidth, screenHeight, options); this.type = RENDERER_TYPE.CANVAS; @@ -96,7 +96,7 @@ this.context = null; this.renderingToScreen = false; - this.resize(width, height); + this.resize(screenWidth, screenHeight); } /** @@ -283,12 +283,12 @@ * * @extends PIXI.SystemRenderer#resize * - * @param {number} width - The new width of the canvas view - * @param {number} height - The new height of the canvas view + * @param {number} screenWidth - the new width of the screen + * @param {number} screenHeight - the new height of the screen */ - resize(width, height) + resize(screenWidth, screenHeight) { - super.resize(width, height); + super.resize(screenWidth, screenHeight); // reset the scale mode.. oddly this seems to be reset when the canvas is resized. // surely a browser bug?? Let pixi fix that for you.. diff --git a/src/core/renderers/webgl/TextureManager.js b/src/core/renderers/webgl/TextureManager.js index b685c1a..8227289 100644 --- a/src/core/renderers/webgl/TextureManager.js +++ b/src/core/renderers/webgl/TextureManager.js @@ -61,7 +61,7 @@ * Updates and/or Creates a WebGL texture for the renderer's context. * * @param {PIXI.BaseTexture|PIXI.Texture} texture - the texture to update - * @param {Number} location - the location the texture will be bound to. + * @param {number} location - the location the texture will be bound to. * @return {GLTexture} The gl texture. */ updateTexture(texture, location) diff --git a/src/core/renderers/webgl/WebGLRenderer.js b/src/core/renderers/webgl/WebGLRenderer.js index e4cd8a0..830b382 100644 --- a/src/core/renderers/webgl/WebGLRenderer.js +++ b/src/core/renderers/webgl/WebGLRenderer.js @@ -31,8 +31,8 @@ { /** * - * @param {number} [width=0] - the width of the canvas view - * @param {number} [height=0] - the height of the canvas view + * @param {number} [screenWidth=800] - the width of the screen + * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false @@ -53,9 +53,9 @@ * @param {boolean} [options.legacy=false] - If true Pixi will aim to ensure compatibility * with older / less advanced devices. If you experiance unexplained flickering try setting this to true. */ - constructor(width, height, options = {}) + constructor(screenWidth, screenHeight, options = {}) { - super('WebGL', width, height, options); + super('WebGL', screenWidth, screenHeight, options); this.legacy = !!options.legacy; @@ -245,7 +245,7 @@ glCore._testingContext = gl; // setup the width/height properties and gl viewport - this.resize(this.width, this.height); + this.resize(this.screen.width, this.screen.height); } /** @@ -343,16 +343,16 @@ /** * Resizes the webGL view to the specified width and height. * - * @param {number} width - the new width of the webGL view - * @param {number} height - the new height of the webGL view + * @param {number} screenWidth - the new width of the screen + * @param {number} screenHeight - the new height of the screen */ - resize(width, height) + resize(screenWidth, screenHeight) { // if(width * this.resolution === this.width && height * this.resolution === this.height)return; - SystemRenderer.prototype.resize.call(this, width, height); + SystemRenderer.prototype.resize.call(this, screenWidth, screenHeight); - this.rootRenderTarget.resize(width, height); + this.rootRenderTarget.resize(screenWidth, screenHeight); if (this._activeRenderTarget === this.rootRenderTarget) { @@ -396,6 +396,26 @@ } /** + * Erases the render texture and fills the drawing area with a colour + * + * @param {PIXI.RenderTexture} renderTexture - The render texture to clear + * @param {number} [clearColor] - The colour + * @return {PIXI.WebGLRenderer} Returns itself. + */ + clearRenderTexture(renderTexture, clearColor) + { + const baseTexture = renderTexture.baseTexture; + const renderTarget = baseTexture._glRenderTargets[this.CONTEXT_UID]; + + if (renderTarget) + { + renderTarget.clear(clearColor); + } + + return this; + } + + /** * Binds a render texture for rendering * * @param {PIXI.RenderTexture} renderTexture - The render texture to render @@ -474,9 +494,10 @@ * Changes the current GLShader to the one given in parameter * * @param {PIXI.glCore.Shader} shader - the new glShader + * @param {boolean} [autoProject=true] - Whether automatically set the projection matrix * @return {PIXI.WebGLRenderer} Returns itself. */ - _bindGLShader(shader) + _bindGLShader(shader, autoProject) { // TODO cache if (this._activeShader !== shader) @@ -484,8 +505,14 @@ this._activeShader = shader; shader.bind(); - // automatically set the projection matrix - shader.uniforms.projectionMatrix = this._activeRenderTarget.projectionMatrix.toArray(true); + // `autoProject` normally would be a default parameter set to true + // but because of how Babel transpiles default parameters + // it hinders the performance of this method. + if (autoProject !== false) + { + // automatically set the projection matrix + shader.uniforms.projectionMatrix = this._activeRenderTarget.projectionMatrix.toArray(true); + } } return this; diff --git a/src/core/renderers/webgl/WebGLState.js b/src/core/renderers/webgl/WebGLState.js index ba6ecc5..6c846d5 100755 --- a/src/core/renderers/webgl/WebGLState.js +++ b/src/core/renderers/webgl/WebGLState.js @@ -82,18 +82,20 @@ push() { // next state.. - let state = this.stack[++this.stackIndex]; + let state = this.stack[this.stackIndex]; if (!state) { state = this.stack[this.stackIndex] = new Uint8Array(16); } + ++this.stackIndex; + // copy state.. // set active state so we can force overrides of gl state for (let i = 0; i < this.activeState.length; i++) { - this.activeState[i] = state[i]; + state[i] = this.activeState[i]; } } diff --git a/src/core/renderers/webgl/filters/Filter.js b/src/core/renderers/webgl/filters/Filter.js index 2c3eaad..4eb6128 100644 --- a/src/core/renderers/webgl/filters/Filter.js +++ b/src/core/renderers/webgl/filters/Filter.js @@ -9,7 +9,7 @@ * @memberof PIXI * @extends PIXI.Shader */ -class Filter extends Shader +export default class Filter extends Shader { /** * @param {string} [vertexSrc] - The source of the vertex shader. @@ -56,15 +56,16 @@ * @param {PIXI.RenderTarget} input - The input render target. * @param {PIXI.RenderTarget} output - The target to output to. * @param {boolean} clear - Should the output be cleared before rendering to it + * @param {object} [currentState] - It's current state of filter. + * There are some useful properties in the currentState : + * target, filters, sourceFrame, destinationFrame, renderTarget, resolution */ - apply(filterManager, input, output, clear) + apply(filterManager, input, output, clear, currentState) // eslint-disable-line no-unused-vars { // do as you please! - filterManager.applyFilter(this, input, output, clear); + filterManager.applyFilter(this, input, output, clear, currentState); // or just do a regular render.. } } - -export default Filter; diff --git a/src/core/renderers/webgl/filters/extractUniformsFromSrc.js b/src/core/renderers/webgl/filters/extractUniformsFromSrc.js index d78a0b1..4893214 100644 --- a/src/core/renderers/webgl/filters/extractUniformsFromSrc.js +++ b/src/core/renderers/webgl/filters/extractUniformsFromSrc.js @@ -12,7 +12,7 @@ function extractUniformsFromString(string) { - const maskRegex = new RegExp('^(projectionMatrix|uSampler|filterArea)$'); + const maskRegex = new RegExp('^(projectionMatrix|uSampler|filterArea|filterClamp)$'); const uniforms = {}; let nameSplit; diff --git a/src/core/renderers/webgl/managers/FilterManager.js b/src/core/renderers/webgl/managers/FilterManager.js index 43fc869..6c7422d 100644 --- a/src/core/renderers/webgl/managers/FilterManager.js +++ b/src/core/renderers/webgl/managers/FilterManager.js @@ -2,6 +2,7 @@ import RenderTarget from '../utils/RenderTarget'; import Quad from '../utils/Quad'; import { Rectangle } from '../../../math'; +import Shader from '../../../Shader'; import * as filterTransforms from '../filters/filterTransforms'; import bitTwiddle from 'bit-twiddle'; @@ -22,8 +23,6 @@ this.filters = []; this.target = null; this.resolution = 1; - - this.firstRun = true; } } @@ -155,7 +154,7 @@ if (filters.length === 1) { - filters[0].apply(this, currentState.renderTarget, lastState.renderTarget, false); + filters[0].apply(this, currentState.renderTarget, lastState.renderTarget, false, currentState); this.freePotRenderTarget(currentState.renderTarget); } else @@ -177,7 +176,7 @@ for (i = 0; i < filters.length - 1; ++i) { - filters[i].apply(this, flip, flop, true); + filters[i].apply(this, flip, flop, true, currentState); const t = flip; @@ -185,7 +184,7 @@ flop = t; } - filters[i].apply(this, flip, lastState.renderTarget, false); + filters[i].apply(this, flip, lastState.renderTarget, false, currentState); this.freePotRenderTarget(flip); this.freePotRenderTarget(flop); @@ -212,16 +211,28 @@ const renderer = this.renderer; const gl = renderer.gl; - renderer.bindShader(filter, true); + let shader = filter.glShaders[renderer.CONTEXT_UID]; - // TODO theres a better way! - // quad can be a mesh and we then should be able to pull this off without accessing the shader directly - const shader = renderer.shaderManager.getGLShader(); - - // TODO change quad to mesh.. - if (!this.firstRun) + // cacheing.. + if (!shader) { - this.firstRun = true; + if (filter.glShaderKey) + { + shader = this.shaderCache[filter.glShaderKey]; + + if (!shader) + { + shader = new Shader(this.gl, filter.vertexSrc, filter.fragmentSrc); + + filter.glShaders[renderer.CONTEXT_UID] = this.shaderCache[filter.glShaderKey] = shader; + } + } + else + { + shader = filter.glShaders[renderer.CONTEXT_UID] = new Shader(this.gl, filter.vertexSrc, filter.fragmentSrc); + } + + // TODO - this only needs to be done once? renderer.bindVao(null); this.quad.initVao(shader); @@ -244,58 +255,146 @@ renderer.maskManager.pushScissorMask(null, renderer.maskManager.scissorData); } - // renderer._bindGLShader(shader); + renderer.bindShader(shader); - let currentState; - const uniforms = filter.uniforms; + // free unit 0 for us, doesn't matter what was there + // don't try to restore it, because syncUniforms can upload it to another slot + // and it'll be a problem + const tex = this.renderer.emptyTextures[0]; - if (uniforms.filterArea) - { - currentState = this.filterData.stack[this.filterData.index]; - - const filterArea = uniforms.filterArea; - - filterArea[0] = currentState.renderTarget.size.width; - filterArea[1] = currentState.renderTarget.size.height; - filterArea[2] = currentState.sourceFrame.x; - filterArea[3] = currentState.sourceFrame.y; - - filter.uniforms.filterArea = filterArea; - } - - // use this to clamp displaced texture coords so they belong to filterArea - // see displacementFilter fragment shader for an example - if (uniforms.filterClamp) - { - currentState = this.filterData.stack[this.filterData.index]; - - const filterClamp = uniforms.filterClamp; - - filterClamp[0] = 0; - filterClamp[1] = 0; - filterClamp[2] = (currentState.sourceFrame.width - 1) / currentState.renderTarget.size.width; - filterClamp[3] = (currentState.sourceFrame.height - 1) / currentState.renderTarget.size.height; - - filter.uniforms.filterClamp = filterClamp; - } - - renderer.shaderManager.setUniforms(filter.uniforms); + this.renderer.boundTextures[0] = tex; + // this syncs the pixi filters uniforms with glsl uniforms + this.syncUniforms(shader, filter); renderer.state.setBlendMode(filter.blendMode); - // temporary bypass cache.. - const tex = this.renderer.boundTextures[0]; - gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, input.texture.texture); this.quad.vao.draw(this.renderer.gl.TRIANGLES, 6, 0); - // restore cache. gl.bindTexture(gl.TEXTURE_2D, tex._glTextures[this.renderer.CONTEXT_UID].texture); } /** + * Uploads the uniforms of the filter. + * + * @param {GLShader} shader - The underlying gl shader. + * @param {PIXI.Filter} filter - The filter we are synchronizing. + */ + syncUniforms(shader, filter) + { + const uniformData = filter.uniformData; + const uniforms = filter.uniforms; + + // 0 is reserved for the pixi texture so we start at 1! + let textureCount = 1; + let currentState; + + // filterArea and filterClamp that are handled by FilterManager directly + // they must not appear in uniformData + + if (shader.uniforms.filterArea) + { + currentState = this.filterData.stack[this.filterData.index]; + + const filterArea = shader.uniforms.filterArea; + + filterArea[0] = currentState.renderTarget.size.width; + filterArea[1] = currentState.renderTarget.size.height; + filterArea[2] = currentState.sourceFrame.x; + filterArea[3] = currentState.sourceFrame.y; + + shader.uniforms.filterArea = filterArea; + } + + // use this to clamp displaced texture coords so they belong to filterArea + // see displacementFilter fragment shader for an example + if (shader.uniforms.filterClamp) + { + currentState = currentState || this.filterData.stack[this.filterData.index]; + + const filterClamp = shader.uniforms.filterClamp; + + filterClamp[0] = 0; + filterClamp[1] = 0; + filterClamp[2] = (currentState.sourceFrame.width - 1) / currentState.renderTarget.size.width; + filterClamp[3] = (currentState.sourceFrame.height - 1) / currentState.renderTarget.size.height; + + shader.uniforms.filterClamp = filterClamp; + } + + // TODO Cacheing layer.. + for (const i in uniformData) + { + if (uniformData[i].type === 'sampler2D' && uniforms[i] !== 0) + { + if (uniforms[i].baseTexture) + { + shader.uniforms[i] = this.renderer.bindTexture(uniforms[i].baseTexture, textureCount); + } + else + { + shader.uniforms[i] = textureCount; + + // TODO + // this is helpful as renderTargets can also be set. + // Although thinking about it, we could probably + // make the filter texture cache return a RenderTexture + // rather than a renderTarget + const gl = this.renderer.gl; + + this.renderer.boundTextures[textureCount] = this.renderer.emptyTextures[textureCount]; + gl.activeTexture(gl.TEXTURE0 + textureCount); + + uniforms[i].texture.bind(); + } + + textureCount++; + } + else if (uniformData[i].type === 'mat3') + { + // check if its pixi matrix.. + if (uniforms[i].a !== undefined) + { + shader.uniforms[i] = uniforms[i].toArray(true); + } + else + { + shader.uniforms[i] = uniforms[i]; + } + } + else if (uniformData[i].type === 'vec2') + { + // check if its a point.. + if (uniforms[i].x !== undefined) + { + const val = shader.uniforms[i] || new Float32Array(2); + + val[0] = uniforms[i].x; + val[1] = uniforms[i].y; + shader.uniforms[i] = val; + } + else + { + shader.uniforms[i] = uniforms[i]; + } + } + else if (uniformData[i].type === 'float') + { + if (shader.uniforms.data[i].value !== uniformData[i]) + { + shader.uniforms[i] = uniforms[i]; + } + } + else + { + shader.uniforms[i] = uniforms[i]; + } + } + } + + /** * Gets a render target from the pool, or creates a new one. * * @param {boolean} clear - Should we clear the render texture when we get it? @@ -442,7 +541,6 @@ renderTarget.resolution = resolution; renderTarget.defaultFrame.width = renderTarget.size.width = minWidth / resolution; renderTarget.defaultFrame.height = renderTarget.size.height = minHeight / resolution; - renderTarget.attachStencilBuffer(); return renderTarget; } diff --git a/src/core/settings.js b/src/core/settings.js index bb08cb0..f4dd087 100644 --- a/src/core/settings.js +++ b/src/core/settings.js @@ -77,11 +77,11 @@ * * @static * @memberof PIXI.settings - * @type {RegExp|string} + * @type {RegExp} * @example `@2x` - * @default /@(.+)x/ + * @default /@([0-9\.]+)x/ */ - RETINA_PREFIX: /@(.+)x/, + RETINA_PREFIX: /@([0-9\.]+)x/, /** * The default render options if none are supplied to {@link PIXI.WebGLRenderer} @@ -175,14 +175,24 @@ SCALE_MODE: 0, /** - * Default specify float precision in shaders. + * Default specify float precision in vertex shader. + * + * @static + * @memberof PIXI.settings + * @type {PIXI.PRECISION} + * @default PIXI.PRECISION.HIGH + */ + PRECISION_VERTEX: 'highp', + + /** + * Default specify float precision in fragment shader. * * @static * @memberof PIXI.settings * @type {PIXI.PRECISION} * @default PIXI.PRECISION.MEDIUM */ - PRECISION: 'mediump', + PRECISION_FRAGMENT: 'mediump', /** * Can we upload the same buffer in a single frame? diff --git a/src/core/sprites/Sprite.js b/src/core/sprites/Sprite.js index 28b0b18..121bf58 100644 --- a/src/core/sprites/Sprite.js +++ b/src/core/sprites/Sprite.js @@ -212,11 +212,11 @@ } else { - w0 = orig.width * (1 - anchor._x); - w1 = orig.width * -anchor._x; + w1 = -anchor._x * orig.width; + w0 = w1 + orig.width; - h0 = orig.height * (1 - anchor._y); - h1 = orig.height * -anchor._y; + h1 = -anchor._y * orig.height; + h0 = h1 + orig.height; } // xy @@ -269,11 +269,11 @@ const tx = wt.tx; const ty = wt.ty; - const w0 = (orig.width) * (1 - anchor._x); - const w1 = (orig.width) * -anchor._x; + const w1 = -anchor._x * orig.width; + const w0 = w1 + orig.width; - const h0 = orig.height * (1 - anchor._y); - const h1 = orig.height * -anchor._y; + const h1 = -anchor._y * orig.height; + const h0 = h1 + orig.height; // xy vertexData[0] = (a * w1) + (c * h1) + tx; @@ -440,7 +440,7 @@ * * @static * @param {number|string|PIXI.BaseTexture|HTMLCanvasElement|HTMLVideoElement} source Source to create texture from - * @return {PIXI.Sprite} The newly created texture + * @return {PIXI.Sprite} The newly created sprite */ static from(source) { @@ -538,8 +538,8 @@ } /** - * The tint applied to the sprite. This is a hex value. A value of - * 0xFFFFFF will remove any tint effect. + * The tint applied to the sprite. This is a hex value. + * A value of 0xFFFFFF will remove any tint effect. * * @member {number} * @default 0xFFFFFF diff --git a/src/core/sprites/canvas/CanvasTinter.js b/src/core/sprites/canvas/CanvasTinter.js index 1cf316b..27e5e0c 100644 --- a/src/core/sprites/canvas/CanvasTinter.js +++ b/src/core/sprites/canvas/CanvasTinter.js @@ -75,8 +75,8 @@ crop.width *= resolution; crop.height *= resolution; - canvas.width = crop.width; - canvas.height = crop.height; + canvas.width = Math.ceil(crop.width); + canvas.height = Math.ceil(crop.height); context.fillStyle = `#${(`00000${(color | 0).toString(16)}`).substr(-6)}`; @@ -130,8 +130,8 @@ crop.width *= resolution; crop.height *= resolution; - canvas.width = crop.width; - canvas.height = crop.height; + canvas.width = Math.ceil(crop.width); + canvas.height = Math.ceil(crop.height); context.globalCompositeOperation = 'copy'; context.fillStyle = `#${(`00000${(color | 0).toString(16)}`).substr(-6)}`; @@ -172,8 +172,8 @@ crop.width *= resolution; crop.height *= resolution; - canvas.width = crop.width; - canvas.height = crop.height; + canvas.width = Math.ceil(crop.width); + canvas.height = Math.ceil(crop.height); context.globalCompositeOperation = 'copy'; context.drawImage( diff --git a/src/core/sprites/webgl/SpriteRenderer.js b/src/core/sprites/webgl/SpriteRenderer.js index 2affcd5..7c352e2 100644 --- a/src/core/sprites/webgl/SpriteRenderer.js +++ b/src/core/sprites/webgl/SpriteRenderer.js @@ -30,7 +30,7 @@ /** * Number of values sent in the vertex buffer. - * positionX, positionY, colorR, colorG, colorB = 5 + * aVertexPosition(2), aTextureCoord(1), aColor(1), aTextureId(1) = 5 * * @member {number} */ @@ -75,7 +75,6 @@ this.shader = null; this.currentIndex = 0; - TICK = 0; this.groups = []; for (let k = 0; k < this.size; k++) diff --git a/package.json b/package.json index 6a366ba..71ade15 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pixi.js", - "version": "4.3.4", + "version": "4.3.5", "description": "Pixi.js is a fast lightweight 2D library that works across all devices.", "author": "Mat Groves", "contributors": [ @@ -61,7 +61,7 @@ "ismobilejs": "^0.4.0", "object-assign": "^4.0.1", "pixi-gl-core": "^1.0.3", - "resource-loader": "^2.0.4" + "resource-loader": "^2.0.6" }, "devDependencies": { "babel-cli": "^6.18.0", @@ -70,9 +70,9 @@ "babel-preset-es2015": "^6.14.0", "babelify": "^7.3.0", "del": "^2.2.0", - "electron-prebuilt": "^1.3.2", + "electron": "^1.4.15", "eslint": "^3.5.0", - "floss": "^1.2.0", + "floss": "^2.0.1", "gh-pages": "^0.11.0", "jaguarjs-jsdoc": "^1.0.1", "js-md5": "^0.4.1", diff --git a/src/accessibility/AccessibilityManager.js b/src/accessibility/AccessibilityManager.js index df2716a..f16bcb1 100644 --- a/src/accessibility/AccessibilityManager.js +++ b/src/accessibility/AccessibilityManager.js @@ -3,7 +3,7 @@ import accessibleTarget from './accessibleTarget'; // add some extra variables to the container.. -Object.assign( +core.utils.mixins.delayMixin( core.DisplayObject.prototype, accessibleTarget ); diff --git a/src/core/Application.js b/src/core/Application.js index ea0f058..5340578 100644 --- a/src/core/Application.js +++ b/src/core/Application.js @@ -1,6 +1,6 @@ import { autoDetectRenderer } from './autoDetectRenderer'; import Container from './display/Container'; -import Ticker from './ticker/Ticker'; +import { shared, Ticker } from './ticker'; /** * Convenience class to create a new PIXI application. @@ -35,9 +35,9 @@ * @param {boolean} [noWebGL=false] - prevents selection of WebGL renderer, even if such is present * @param {boolean} [options.legacy=false] - If true Pixi will aim to ensure compatibility * with older / less advanced devices. If you experience unexplained flickering try setting this to true. - + * @param {boolean} [useSharedTicker=false] - `true` to use PIXI.ticker.shared, `false` to create new ticker. */ - constructor(width, height, options, noWebGL) + constructor(width, height, options, noWebGL, useSharedTicker = false) { /** * WebGL renderer if available, otherwise CanvasRenderer @@ -46,23 +46,46 @@ this.renderer = autoDetectRenderer(width, height, options, noWebGL); /** - * The root display container that's renderered. + * The root display container that's rendered. * @member {PIXI.Container} */ this.stage = new Container(); /** + * Internal reference to the ticker + * @member {PIXI.ticker.Ticker} + * @private + */ + this._ticker = null; + + /** * Ticker for doing render updates. * @member {PIXI.ticker.Ticker} + * @default PIXI.ticker.shared */ - this.ticker = new Ticker(); - - this.ticker.add(this.render, this); + this.ticker = useSharedTicker ? shared : new Ticker(); // Start the rendering this.start(); } + set ticker(ticker) // eslint-disable-line require-jsdoc + { + if (this._ticker) + { + this._ticker.remove(this.render, this); + } + this._ticker = ticker; + if (ticker) + { + ticker.add(this.render, this); + } + } + get ticker() // eslint-disable-line require-jsdoc + { + return this._ticker; + } + /** * Render the current stage. */ @@ -76,7 +99,7 @@ */ stop() { - this.ticker.stop(); + this._ticker.stop(); } /** @@ -84,7 +107,7 @@ */ start() { - this.ticker.start(); + this._ticker.start(); } /** @@ -98,13 +121,22 @@ } /** + * Reference to the renderer's screen rectangle. Its safe to use as filterArea or hitArea for whole screen + * @member {PIXI.Rectangle} + * @readonly + */ + get screen() + { + return this.renderer.screen; + } + + /** * Destroy and don't use after this. * @param {Boolean} [removeView=false] Automatically remove canvas from DOM. */ destroy(removeView) { this.stop(); - this.ticker.remove(this.render, this); this.ticker = null; this.stage.destroy(); diff --git a/src/core/Shader.js b/src/core/Shader.js new file mode 100644 index 0000000..78b17e6 --- /dev/null +++ b/src/core/Shader.js @@ -0,0 +1,46 @@ +import { GLShader } from 'pixi-gl-core'; +import settings from './settings'; + +function checkPrecision(src, def) +{ + if (src instanceof Array) + { + if (src[0].substring(0, 9) !== 'precision') + { + const copy = src.slice(0); + + copy.unshift(`precision ${def} float;`); + + return copy; + } + } + else if (src.substring(0, 9) !== 'precision') + { + return `precision ${def} float;\n${src}`; + } + + return src; +} + +/** + * Wrapper class, webGL Shader for Pixi. + * Adds precision string if vertexSrc or fragmentSrc have no mention of it. + * + * @class + * @extends GLShader + * @memberof PIXI + */ +export default class Shader extends GLShader +{ + /** + * + * @param {WebGLRenderingContext} gl - The current WebGL rendering context + * @param {string|string[]} vertexSrc - The vertex shader source as an array of strings. + * @param {string|string[]} fragmentSrc - The fragment shader source as an array of strings. + */ + constructor(gl, vertexSrc, fragmentSrc) + { + super(gl, checkPrecision(vertexSrc, settings.PRECISION_VERTEX), + checkPrecision(fragmentSrc, settings.PRECISION_FRAGMENT)); + } +} diff --git a/src/core/const.js b/src/core/const.js index ef6f662..3de0efe 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -188,7 +188,7 @@ * The gc modes that are supported by pixi. * * The {@link PIXI.settings.GC_MODE} Garbage Collection mode for pixi textures is AUTO - * If set to GC_MODE, the renderer will occasianally check textures usage. If they are not + * If set to GC_MODE, the renderer will occasionally check textures usage. If they are not * used for a specified period of time they will be removed from the GPU. They will of course * be uploaded again when they are required. This is a silent behind the scenes process that * should ensure that the GPU does not get filled up. diff --git a/src/core/display/Container.js b/src/core/display/Container.js index ed8cd5f..17fde8d 100644 --- a/src/core/display/Container.js +++ b/src/core/display/Container.js @@ -73,11 +73,12 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.push(child); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -108,11 +109,12 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.splice(index, 0, child); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -225,10 +227,11 @@ if (index === -1) return null; child.parent = null; + // ensure child transform will be recalculated + child.transform._parentID = -1; removeItems(this.children, index, 1); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -249,11 +252,12 @@ { const child = this.getChildAt(index); + // ensure child transform will be recalculated.. child.parent = null; + child.transform._parentID = -1; removeItems(this.children, index, 1); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -284,13 +288,12 @@ for (let i = 0; i < removed.length; ++i) { removed[i].parent = null; + if (removed[i].transform) + { + removed[i].transform._parentID = -1; + } } - // ensure a transform will be recalculated.. - if (this.transform) - { - this.transform._parentID = -1; - } this._boundsID++; this.onChildrenChange(beginIndex); diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index f37f2f5..be4c4fb 100644 --- a/src/core/graphics/Graphics.js +++ b/src/core/graphics/Graphics.js @@ -28,8 +28,9 @@ { /** * + * @param {boolean} [nativeLines=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP */ - constructor() + constructor(nativeLines = false) { super(); @@ -50,6 +51,13 @@ this.lineWidth = 0; /** + * If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * + * @member {boolean} + */ + this.nativeLines = nativeLines; + + /** * The color of any lines drawn. * * @member {string} @@ -757,28 +765,14 @@ if (!this._spriteRect) { - if (!Graphics._SPRITE_TEXTURE) - { - Graphics._SPRITE_TEXTURE = RenderTexture.create(10, 10); - - const canvas = document.createElement('canvas'); - - canvas.width = 10; - canvas.height = 10; - - const context = canvas.getContext('2d'); - - context.fillStyle = 'white'; - context.fillRect(0, 0, 10, 10); - - Graphics._SPRITE_TEXTURE = Texture.fromCanvas(canvas); - } - - this._spriteRect = new Sprite(Graphics._SPRITE_TEXTURE); + this._spriteRect = new Sprite(new Texture(Texture.WHITE)); } + + const sprite = this._spriteRect; + if (this.tint === 0xffffff) { - this._spriteRect.tint = this.graphicsData[0].fillColor; + sprite.tint = this.graphicsData[0].fillColor; } else { @@ -792,20 +786,21 @@ t1[1] *= t2[1]; t1[2] *= t2[2]; - this._spriteRect.tint = rgb2hex(t1); + sprite.tint = rgb2hex(t1); } - this._spriteRect.alpha = this.graphicsData[0].fillAlpha; - this._spriteRect.worldAlpha = this.worldAlpha * this._spriteRect.alpha; + sprite.alpha = this.graphicsData[0].fillAlpha; + sprite.worldAlpha = this.worldAlpha * sprite.alpha; + sprite.blendMode = this.blendMode; - Graphics._SPRITE_TEXTURE._frame.width = rect.width; - Graphics._SPRITE_TEXTURE._frame.height = rect.height; + sprite.texture._frame.width = rect.width; + sprite.texture._frame.height = rect.height; - this._spriteRect.transform.worldTransform = this.transform.worldTransform; + sprite.transform.worldTransform = this.transform.worldTransform; - this._spriteRect.anchor.set(-rect.x / rect.width, -rect.y / rect.height); - this._spriteRect._onAnchorUpdate(); + sprite.anchor.set(-rect.x / rect.width, -rect.y / rect.height); + sprite._onAnchorUpdate(); - this._spriteRect._renderWebGL(renderer); + sprite._renderWebGL(renderer); } /** @@ -1031,6 +1026,7 @@ this.fillColor, this.fillAlpha, this.filling, + this.nativeLines, shape ); diff --git a/src/core/graphics/GraphicsData.js b/src/core/graphics/GraphicsData.js index f5a87ae..b6f03c0 100644 --- a/src/core/graphics/GraphicsData.js +++ b/src/core/graphics/GraphicsData.js @@ -14,14 +14,19 @@ * @param {number} fillColor - the color of the fill * @param {number} fillAlpha - the alpha of the fill * @param {boolean} fill - whether or not the shape is filled with a colour + * @param {boolean} nativeLines - the method for drawing lines * @param {PIXI.Circle|PIXI.Rectangle|PIXI.Ellipse|PIXI.Polygon} shape - The shape object to draw. */ - constructor(lineWidth, lineColor, lineAlpha, fillColor, fillAlpha, fill, shape) + constructor(lineWidth, lineColor, lineAlpha, fillColor, fillAlpha, fill, nativeLines, shape) { /** * @member {number} the width of the line to draw */ this.lineWidth = lineWidth; + /** + * @member {boolean} if true the liens will be draw using LINES instead of TRIANGLE_STRIP + */ + this.nativeLines = nativeLines; /** * @member {number} the color of the line to draw @@ -85,6 +90,7 @@ this.fillColor, this.fillAlpha, this.fill, + this.nativeLines, this.shape ); } diff --git a/src/core/graphics/webgl/GraphicsRenderer.js b/src/core/graphics/webgl/GraphicsRenderer.js index 14083fe..67f5e92 100644 --- a/src/core/graphics/webgl/GraphicsRenderer.js +++ b/src/core/graphics/webgl/GraphicsRenderer.js @@ -102,7 +102,15 @@ shaderTemp.uniforms.alpha = graphics.worldAlpha; renderer.bindVao(webGLData.vao); - webGLData.vao.draw(gl.TRIANGLE_STRIP, webGLData.indices.length); + + if (graphics.nativeLines) + { + gl.drawArrays(gl.LINES, 0, webGLData.points.length / 6); + } + else + { + webGLData.vao.draw(gl.TRIANGLE_STRIP, webGLData.indices.length); + } } } diff --git a/src/core/graphics/webgl/utils/buildLine.js b/src/core/graphics/webgl/utils/buildLine.js index ecf43f7..4d6862e 100644 --- a/src/core/graphics/webgl/utils/buildLine.js +++ b/src/core/graphics/webgl/utils/buildLine.js @@ -11,7 +11,29 @@ * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties * @param {object} webGLData - an object containing all the webGL-specific information to create this shape */ -export default function buildLine(graphicsData, webGLData) +export default function (graphicsData, webGLData) +{ + if (graphicsData.nativeLines) + { + buildNativeLine(graphicsData, webGLData); + } + else + { + buildLine(graphicsData, webGLData); + } +} + +/** + * Builds a line to draw using the poligon method. + * + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @private + * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties + * @param {object} webGLData - an object containing all the webGL-specific information to create this shape + */ +function buildLine(graphicsData, webGLData) { // TODO OPTIMISE! let points = graphicsData.points; @@ -224,3 +246,46 @@ indices.push(indexStart - 1); } + +/** + * Builds a line to draw using the gl.drawArrays(gl.LINES) method + * + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @private + * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties + * @param {object} webGLData - an object containing all the webGL-specific information to create this shape + */ +function buildNativeLine(graphicsData, webGLData) +{ + let i = 0; + const points = graphicsData.points; + + if (points.length === 0) return; + + const verts = webGLData.points; + const length = points.length / 2; + + // sort color + const color = hex2rgb(graphicsData.lineColor); + const alpha = graphicsData.lineAlpha; + const r = color[0] * alpha; + const g = color[1] * alpha; + const b = color[2] * alpha; + + for (i = 1; i < length; i++) + { + const p1x = points[(i - 1) * 2]; + const p1y = points[((i - 1) * 2) + 1]; + + const p2x = points[i * 2]; + const p2y = points[(i * 2) + 1]; + + verts.push(p1x, p1y); + verts.push(r, g, b, alpha); + + verts.push(p2x, p2y); + verts.push(r, g, b, alpha); + } +} diff --git a/src/core/graphics/webgl/utils/buildRoundedRectangle.js b/src/core/graphics/webgl/utils/buildRoundedRectangle.js index 9abc807..a6c8461 100644 --- a/src/core/graphics/webgl/utils/buildRoundedRectangle.js +++ b/src/core/graphics/webgl/utils/buildRoundedRectangle.js @@ -77,6 +77,26 @@ } /** + * Calculate a single point for a quadratic bezier curve. + * Utility function used by quadraticBezierCurve. + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @private + * @param {number} n1 - first number + * @param {number} n2 - second number + * @param {number} perc - percentage + * @return {number} the result + * + */ +function getPt(n1, n2, perc) +{ + const diff = n2 - n1; + + return n1 + (diff * perc); +} + +/** * Calculate the points for a quadratic bezier curve. (helper function..) * Based on: https://stackoverflow.com/questions/785097/how-do-i-implement-a-bezier-curve-in-c * @@ -105,13 +125,6 @@ let x = 0; let y = 0; - function getPt(n1, n2, perc) - { - const diff = n2 - n1; - - return n1 + (diff * perc); - } - for (let i = 0, j = 0; i <= n; ++i) { j = i / n; diff --git a/src/core/index.js b/src/core/index.js index f7a817c..4f414e8 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -30,6 +30,7 @@ export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; export { default as CanvasGraphicsRenderer } from './graphics/canvas/CanvasGraphicsRenderer'; +export { default as Spritesheet } from './textures/Spritesheet'; export { default as Texture } from './textures/Texture'; export { default as BaseTexture } from './textures/BaseTexture'; export { default as RenderTexture } from './textures/RenderTexture'; diff --git a/src/core/math/Matrix.js b/src/core/math/Matrix.js index 0b77477..3679860 100644 --- a/src/core/math/Matrix.js +++ b/src/core/math/Matrix.js @@ -13,45 +13,50 @@ export default class Matrix { /** - * + * @param {number} [a=1] - x scale + * @param {number} [b=0] - y skew + * @param {number} [c=0] - x skew + * @param {number} [d=1] - y scale + * @param {number} [tx=0] - x translation + * @param {number} [ty=0] - y translation */ - constructor() + constructor(a = 1, b = 0, c = 0, d = 1, tx = 0, ty = 0) { /** * @member {number} * @default 1 */ - this.a = 1; + this.a = a; /** * @member {number} * @default 0 */ - this.b = 0; + this.b = b; /** * @member {number} * @default 0 */ - this.c = 0; + this.c = c; /** * @member {number} * @default 1 */ - this.d = 1; + this.d = d; /** * @member {number} * @default 0 */ - this.tx = 0; + this.tx = tx; /** * @member {number} * @default 0 */ - this.ty = 0; + this.ty = ty; this.array = null; } diff --git a/src/core/renderers/SystemRenderer.js b/src/core/renderers/SystemRenderer.js index fa116a6..a24c775 100644 --- a/src/core/renderers/SystemRenderer.js +++ b/src/core/renderers/SystemRenderer.js @@ -1,5 +1,5 @@ import { sayHello, hex2string, hex2rgb } from '../utils'; -import { Matrix } from '../math'; +import { Matrix, Rectangle } from '../math'; import { RENDERER_TYPE } from '../const'; import settings from '../settings'; import Container from '../display/Container'; @@ -21,8 +21,8 @@ { /** * @param {string} system - The name of the system this renderer is for. - * @param {number} [width=800] - the width of the canvas view - * @param {number} [height=600] - the height of the canvas view + * @param {number} [screenWidth=800] - the width of the screen + * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false @@ -37,7 +37,7 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(system, width, height, options) + constructor(system, screenWidth, screenHeight, options) { super(); @@ -69,20 +69,13 @@ this.type = RENDERER_TYPE.UNKNOWN; /** - * The width of the canvas view + * Measurements of the screen. (0, 0, screenWidth, screenHeight) * - * @member {number} - * @default 800 - */ - this.width = width || 800; - - /** - * The height of the canvas view + * Its safe to use as filterArea or hitArea for whole stage * - * @member {number} - * @default 600 + * @member {PIXI.Rectangle} */ - this.height = height || 600; + this.screen = new Rectangle(0, 0, screenWidth || 800, screenHeight || 600); /** * The canvas element that everything is drawn to @@ -107,7 +100,7 @@ this.transparent = options.transparent; /** - * Whether the render view should be resized automatically + * Whether css dimensions of canvas view should be resized to screen dimensions automatically * * @member {boolean} */ @@ -192,23 +185,48 @@ } /** - * Resizes the canvas view to the specified width and height + * Same as view.width, actual number of pixels in the canvas by horizontal * - * @param {number} width - the new width of the canvas view - * @param {number} height - the new height of the canvas view + * @member {number} + * @readonly + * @default 800 */ - resize(width, height) + get width() { - this.width = width * this.resolution; - this.height = height * this.resolution; + return this.view.width; + } - this.view.width = this.width; - this.view.height = this.height; + /** + * Same as view.height, actual number of pixels in the canvas by vertical + * + * @member {number} + * @readonly + * @default 600 + */ + get height() + { + return this.view.height; + } + + /** + * Resizes the screen and canvas to the specified width and height + * Canvas dimensions are multiplied by resolution + * + * @param {number} screenWidth - the new width of the screen + * @param {number} screenHeight - the new height of the screen + */ + resize(screenWidth, screenHeight) + { + this.screen.width = screenWidth; + this.screen.height = screenHeight; + + this.view.width = screenWidth * this.resolution; + this.view.height = screenHeight * this.resolution; if (this.autoResize) { - this.view.style.width = `${this.width / this.resolution}px`; - this.view.style.height = `${this.height / this.resolution}px`; + this.view.style.width = `${screenWidth}px`; + this.view.style.height = `${screenHeight}px`; } } @@ -249,11 +267,10 @@ this.type = RENDERER_TYPE.UNKNOWN; - this.width = 0; - this.height = 0; - this.view = null; + this.screen = null; + this.resolution = 0; this.transparent = false; diff --git a/src/core/renderers/canvas/CanvasRenderer.js b/src/core/renderers/canvas/CanvasRenderer.js index 1bd2d5e..e84e49c 100644 --- a/src/core/renderers/canvas/CanvasRenderer.js +++ b/src/core/renderers/canvas/CanvasRenderer.js @@ -18,8 +18,8 @@ export default class CanvasRenderer extends SystemRenderer { /** - * @param {number} [width=800] - the width of the canvas view - * @param {number} [height=600] - the height of the canvas view + * @param {number} [screenWidth=800] - the width of the screen + * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false @@ -34,9 +34,9 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(width, height, options = {}) + constructor(screenWidth, screenHeight, options = {}) { - super('Canvas', width, height, options); + super('Canvas', screenWidth, screenHeight, options); this.type = RENDERER_TYPE.CANVAS; @@ -96,7 +96,7 @@ this.context = null; this.renderingToScreen = false; - this.resize(width, height); + this.resize(screenWidth, screenHeight); } /** @@ -283,12 +283,12 @@ * * @extends PIXI.SystemRenderer#resize * - * @param {number} width - The new width of the canvas view - * @param {number} height - The new height of the canvas view + * @param {number} screenWidth - the new width of the screen + * @param {number} screenHeight - the new height of the screen */ - resize(width, height) + resize(screenWidth, screenHeight) { - super.resize(width, height); + super.resize(screenWidth, screenHeight); // reset the scale mode.. oddly this seems to be reset when the canvas is resized. // surely a browser bug?? Let pixi fix that for you.. diff --git a/src/core/renderers/webgl/TextureManager.js b/src/core/renderers/webgl/TextureManager.js index b685c1a..8227289 100644 --- a/src/core/renderers/webgl/TextureManager.js +++ b/src/core/renderers/webgl/TextureManager.js @@ -61,7 +61,7 @@ * Updates and/or Creates a WebGL texture for the renderer's context. * * @param {PIXI.BaseTexture|PIXI.Texture} texture - the texture to update - * @param {Number} location - the location the texture will be bound to. + * @param {number} location - the location the texture will be bound to. * @return {GLTexture} The gl texture. */ updateTexture(texture, location) diff --git a/src/core/renderers/webgl/WebGLRenderer.js b/src/core/renderers/webgl/WebGLRenderer.js index e4cd8a0..830b382 100644 --- a/src/core/renderers/webgl/WebGLRenderer.js +++ b/src/core/renderers/webgl/WebGLRenderer.js @@ -31,8 +31,8 @@ { /** * - * @param {number} [width=0] - the width of the canvas view - * @param {number} [height=0] - the height of the canvas view + * @param {number} [screenWidth=800] - the width of the screen + * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false @@ -53,9 +53,9 @@ * @param {boolean} [options.legacy=false] - If true Pixi will aim to ensure compatibility * with older / less advanced devices. If you experiance unexplained flickering try setting this to true. */ - constructor(width, height, options = {}) + constructor(screenWidth, screenHeight, options = {}) { - super('WebGL', width, height, options); + super('WebGL', screenWidth, screenHeight, options); this.legacy = !!options.legacy; @@ -245,7 +245,7 @@ glCore._testingContext = gl; // setup the width/height properties and gl viewport - this.resize(this.width, this.height); + this.resize(this.screen.width, this.screen.height); } /** @@ -343,16 +343,16 @@ /** * Resizes the webGL view to the specified width and height. * - * @param {number} width - the new width of the webGL view - * @param {number} height - the new height of the webGL view + * @param {number} screenWidth - the new width of the screen + * @param {number} screenHeight - the new height of the screen */ - resize(width, height) + resize(screenWidth, screenHeight) { // if(width * this.resolution === this.width && height * this.resolution === this.height)return; - SystemRenderer.prototype.resize.call(this, width, height); + SystemRenderer.prototype.resize.call(this, screenWidth, screenHeight); - this.rootRenderTarget.resize(width, height); + this.rootRenderTarget.resize(screenWidth, screenHeight); if (this._activeRenderTarget === this.rootRenderTarget) { @@ -396,6 +396,26 @@ } /** + * Erases the render texture and fills the drawing area with a colour + * + * @param {PIXI.RenderTexture} renderTexture - The render texture to clear + * @param {number} [clearColor] - The colour + * @return {PIXI.WebGLRenderer} Returns itself. + */ + clearRenderTexture(renderTexture, clearColor) + { + const baseTexture = renderTexture.baseTexture; + const renderTarget = baseTexture._glRenderTargets[this.CONTEXT_UID]; + + if (renderTarget) + { + renderTarget.clear(clearColor); + } + + return this; + } + + /** * Binds a render texture for rendering * * @param {PIXI.RenderTexture} renderTexture - The render texture to render @@ -474,9 +494,10 @@ * Changes the current GLShader to the one given in parameter * * @param {PIXI.glCore.Shader} shader - the new glShader + * @param {boolean} [autoProject=true] - Whether automatically set the projection matrix * @return {PIXI.WebGLRenderer} Returns itself. */ - _bindGLShader(shader) + _bindGLShader(shader, autoProject) { // TODO cache if (this._activeShader !== shader) @@ -484,8 +505,14 @@ this._activeShader = shader; shader.bind(); - // automatically set the projection matrix - shader.uniforms.projectionMatrix = this._activeRenderTarget.projectionMatrix.toArray(true); + // `autoProject` normally would be a default parameter set to true + // but because of how Babel transpiles default parameters + // it hinders the performance of this method. + if (autoProject !== false) + { + // automatically set the projection matrix + shader.uniforms.projectionMatrix = this._activeRenderTarget.projectionMatrix.toArray(true); + } } return this; diff --git a/src/core/renderers/webgl/WebGLState.js b/src/core/renderers/webgl/WebGLState.js index ba6ecc5..6c846d5 100755 --- a/src/core/renderers/webgl/WebGLState.js +++ b/src/core/renderers/webgl/WebGLState.js @@ -82,18 +82,20 @@ push() { // next state.. - let state = this.stack[++this.stackIndex]; + let state = this.stack[this.stackIndex]; if (!state) { state = this.stack[this.stackIndex] = new Uint8Array(16); } + ++this.stackIndex; + // copy state.. // set active state so we can force overrides of gl state for (let i = 0; i < this.activeState.length; i++) { - this.activeState[i] = state[i]; + state[i] = this.activeState[i]; } } diff --git a/src/core/renderers/webgl/filters/Filter.js b/src/core/renderers/webgl/filters/Filter.js index 2c3eaad..4eb6128 100644 --- a/src/core/renderers/webgl/filters/Filter.js +++ b/src/core/renderers/webgl/filters/Filter.js @@ -9,7 +9,7 @@ * @memberof PIXI * @extends PIXI.Shader */ -class Filter extends Shader +export default class Filter extends Shader { /** * @param {string} [vertexSrc] - The source of the vertex shader. @@ -56,15 +56,16 @@ * @param {PIXI.RenderTarget} input - The input render target. * @param {PIXI.RenderTarget} output - The target to output to. * @param {boolean} clear - Should the output be cleared before rendering to it + * @param {object} [currentState] - It's current state of filter. + * There are some useful properties in the currentState : + * target, filters, sourceFrame, destinationFrame, renderTarget, resolution */ - apply(filterManager, input, output, clear) + apply(filterManager, input, output, clear, currentState) // eslint-disable-line no-unused-vars { // do as you please! - filterManager.applyFilter(this, input, output, clear); + filterManager.applyFilter(this, input, output, clear, currentState); // or just do a regular render.. } } - -export default Filter; diff --git a/src/core/renderers/webgl/filters/extractUniformsFromSrc.js b/src/core/renderers/webgl/filters/extractUniformsFromSrc.js index d78a0b1..4893214 100644 --- a/src/core/renderers/webgl/filters/extractUniformsFromSrc.js +++ b/src/core/renderers/webgl/filters/extractUniformsFromSrc.js @@ -12,7 +12,7 @@ function extractUniformsFromString(string) { - const maskRegex = new RegExp('^(projectionMatrix|uSampler|filterArea)$'); + const maskRegex = new RegExp('^(projectionMatrix|uSampler|filterArea|filterClamp)$'); const uniforms = {}; let nameSplit; diff --git a/src/core/renderers/webgl/managers/FilterManager.js b/src/core/renderers/webgl/managers/FilterManager.js index 43fc869..6c7422d 100644 --- a/src/core/renderers/webgl/managers/FilterManager.js +++ b/src/core/renderers/webgl/managers/FilterManager.js @@ -2,6 +2,7 @@ import RenderTarget from '../utils/RenderTarget'; import Quad from '../utils/Quad'; import { Rectangle } from '../../../math'; +import Shader from '../../../Shader'; import * as filterTransforms from '../filters/filterTransforms'; import bitTwiddle from 'bit-twiddle'; @@ -22,8 +23,6 @@ this.filters = []; this.target = null; this.resolution = 1; - - this.firstRun = true; } } @@ -155,7 +154,7 @@ if (filters.length === 1) { - filters[0].apply(this, currentState.renderTarget, lastState.renderTarget, false); + filters[0].apply(this, currentState.renderTarget, lastState.renderTarget, false, currentState); this.freePotRenderTarget(currentState.renderTarget); } else @@ -177,7 +176,7 @@ for (i = 0; i < filters.length - 1; ++i) { - filters[i].apply(this, flip, flop, true); + filters[i].apply(this, flip, flop, true, currentState); const t = flip; @@ -185,7 +184,7 @@ flop = t; } - filters[i].apply(this, flip, lastState.renderTarget, false); + filters[i].apply(this, flip, lastState.renderTarget, false, currentState); this.freePotRenderTarget(flip); this.freePotRenderTarget(flop); @@ -212,16 +211,28 @@ const renderer = this.renderer; const gl = renderer.gl; - renderer.bindShader(filter, true); + let shader = filter.glShaders[renderer.CONTEXT_UID]; - // TODO theres a better way! - // quad can be a mesh and we then should be able to pull this off without accessing the shader directly - const shader = renderer.shaderManager.getGLShader(); - - // TODO change quad to mesh.. - if (!this.firstRun) + // cacheing.. + if (!shader) { - this.firstRun = true; + if (filter.glShaderKey) + { + shader = this.shaderCache[filter.glShaderKey]; + + if (!shader) + { + shader = new Shader(this.gl, filter.vertexSrc, filter.fragmentSrc); + + filter.glShaders[renderer.CONTEXT_UID] = this.shaderCache[filter.glShaderKey] = shader; + } + } + else + { + shader = filter.glShaders[renderer.CONTEXT_UID] = new Shader(this.gl, filter.vertexSrc, filter.fragmentSrc); + } + + // TODO - this only needs to be done once? renderer.bindVao(null); this.quad.initVao(shader); @@ -244,58 +255,146 @@ renderer.maskManager.pushScissorMask(null, renderer.maskManager.scissorData); } - // renderer._bindGLShader(shader); + renderer.bindShader(shader); - let currentState; - const uniforms = filter.uniforms; + // free unit 0 for us, doesn't matter what was there + // don't try to restore it, because syncUniforms can upload it to another slot + // and it'll be a problem + const tex = this.renderer.emptyTextures[0]; - if (uniforms.filterArea) - { - currentState = this.filterData.stack[this.filterData.index]; - - const filterArea = uniforms.filterArea; - - filterArea[0] = currentState.renderTarget.size.width; - filterArea[1] = currentState.renderTarget.size.height; - filterArea[2] = currentState.sourceFrame.x; - filterArea[3] = currentState.sourceFrame.y; - - filter.uniforms.filterArea = filterArea; - } - - // use this to clamp displaced texture coords so they belong to filterArea - // see displacementFilter fragment shader for an example - if (uniforms.filterClamp) - { - currentState = this.filterData.stack[this.filterData.index]; - - const filterClamp = uniforms.filterClamp; - - filterClamp[0] = 0; - filterClamp[1] = 0; - filterClamp[2] = (currentState.sourceFrame.width - 1) / currentState.renderTarget.size.width; - filterClamp[3] = (currentState.sourceFrame.height - 1) / currentState.renderTarget.size.height; - - filter.uniforms.filterClamp = filterClamp; - } - - renderer.shaderManager.setUniforms(filter.uniforms); + this.renderer.boundTextures[0] = tex; + // this syncs the pixi filters uniforms with glsl uniforms + this.syncUniforms(shader, filter); renderer.state.setBlendMode(filter.blendMode); - // temporary bypass cache.. - const tex = this.renderer.boundTextures[0]; - gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, input.texture.texture); this.quad.vao.draw(this.renderer.gl.TRIANGLES, 6, 0); - // restore cache. gl.bindTexture(gl.TEXTURE_2D, tex._glTextures[this.renderer.CONTEXT_UID].texture); } /** + * Uploads the uniforms of the filter. + * + * @param {GLShader} shader - The underlying gl shader. + * @param {PIXI.Filter} filter - The filter we are synchronizing. + */ + syncUniforms(shader, filter) + { + const uniformData = filter.uniformData; + const uniforms = filter.uniforms; + + // 0 is reserved for the pixi texture so we start at 1! + let textureCount = 1; + let currentState; + + // filterArea and filterClamp that are handled by FilterManager directly + // they must not appear in uniformData + + if (shader.uniforms.filterArea) + { + currentState = this.filterData.stack[this.filterData.index]; + + const filterArea = shader.uniforms.filterArea; + + filterArea[0] = currentState.renderTarget.size.width; + filterArea[1] = currentState.renderTarget.size.height; + filterArea[2] = currentState.sourceFrame.x; + filterArea[3] = currentState.sourceFrame.y; + + shader.uniforms.filterArea = filterArea; + } + + // use this to clamp displaced texture coords so they belong to filterArea + // see displacementFilter fragment shader for an example + if (shader.uniforms.filterClamp) + { + currentState = currentState || this.filterData.stack[this.filterData.index]; + + const filterClamp = shader.uniforms.filterClamp; + + filterClamp[0] = 0; + filterClamp[1] = 0; + filterClamp[2] = (currentState.sourceFrame.width - 1) / currentState.renderTarget.size.width; + filterClamp[3] = (currentState.sourceFrame.height - 1) / currentState.renderTarget.size.height; + + shader.uniforms.filterClamp = filterClamp; + } + + // TODO Cacheing layer.. + for (const i in uniformData) + { + if (uniformData[i].type === 'sampler2D' && uniforms[i] !== 0) + { + if (uniforms[i].baseTexture) + { + shader.uniforms[i] = this.renderer.bindTexture(uniforms[i].baseTexture, textureCount); + } + else + { + shader.uniforms[i] = textureCount; + + // TODO + // this is helpful as renderTargets can also be set. + // Although thinking about it, we could probably + // make the filter texture cache return a RenderTexture + // rather than a renderTarget + const gl = this.renderer.gl; + + this.renderer.boundTextures[textureCount] = this.renderer.emptyTextures[textureCount]; + gl.activeTexture(gl.TEXTURE0 + textureCount); + + uniforms[i].texture.bind(); + } + + textureCount++; + } + else if (uniformData[i].type === 'mat3') + { + // check if its pixi matrix.. + if (uniforms[i].a !== undefined) + { + shader.uniforms[i] = uniforms[i].toArray(true); + } + else + { + shader.uniforms[i] = uniforms[i]; + } + } + else if (uniformData[i].type === 'vec2') + { + // check if its a point.. + if (uniforms[i].x !== undefined) + { + const val = shader.uniforms[i] || new Float32Array(2); + + val[0] = uniforms[i].x; + val[1] = uniforms[i].y; + shader.uniforms[i] = val; + } + else + { + shader.uniforms[i] = uniforms[i]; + } + } + else if (uniformData[i].type === 'float') + { + if (shader.uniforms.data[i].value !== uniformData[i]) + { + shader.uniforms[i] = uniforms[i]; + } + } + else + { + shader.uniforms[i] = uniforms[i]; + } + } + } + + /** * Gets a render target from the pool, or creates a new one. * * @param {boolean} clear - Should we clear the render texture when we get it? @@ -442,7 +541,6 @@ renderTarget.resolution = resolution; renderTarget.defaultFrame.width = renderTarget.size.width = minWidth / resolution; renderTarget.defaultFrame.height = renderTarget.size.height = minHeight / resolution; - renderTarget.attachStencilBuffer(); return renderTarget; } diff --git a/src/core/settings.js b/src/core/settings.js index bb08cb0..f4dd087 100644 --- a/src/core/settings.js +++ b/src/core/settings.js @@ -77,11 +77,11 @@ * * @static * @memberof PIXI.settings - * @type {RegExp|string} + * @type {RegExp} * @example `@2x` - * @default /@(.+)x/ + * @default /@([0-9\.]+)x/ */ - RETINA_PREFIX: /@(.+)x/, + RETINA_PREFIX: /@([0-9\.]+)x/, /** * The default render options if none are supplied to {@link PIXI.WebGLRenderer} @@ -175,14 +175,24 @@ SCALE_MODE: 0, /** - * Default specify float precision in shaders. + * Default specify float precision in vertex shader. + * + * @static + * @memberof PIXI.settings + * @type {PIXI.PRECISION} + * @default PIXI.PRECISION.HIGH + */ + PRECISION_VERTEX: 'highp', + + /** + * Default specify float precision in fragment shader. * * @static * @memberof PIXI.settings * @type {PIXI.PRECISION} * @default PIXI.PRECISION.MEDIUM */ - PRECISION: 'mediump', + PRECISION_FRAGMENT: 'mediump', /** * Can we upload the same buffer in a single frame? diff --git a/src/core/sprites/Sprite.js b/src/core/sprites/Sprite.js index 28b0b18..121bf58 100644 --- a/src/core/sprites/Sprite.js +++ b/src/core/sprites/Sprite.js @@ -212,11 +212,11 @@ } else { - w0 = orig.width * (1 - anchor._x); - w1 = orig.width * -anchor._x; + w1 = -anchor._x * orig.width; + w0 = w1 + orig.width; - h0 = orig.height * (1 - anchor._y); - h1 = orig.height * -anchor._y; + h1 = -anchor._y * orig.height; + h0 = h1 + orig.height; } // xy @@ -269,11 +269,11 @@ const tx = wt.tx; const ty = wt.ty; - const w0 = (orig.width) * (1 - anchor._x); - const w1 = (orig.width) * -anchor._x; + const w1 = -anchor._x * orig.width; + const w0 = w1 + orig.width; - const h0 = orig.height * (1 - anchor._y); - const h1 = orig.height * -anchor._y; + const h1 = -anchor._y * orig.height; + const h0 = h1 + orig.height; // xy vertexData[0] = (a * w1) + (c * h1) + tx; @@ -440,7 +440,7 @@ * * @static * @param {number|string|PIXI.BaseTexture|HTMLCanvasElement|HTMLVideoElement} source Source to create texture from - * @return {PIXI.Sprite} The newly created texture + * @return {PIXI.Sprite} The newly created sprite */ static from(source) { @@ -538,8 +538,8 @@ } /** - * The tint applied to the sprite. This is a hex value. A value of - * 0xFFFFFF will remove any tint effect. + * The tint applied to the sprite. This is a hex value. + * A value of 0xFFFFFF will remove any tint effect. * * @member {number} * @default 0xFFFFFF diff --git a/src/core/sprites/canvas/CanvasTinter.js b/src/core/sprites/canvas/CanvasTinter.js index 1cf316b..27e5e0c 100644 --- a/src/core/sprites/canvas/CanvasTinter.js +++ b/src/core/sprites/canvas/CanvasTinter.js @@ -75,8 +75,8 @@ crop.width *= resolution; crop.height *= resolution; - canvas.width = crop.width; - canvas.height = crop.height; + canvas.width = Math.ceil(crop.width); + canvas.height = Math.ceil(crop.height); context.fillStyle = `#${(`00000${(color | 0).toString(16)}`).substr(-6)}`; @@ -130,8 +130,8 @@ crop.width *= resolution; crop.height *= resolution; - canvas.width = crop.width; - canvas.height = crop.height; + canvas.width = Math.ceil(crop.width); + canvas.height = Math.ceil(crop.height); context.globalCompositeOperation = 'copy'; context.fillStyle = `#${(`00000${(color | 0).toString(16)}`).substr(-6)}`; @@ -172,8 +172,8 @@ crop.width *= resolution; crop.height *= resolution; - canvas.width = crop.width; - canvas.height = crop.height; + canvas.width = Math.ceil(crop.width); + canvas.height = Math.ceil(crop.height); context.globalCompositeOperation = 'copy'; context.drawImage( diff --git a/src/core/sprites/webgl/SpriteRenderer.js b/src/core/sprites/webgl/SpriteRenderer.js index 2affcd5..7c352e2 100644 --- a/src/core/sprites/webgl/SpriteRenderer.js +++ b/src/core/sprites/webgl/SpriteRenderer.js @@ -30,7 +30,7 @@ /** * Number of values sent in the vertex buffer. - * positionX, positionY, colorR, colorG, colorB = 5 + * aVertexPosition(2), aTextureCoord(1), aColor(1), aTextureId(1) = 5 * * @member {number} */ @@ -75,7 +75,6 @@ this.shader = null; this.currentIndex = 0; - TICK = 0; this.groups = []; for (let k = 0; k < this.size; k++) diff --git a/src/core/textures/BaseTexture.js b/src/core/textures/BaseTexture.js index 853de7e..aeccf39 100644 --- a/src/core/textures/BaseTexture.js +++ b/src/core/textures/BaseTexture.js @@ -232,16 +232,24 @@ this.realWidth = this.source.naturalWidth || this.source.videoWidth || this.source.width; this.realHeight = this.source.naturalHeight || this.source.videoHeight || this.source.height; - this.width = this.realWidth / this.resolution; - this.height = this.realHeight / this.resolution; - - this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); + this._updateDimensions(); } this.emit('update', this); } /** + * Update dimensions from real values + */ + _updateDimensions() + { + this.width = this.realWidth / this.resolution; + this.height = this.realHeight / this.resolution; + + this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); + } + + /** * Load a source. * * If the source is not-immediately-available, such as an image that needs to be @@ -525,11 +533,7 @@ this.realWidth = Math.round(svgWidth * this.sourceScale); this.realHeight = Math.round(svgHeight * this.sourceScale); - this.width = this.realWidth / this.resolution; - this.height = this.realHeight / this.resolution; - - // Check pow2 after scale - this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); + this._updateDimensions(); // Create a canvas element const canvas = document.createElement('canvas'); @@ -690,4 +694,52 @@ return baseTexture; } + + /** + * Helper function that creates a base texture based on the source you provide. + * The source can be - image url, image element, canvas element. + * + * @static + * @param {string|HTMLImageElement|HTMLCanvasElement} source - The source to create base texture from. + * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values + * @param {number} [sourceScale=(auto)] - Scale for the original image, used with Svg images. + * @return {PIXI.BaseTexture} The new base texture. + */ + static from(source, scaleMode, sourceScale) + { + if (typeof source === 'string') + { + return BaseTexture.fromImage(source, undefined, scaleMode, sourceScale); + } + else if (source instanceof HTMLImageElement) + { + const imageUrl = source.src; + let baseTexture = BaseTextureCache[imageUrl]; + + if (!baseTexture) + { + baseTexture = new BaseTexture(source, scaleMode); + baseTexture.imageUrl = imageUrl; + + if (sourceScale) + { + baseTexture.sourceScale = sourceScale; + } + + // if there is an @2x at the end of the url we are going to assume its a highres image + baseTexture.resolution = getResolutionOfUrl(imageUrl); + + BaseTextureCache[imageUrl] = baseTexture; + } + + return baseTexture; + } + else if (source instanceof HTMLCanvasElement) + { + return BaseTexture.fromCanvas(source, scaleMode); + } + + // lets assume its a base texture! + return source; + } } diff --git a/package.json b/package.json index 6a366ba..71ade15 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pixi.js", - "version": "4.3.4", + "version": "4.3.5", "description": "Pixi.js is a fast lightweight 2D library that works across all devices.", "author": "Mat Groves", "contributors": [ @@ -61,7 +61,7 @@ "ismobilejs": "^0.4.0", "object-assign": "^4.0.1", "pixi-gl-core": "^1.0.3", - "resource-loader": "^2.0.4" + "resource-loader": "^2.0.6" }, "devDependencies": { "babel-cli": "^6.18.0", @@ -70,9 +70,9 @@ "babel-preset-es2015": "^6.14.0", "babelify": "^7.3.0", "del": "^2.2.0", - "electron-prebuilt": "^1.3.2", + "electron": "^1.4.15", "eslint": "^3.5.0", - "floss": "^1.2.0", + "floss": "^2.0.1", "gh-pages": "^0.11.0", "jaguarjs-jsdoc": "^1.0.1", "js-md5": "^0.4.1", diff --git a/src/accessibility/AccessibilityManager.js b/src/accessibility/AccessibilityManager.js index df2716a..f16bcb1 100644 --- a/src/accessibility/AccessibilityManager.js +++ b/src/accessibility/AccessibilityManager.js @@ -3,7 +3,7 @@ import accessibleTarget from './accessibleTarget'; // add some extra variables to the container.. -Object.assign( +core.utils.mixins.delayMixin( core.DisplayObject.prototype, accessibleTarget ); diff --git a/src/core/Application.js b/src/core/Application.js index ea0f058..5340578 100644 --- a/src/core/Application.js +++ b/src/core/Application.js @@ -1,6 +1,6 @@ import { autoDetectRenderer } from './autoDetectRenderer'; import Container from './display/Container'; -import Ticker from './ticker/Ticker'; +import { shared, Ticker } from './ticker'; /** * Convenience class to create a new PIXI application. @@ -35,9 +35,9 @@ * @param {boolean} [noWebGL=false] - prevents selection of WebGL renderer, even if such is present * @param {boolean} [options.legacy=false] - If true Pixi will aim to ensure compatibility * with older / less advanced devices. If you experience unexplained flickering try setting this to true. - + * @param {boolean} [useSharedTicker=false] - `true` to use PIXI.ticker.shared, `false` to create new ticker. */ - constructor(width, height, options, noWebGL) + constructor(width, height, options, noWebGL, useSharedTicker = false) { /** * WebGL renderer if available, otherwise CanvasRenderer @@ -46,23 +46,46 @@ this.renderer = autoDetectRenderer(width, height, options, noWebGL); /** - * The root display container that's renderered. + * The root display container that's rendered. * @member {PIXI.Container} */ this.stage = new Container(); /** + * Internal reference to the ticker + * @member {PIXI.ticker.Ticker} + * @private + */ + this._ticker = null; + + /** * Ticker for doing render updates. * @member {PIXI.ticker.Ticker} + * @default PIXI.ticker.shared */ - this.ticker = new Ticker(); - - this.ticker.add(this.render, this); + this.ticker = useSharedTicker ? shared : new Ticker(); // Start the rendering this.start(); } + set ticker(ticker) // eslint-disable-line require-jsdoc + { + if (this._ticker) + { + this._ticker.remove(this.render, this); + } + this._ticker = ticker; + if (ticker) + { + ticker.add(this.render, this); + } + } + get ticker() // eslint-disable-line require-jsdoc + { + return this._ticker; + } + /** * Render the current stage. */ @@ -76,7 +99,7 @@ */ stop() { - this.ticker.stop(); + this._ticker.stop(); } /** @@ -84,7 +107,7 @@ */ start() { - this.ticker.start(); + this._ticker.start(); } /** @@ -98,13 +121,22 @@ } /** + * Reference to the renderer's screen rectangle. Its safe to use as filterArea or hitArea for whole screen + * @member {PIXI.Rectangle} + * @readonly + */ + get screen() + { + return this.renderer.screen; + } + + /** * Destroy and don't use after this. * @param {Boolean} [removeView=false] Automatically remove canvas from DOM. */ destroy(removeView) { this.stop(); - this.ticker.remove(this.render, this); this.ticker = null; this.stage.destroy(); diff --git a/src/core/Shader.js b/src/core/Shader.js new file mode 100644 index 0000000..78b17e6 --- /dev/null +++ b/src/core/Shader.js @@ -0,0 +1,46 @@ +import { GLShader } from 'pixi-gl-core'; +import settings from './settings'; + +function checkPrecision(src, def) +{ + if (src instanceof Array) + { + if (src[0].substring(0, 9) !== 'precision') + { + const copy = src.slice(0); + + copy.unshift(`precision ${def} float;`); + + return copy; + } + } + else if (src.substring(0, 9) !== 'precision') + { + return `precision ${def} float;\n${src}`; + } + + return src; +} + +/** + * Wrapper class, webGL Shader for Pixi. + * Adds precision string if vertexSrc or fragmentSrc have no mention of it. + * + * @class + * @extends GLShader + * @memberof PIXI + */ +export default class Shader extends GLShader +{ + /** + * + * @param {WebGLRenderingContext} gl - The current WebGL rendering context + * @param {string|string[]} vertexSrc - The vertex shader source as an array of strings. + * @param {string|string[]} fragmentSrc - The fragment shader source as an array of strings. + */ + constructor(gl, vertexSrc, fragmentSrc) + { + super(gl, checkPrecision(vertexSrc, settings.PRECISION_VERTEX), + checkPrecision(fragmentSrc, settings.PRECISION_FRAGMENT)); + } +} diff --git a/src/core/const.js b/src/core/const.js index ef6f662..3de0efe 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -188,7 +188,7 @@ * The gc modes that are supported by pixi. * * The {@link PIXI.settings.GC_MODE} Garbage Collection mode for pixi textures is AUTO - * If set to GC_MODE, the renderer will occasianally check textures usage. If they are not + * If set to GC_MODE, the renderer will occasionally check textures usage. If they are not * used for a specified period of time they will be removed from the GPU. They will of course * be uploaded again when they are required. This is a silent behind the scenes process that * should ensure that the GPU does not get filled up. diff --git a/src/core/display/Container.js b/src/core/display/Container.js index ed8cd5f..17fde8d 100644 --- a/src/core/display/Container.js +++ b/src/core/display/Container.js @@ -73,11 +73,12 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.push(child); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -108,11 +109,12 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.splice(index, 0, child); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -225,10 +227,11 @@ if (index === -1) return null; child.parent = null; + // ensure child transform will be recalculated + child.transform._parentID = -1; removeItems(this.children, index, 1); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -249,11 +252,12 @@ { const child = this.getChildAt(index); + // ensure child transform will be recalculated.. child.parent = null; + child.transform._parentID = -1; removeItems(this.children, index, 1); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -284,13 +288,12 @@ for (let i = 0; i < removed.length; ++i) { removed[i].parent = null; + if (removed[i].transform) + { + removed[i].transform._parentID = -1; + } } - // ensure a transform will be recalculated.. - if (this.transform) - { - this.transform._parentID = -1; - } this._boundsID++; this.onChildrenChange(beginIndex); diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index f37f2f5..be4c4fb 100644 --- a/src/core/graphics/Graphics.js +++ b/src/core/graphics/Graphics.js @@ -28,8 +28,9 @@ { /** * + * @param {boolean} [nativeLines=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP */ - constructor() + constructor(nativeLines = false) { super(); @@ -50,6 +51,13 @@ this.lineWidth = 0; /** + * If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * + * @member {boolean} + */ + this.nativeLines = nativeLines; + + /** * The color of any lines drawn. * * @member {string} @@ -757,28 +765,14 @@ if (!this._spriteRect) { - if (!Graphics._SPRITE_TEXTURE) - { - Graphics._SPRITE_TEXTURE = RenderTexture.create(10, 10); - - const canvas = document.createElement('canvas'); - - canvas.width = 10; - canvas.height = 10; - - const context = canvas.getContext('2d'); - - context.fillStyle = 'white'; - context.fillRect(0, 0, 10, 10); - - Graphics._SPRITE_TEXTURE = Texture.fromCanvas(canvas); - } - - this._spriteRect = new Sprite(Graphics._SPRITE_TEXTURE); + this._spriteRect = new Sprite(new Texture(Texture.WHITE)); } + + const sprite = this._spriteRect; + if (this.tint === 0xffffff) { - this._spriteRect.tint = this.graphicsData[0].fillColor; + sprite.tint = this.graphicsData[0].fillColor; } else { @@ -792,20 +786,21 @@ t1[1] *= t2[1]; t1[2] *= t2[2]; - this._spriteRect.tint = rgb2hex(t1); + sprite.tint = rgb2hex(t1); } - this._spriteRect.alpha = this.graphicsData[0].fillAlpha; - this._spriteRect.worldAlpha = this.worldAlpha * this._spriteRect.alpha; + sprite.alpha = this.graphicsData[0].fillAlpha; + sprite.worldAlpha = this.worldAlpha * sprite.alpha; + sprite.blendMode = this.blendMode; - Graphics._SPRITE_TEXTURE._frame.width = rect.width; - Graphics._SPRITE_TEXTURE._frame.height = rect.height; + sprite.texture._frame.width = rect.width; + sprite.texture._frame.height = rect.height; - this._spriteRect.transform.worldTransform = this.transform.worldTransform; + sprite.transform.worldTransform = this.transform.worldTransform; - this._spriteRect.anchor.set(-rect.x / rect.width, -rect.y / rect.height); - this._spriteRect._onAnchorUpdate(); + sprite.anchor.set(-rect.x / rect.width, -rect.y / rect.height); + sprite._onAnchorUpdate(); - this._spriteRect._renderWebGL(renderer); + sprite._renderWebGL(renderer); } /** @@ -1031,6 +1026,7 @@ this.fillColor, this.fillAlpha, this.filling, + this.nativeLines, shape ); diff --git a/src/core/graphics/GraphicsData.js b/src/core/graphics/GraphicsData.js index f5a87ae..b6f03c0 100644 --- a/src/core/graphics/GraphicsData.js +++ b/src/core/graphics/GraphicsData.js @@ -14,14 +14,19 @@ * @param {number} fillColor - the color of the fill * @param {number} fillAlpha - the alpha of the fill * @param {boolean} fill - whether or not the shape is filled with a colour + * @param {boolean} nativeLines - the method for drawing lines * @param {PIXI.Circle|PIXI.Rectangle|PIXI.Ellipse|PIXI.Polygon} shape - The shape object to draw. */ - constructor(lineWidth, lineColor, lineAlpha, fillColor, fillAlpha, fill, shape) + constructor(lineWidth, lineColor, lineAlpha, fillColor, fillAlpha, fill, nativeLines, shape) { /** * @member {number} the width of the line to draw */ this.lineWidth = lineWidth; + /** + * @member {boolean} if true the liens will be draw using LINES instead of TRIANGLE_STRIP + */ + this.nativeLines = nativeLines; /** * @member {number} the color of the line to draw @@ -85,6 +90,7 @@ this.fillColor, this.fillAlpha, this.fill, + this.nativeLines, this.shape ); } diff --git a/src/core/graphics/webgl/GraphicsRenderer.js b/src/core/graphics/webgl/GraphicsRenderer.js index 14083fe..67f5e92 100644 --- a/src/core/graphics/webgl/GraphicsRenderer.js +++ b/src/core/graphics/webgl/GraphicsRenderer.js @@ -102,7 +102,15 @@ shaderTemp.uniforms.alpha = graphics.worldAlpha; renderer.bindVao(webGLData.vao); - webGLData.vao.draw(gl.TRIANGLE_STRIP, webGLData.indices.length); + + if (graphics.nativeLines) + { + gl.drawArrays(gl.LINES, 0, webGLData.points.length / 6); + } + else + { + webGLData.vao.draw(gl.TRIANGLE_STRIP, webGLData.indices.length); + } } } diff --git a/src/core/graphics/webgl/utils/buildLine.js b/src/core/graphics/webgl/utils/buildLine.js index ecf43f7..4d6862e 100644 --- a/src/core/graphics/webgl/utils/buildLine.js +++ b/src/core/graphics/webgl/utils/buildLine.js @@ -11,7 +11,29 @@ * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties * @param {object} webGLData - an object containing all the webGL-specific information to create this shape */ -export default function buildLine(graphicsData, webGLData) +export default function (graphicsData, webGLData) +{ + if (graphicsData.nativeLines) + { + buildNativeLine(graphicsData, webGLData); + } + else + { + buildLine(graphicsData, webGLData); + } +} + +/** + * Builds a line to draw using the poligon method. + * + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @private + * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties + * @param {object} webGLData - an object containing all the webGL-specific information to create this shape + */ +function buildLine(graphicsData, webGLData) { // TODO OPTIMISE! let points = graphicsData.points; @@ -224,3 +246,46 @@ indices.push(indexStart - 1); } + +/** + * Builds a line to draw using the gl.drawArrays(gl.LINES) method + * + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @private + * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties + * @param {object} webGLData - an object containing all the webGL-specific information to create this shape + */ +function buildNativeLine(graphicsData, webGLData) +{ + let i = 0; + const points = graphicsData.points; + + if (points.length === 0) return; + + const verts = webGLData.points; + const length = points.length / 2; + + // sort color + const color = hex2rgb(graphicsData.lineColor); + const alpha = graphicsData.lineAlpha; + const r = color[0] * alpha; + const g = color[1] * alpha; + const b = color[2] * alpha; + + for (i = 1; i < length; i++) + { + const p1x = points[(i - 1) * 2]; + const p1y = points[((i - 1) * 2) + 1]; + + const p2x = points[i * 2]; + const p2y = points[(i * 2) + 1]; + + verts.push(p1x, p1y); + verts.push(r, g, b, alpha); + + verts.push(p2x, p2y); + verts.push(r, g, b, alpha); + } +} diff --git a/src/core/graphics/webgl/utils/buildRoundedRectangle.js b/src/core/graphics/webgl/utils/buildRoundedRectangle.js index 9abc807..a6c8461 100644 --- a/src/core/graphics/webgl/utils/buildRoundedRectangle.js +++ b/src/core/graphics/webgl/utils/buildRoundedRectangle.js @@ -77,6 +77,26 @@ } /** + * Calculate a single point for a quadratic bezier curve. + * Utility function used by quadraticBezierCurve. + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @private + * @param {number} n1 - first number + * @param {number} n2 - second number + * @param {number} perc - percentage + * @return {number} the result + * + */ +function getPt(n1, n2, perc) +{ + const diff = n2 - n1; + + return n1 + (diff * perc); +} + +/** * Calculate the points for a quadratic bezier curve. (helper function..) * Based on: https://stackoverflow.com/questions/785097/how-do-i-implement-a-bezier-curve-in-c * @@ -105,13 +125,6 @@ let x = 0; let y = 0; - function getPt(n1, n2, perc) - { - const diff = n2 - n1; - - return n1 + (diff * perc); - } - for (let i = 0, j = 0; i <= n; ++i) { j = i / n; diff --git a/src/core/index.js b/src/core/index.js index f7a817c..4f414e8 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -30,6 +30,7 @@ export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; export { default as CanvasGraphicsRenderer } from './graphics/canvas/CanvasGraphicsRenderer'; +export { default as Spritesheet } from './textures/Spritesheet'; export { default as Texture } from './textures/Texture'; export { default as BaseTexture } from './textures/BaseTexture'; export { default as RenderTexture } from './textures/RenderTexture'; diff --git a/src/core/math/Matrix.js b/src/core/math/Matrix.js index 0b77477..3679860 100644 --- a/src/core/math/Matrix.js +++ b/src/core/math/Matrix.js @@ -13,45 +13,50 @@ export default class Matrix { /** - * + * @param {number} [a=1] - x scale + * @param {number} [b=0] - y skew + * @param {number} [c=0] - x skew + * @param {number} [d=1] - y scale + * @param {number} [tx=0] - x translation + * @param {number} [ty=0] - y translation */ - constructor() + constructor(a = 1, b = 0, c = 0, d = 1, tx = 0, ty = 0) { /** * @member {number} * @default 1 */ - this.a = 1; + this.a = a; /** * @member {number} * @default 0 */ - this.b = 0; + this.b = b; /** * @member {number} * @default 0 */ - this.c = 0; + this.c = c; /** * @member {number} * @default 1 */ - this.d = 1; + this.d = d; /** * @member {number} * @default 0 */ - this.tx = 0; + this.tx = tx; /** * @member {number} * @default 0 */ - this.ty = 0; + this.ty = ty; this.array = null; } diff --git a/src/core/renderers/SystemRenderer.js b/src/core/renderers/SystemRenderer.js index fa116a6..a24c775 100644 --- a/src/core/renderers/SystemRenderer.js +++ b/src/core/renderers/SystemRenderer.js @@ -1,5 +1,5 @@ import { sayHello, hex2string, hex2rgb } from '../utils'; -import { Matrix } from '../math'; +import { Matrix, Rectangle } from '../math'; import { RENDERER_TYPE } from '../const'; import settings from '../settings'; import Container from '../display/Container'; @@ -21,8 +21,8 @@ { /** * @param {string} system - The name of the system this renderer is for. - * @param {number} [width=800] - the width of the canvas view - * @param {number} [height=600] - the height of the canvas view + * @param {number} [screenWidth=800] - the width of the screen + * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false @@ -37,7 +37,7 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(system, width, height, options) + constructor(system, screenWidth, screenHeight, options) { super(); @@ -69,20 +69,13 @@ this.type = RENDERER_TYPE.UNKNOWN; /** - * The width of the canvas view + * Measurements of the screen. (0, 0, screenWidth, screenHeight) * - * @member {number} - * @default 800 - */ - this.width = width || 800; - - /** - * The height of the canvas view + * Its safe to use as filterArea or hitArea for whole stage * - * @member {number} - * @default 600 + * @member {PIXI.Rectangle} */ - this.height = height || 600; + this.screen = new Rectangle(0, 0, screenWidth || 800, screenHeight || 600); /** * The canvas element that everything is drawn to @@ -107,7 +100,7 @@ this.transparent = options.transparent; /** - * Whether the render view should be resized automatically + * Whether css dimensions of canvas view should be resized to screen dimensions automatically * * @member {boolean} */ @@ -192,23 +185,48 @@ } /** - * Resizes the canvas view to the specified width and height + * Same as view.width, actual number of pixels in the canvas by horizontal * - * @param {number} width - the new width of the canvas view - * @param {number} height - the new height of the canvas view + * @member {number} + * @readonly + * @default 800 */ - resize(width, height) + get width() { - this.width = width * this.resolution; - this.height = height * this.resolution; + return this.view.width; + } - this.view.width = this.width; - this.view.height = this.height; + /** + * Same as view.height, actual number of pixels in the canvas by vertical + * + * @member {number} + * @readonly + * @default 600 + */ + get height() + { + return this.view.height; + } + + /** + * Resizes the screen and canvas to the specified width and height + * Canvas dimensions are multiplied by resolution + * + * @param {number} screenWidth - the new width of the screen + * @param {number} screenHeight - the new height of the screen + */ + resize(screenWidth, screenHeight) + { + this.screen.width = screenWidth; + this.screen.height = screenHeight; + + this.view.width = screenWidth * this.resolution; + this.view.height = screenHeight * this.resolution; if (this.autoResize) { - this.view.style.width = `${this.width / this.resolution}px`; - this.view.style.height = `${this.height / this.resolution}px`; + this.view.style.width = `${screenWidth}px`; + this.view.style.height = `${screenHeight}px`; } } @@ -249,11 +267,10 @@ this.type = RENDERER_TYPE.UNKNOWN; - this.width = 0; - this.height = 0; - this.view = null; + this.screen = null; + this.resolution = 0; this.transparent = false; diff --git a/src/core/renderers/canvas/CanvasRenderer.js b/src/core/renderers/canvas/CanvasRenderer.js index 1bd2d5e..e84e49c 100644 --- a/src/core/renderers/canvas/CanvasRenderer.js +++ b/src/core/renderers/canvas/CanvasRenderer.js @@ -18,8 +18,8 @@ export default class CanvasRenderer extends SystemRenderer { /** - * @param {number} [width=800] - the width of the canvas view - * @param {number} [height=600] - the height of the canvas view + * @param {number} [screenWidth=800] - the width of the screen + * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false @@ -34,9 +34,9 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(width, height, options = {}) + constructor(screenWidth, screenHeight, options = {}) { - super('Canvas', width, height, options); + super('Canvas', screenWidth, screenHeight, options); this.type = RENDERER_TYPE.CANVAS; @@ -96,7 +96,7 @@ this.context = null; this.renderingToScreen = false; - this.resize(width, height); + this.resize(screenWidth, screenHeight); } /** @@ -283,12 +283,12 @@ * * @extends PIXI.SystemRenderer#resize * - * @param {number} width - The new width of the canvas view - * @param {number} height - The new height of the canvas view + * @param {number} screenWidth - the new width of the screen + * @param {number} screenHeight - the new height of the screen */ - resize(width, height) + resize(screenWidth, screenHeight) { - super.resize(width, height); + super.resize(screenWidth, screenHeight); // reset the scale mode.. oddly this seems to be reset when the canvas is resized. // surely a browser bug?? Let pixi fix that for you.. diff --git a/src/core/renderers/webgl/TextureManager.js b/src/core/renderers/webgl/TextureManager.js index b685c1a..8227289 100644 --- a/src/core/renderers/webgl/TextureManager.js +++ b/src/core/renderers/webgl/TextureManager.js @@ -61,7 +61,7 @@ * Updates and/or Creates a WebGL texture for the renderer's context. * * @param {PIXI.BaseTexture|PIXI.Texture} texture - the texture to update - * @param {Number} location - the location the texture will be bound to. + * @param {number} location - the location the texture will be bound to. * @return {GLTexture} The gl texture. */ updateTexture(texture, location) diff --git a/src/core/renderers/webgl/WebGLRenderer.js b/src/core/renderers/webgl/WebGLRenderer.js index e4cd8a0..830b382 100644 --- a/src/core/renderers/webgl/WebGLRenderer.js +++ b/src/core/renderers/webgl/WebGLRenderer.js @@ -31,8 +31,8 @@ { /** * - * @param {number} [width=0] - the width of the canvas view - * @param {number} [height=0] - the height of the canvas view + * @param {number} [screenWidth=800] - the width of the screen + * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false @@ -53,9 +53,9 @@ * @param {boolean} [options.legacy=false] - If true Pixi will aim to ensure compatibility * with older / less advanced devices. If you experiance unexplained flickering try setting this to true. */ - constructor(width, height, options = {}) + constructor(screenWidth, screenHeight, options = {}) { - super('WebGL', width, height, options); + super('WebGL', screenWidth, screenHeight, options); this.legacy = !!options.legacy; @@ -245,7 +245,7 @@ glCore._testingContext = gl; // setup the width/height properties and gl viewport - this.resize(this.width, this.height); + this.resize(this.screen.width, this.screen.height); } /** @@ -343,16 +343,16 @@ /** * Resizes the webGL view to the specified width and height. * - * @param {number} width - the new width of the webGL view - * @param {number} height - the new height of the webGL view + * @param {number} screenWidth - the new width of the screen + * @param {number} screenHeight - the new height of the screen */ - resize(width, height) + resize(screenWidth, screenHeight) { // if(width * this.resolution === this.width && height * this.resolution === this.height)return; - SystemRenderer.prototype.resize.call(this, width, height); + SystemRenderer.prototype.resize.call(this, screenWidth, screenHeight); - this.rootRenderTarget.resize(width, height); + this.rootRenderTarget.resize(screenWidth, screenHeight); if (this._activeRenderTarget === this.rootRenderTarget) { @@ -396,6 +396,26 @@ } /** + * Erases the render texture and fills the drawing area with a colour + * + * @param {PIXI.RenderTexture} renderTexture - The render texture to clear + * @param {number} [clearColor] - The colour + * @return {PIXI.WebGLRenderer} Returns itself. + */ + clearRenderTexture(renderTexture, clearColor) + { + const baseTexture = renderTexture.baseTexture; + const renderTarget = baseTexture._glRenderTargets[this.CONTEXT_UID]; + + if (renderTarget) + { + renderTarget.clear(clearColor); + } + + return this; + } + + /** * Binds a render texture for rendering * * @param {PIXI.RenderTexture} renderTexture - The render texture to render @@ -474,9 +494,10 @@ * Changes the current GLShader to the one given in parameter * * @param {PIXI.glCore.Shader} shader - the new glShader + * @param {boolean} [autoProject=true] - Whether automatically set the projection matrix * @return {PIXI.WebGLRenderer} Returns itself. */ - _bindGLShader(shader) + _bindGLShader(shader, autoProject) { // TODO cache if (this._activeShader !== shader) @@ -484,8 +505,14 @@ this._activeShader = shader; shader.bind(); - // automatically set the projection matrix - shader.uniforms.projectionMatrix = this._activeRenderTarget.projectionMatrix.toArray(true); + // `autoProject` normally would be a default parameter set to true + // but because of how Babel transpiles default parameters + // it hinders the performance of this method. + if (autoProject !== false) + { + // automatically set the projection matrix + shader.uniforms.projectionMatrix = this._activeRenderTarget.projectionMatrix.toArray(true); + } } return this; diff --git a/src/core/renderers/webgl/WebGLState.js b/src/core/renderers/webgl/WebGLState.js index ba6ecc5..6c846d5 100755 --- a/src/core/renderers/webgl/WebGLState.js +++ b/src/core/renderers/webgl/WebGLState.js @@ -82,18 +82,20 @@ push() { // next state.. - let state = this.stack[++this.stackIndex]; + let state = this.stack[this.stackIndex]; if (!state) { state = this.stack[this.stackIndex] = new Uint8Array(16); } + ++this.stackIndex; + // copy state.. // set active state so we can force overrides of gl state for (let i = 0; i < this.activeState.length; i++) { - this.activeState[i] = state[i]; + state[i] = this.activeState[i]; } } diff --git a/src/core/renderers/webgl/filters/Filter.js b/src/core/renderers/webgl/filters/Filter.js index 2c3eaad..4eb6128 100644 --- a/src/core/renderers/webgl/filters/Filter.js +++ b/src/core/renderers/webgl/filters/Filter.js @@ -9,7 +9,7 @@ * @memberof PIXI * @extends PIXI.Shader */ -class Filter extends Shader +export default class Filter extends Shader { /** * @param {string} [vertexSrc] - The source of the vertex shader. @@ -56,15 +56,16 @@ * @param {PIXI.RenderTarget} input - The input render target. * @param {PIXI.RenderTarget} output - The target to output to. * @param {boolean} clear - Should the output be cleared before rendering to it + * @param {object} [currentState] - It's current state of filter. + * There are some useful properties in the currentState : + * target, filters, sourceFrame, destinationFrame, renderTarget, resolution */ - apply(filterManager, input, output, clear) + apply(filterManager, input, output, clear, currentState) // eslint-disable-line no-unused-vars { // do as you please! - filterManager.applyFilter(this, input, output, clear); + filterManager.applyFilter(this, input, output, clear, currentState); // or just do a regular render.. } } - -export default Filter; diff --git a/src/core/renderers/webgl/filters/extractUniformsFromSrc.js b/src/core/renderers/webgl/filters/extractUniformsFromSrc.js index d78a0b1..4893214 100644 --- a/src/core/renderers/webgl/filters/extractUniformsFromSrc.js +++ b/src/core/renderers/webgl/filters/extractUniformsFromSrc.js @@ -12,7 +12,7 @@ function extractUniformsFromString(string) { - const maskRegex = new RegExp('^(projectionMatrix|uSampler|filterArea)$'); + const maskRegex = new RegExp('^(projectionMatrix|uSampler|filterArea|filterClamp)$'); const uniforms = {}; let nameSplit; diff --git a/src/core/renderers/webgl/managers/FilterManager.js b/src/core/renderers/webgl/managers/FilterManager.js index 43fc869..6c7422d 100644 --- a/src/core/renderers/webgl/managers/FilterManager.js +++ b/src/core/renderers/webgl/managers/FilterManager.js @@ -2,6 +2,7 @@ import RenderTarget from '../utils/RenderTarget'; import Quad from '../utils/Quad'; import { Rectangle } from '../../../math'; +import Shader from '../../../Shader'; import * as filterTransforms from '../filters/filterTransforms'; import bitTwiddle from 'bit-twiddle'; @@ -22,8 +23,6 @@ this.filters = []; this.target = null; this.resolution = 1; - - this.firstRun = true; } } @@ -155,7 +154,7 @@ if (filters.length === 1) { - filters[0].apply(this, currentState.renderTarget, lastState.renderTarget, false); + filters[0].apply(this, currentState.renderTarget, lastState.renderTarget, false, currentState); this.freePotRenderTarget(currentState.renderTarget); } else @@ -177,7 +176,7 @@ for (i = 0; i < filters.length - 1; ++i) { - filters[i].apply(this, flip, flop, true); + filters[i].apply(this, flip, flop, true, currentState); const t = flip; @@ -185,7 +184,7 @@ flop = t; } - filters[i].apply(this, flip, lastState.renderTarget, false); + filters[i].apply(this, flip, lastState.renderTarget, false, currentState); this.freePotRenderTarget(flip); this.freePotRenderTarget(flop); @@ -212,16 +211,28 @@ const renderer = this.renderer; const gl = renderer.gl; - renderer.bindShader(filter, true); + let shader = filter.glShaders[renderer.CONTEXT_UID]; - // TODO theres a better way! - // quad can be a mesh and we then should be able to pull this off without accessing the shader directly - const shader = renderer.shaderManager.getGLShader(); - - // TODO change quad to mesh.. - if (!this.firstRun) + // cacheing.. + if (!shader) { - this.firstRun = true; + if (filter.glShaderKey) + { + shader = this.shaderCache[filter.glShaderKey]; + + if (!shader) + { + shader = new Shader(this.gl, filter.vertexSrc, filter.fragmentSrc); + + filter.glShaders[renderer.CONTEXT_UID] = this.shaderCache[filter.glShaderKey] = shader; + } + } + else + { + shader = filter.glShaders[renderer.CONTEXT_UID] = new Shader(this.gl, filter.vertexSrc, filter.fragmentSrc); + } + + // TODO - this only needs to be done once? renderer.bindVao(null); this.quad.initVao(shader); @@ -244,58 +255,146 @@ renderer.maskManager.pushScissorMask(null, renderer.maskManager.scissorData); } - // renderer._bindGLShader(shader); + renderer.bindShader(shader); - let currentState; - const uniforms = filter.uniforms; + // free unit 0 for us, doesn't matter what was there + // don't try to restore it, because syncUniforms can upload it to another slot + // and it'll be a problem + const tex = this.renderer.emptyTextures[0]; - if (uniforms.filterArea) - { - currentState = this.filterData.stack[this.filterData.index]; - - const filterArea = uniforms.filterArea; - - filterArea[0] = currentState.renderTarget.size.width; - filterArea[1] = currentState.renderTarget.size.height; - filterArea[2] = currentState.sourceFrame.x; - filterArea[3] = currentState.sourceFrame.y; - - filter.uniforms.filterArea = filterArea; - } - - // use this to clamp displaced texture coords so they belong to filterArea - // see displacementFilter fragment shader for an example - if (uniforms.filterClamp) - { - currentState = this.filterData.stack[this.filterData.index]; - - const filterClamp = uniforms.filterClamp; - - filterClamp[0] = 0; - filterClamp[1] = 0; - filterClamp[2] = (currentState.sourceFrame.width - 1) / currentState.renderTarget.size.width; - filterClamp[3] = (currentState.sourceFrame.height - 1) / currentState.renderTarget.size.height; - - filter.uniforms.filterClamp = filterClamp; - } - - renderer.shaderManager.setUniforms(filter.uniforms); + this.renderer.boundTextures[0] = tex; + // this syncs the pixi filters uniforms with glsl uniforms + this.syncUniforms(shader, filter); renderer.state.setBlendMode(filter.blendMode); - // temporary bypass cache.. - const tex = this.renderer.boundTextures[0]; - gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, input.texture.texture); this.quad.vao.draw(this.renderer.gl.TRIANGLES, 6, 0); - // restore cache. gl.bindTexture(gl.TEXTURE_2D, tex._glTextures[this.renderer.CONTEXT_UID].texture); } /** + * Uploads the uniforms of the filter. + * + * @param {GLShader} shader - The underlying gl shader. + * @param {PIXI.Filter} filter - The filter we are synchronizing. + */ + syncUniforms(shader, filter) + { + const uniformData = filter.uniformData; + const uniforms = filter.uniforms; + + // 0 is reserved for the pixi texture so we start at 1! + let textureCount = 1; + let currentState; + + // filterArea and filterClamp that are handled by FilterManager directly + // they must not appear in uniformData + + if (shader.uniforms.filterArea) + { + currentState = this.filterData.stack[this.filterData.index]; + + const filterArea = shader.uniforms.filterArea; + + filterArea[0] = currentState.renderTarget.size.width; + filterArea[1] = currentState.renderTarget.size.height; + filterArea[2] = currentState.sourceFrame.x; + filterArea[3] = currentState.sourceFrame.y; + + shader.uniforms.filterArea = filterArea; + } + + // use this to clamp displaced texture coords so they belong to filterArea + // see displacementFilter fragment shader for an example + if (shader.uniforms.filterClamp) + { + currentState = currentState || this.filterData.stack[this.filterData.index]; + + const filterClamp = shader.uniforms.filterClamp; + + filterClamp[0] = 0; + filterClamp[1] = 0; + filterClamp[2] = (currentState.sourceFrame.width - 1) / currentState.renderTarget.size.width; + filterClamp[3] = (currentState.sourceFrame.height - 1) / currentState.renderTarget.size.height; + + shader.uniforms.filterClamp = filterClamp; + } + + // TODO Cacheing layer.. + for (const i in uniformData) + { + if (uniformData[i].type === 'sampler2D' && uniforms[i] !== 0) + { + if (uniforms[i].baseTexture) + { + shader.uniforms[i] = this.renderer.bindTexture(uniforms[i].baseTexture, textureCount); + } + else + { + shader.uniforms[i] = textureCount; + + // TODO + // this is helpful as renderTargets can also be set. + // Although thinking about it, we could probably + // make the filter texture cache return a RenderTexture + // rather than a renderTarget + const gl = this.renderer.gl; + + this.renderer.boundTextures[textureCount] = this.renderer.emptyTextures[textureCount]; + gl.activeTexture(gl.TEXTURE0 + textureCount); + + uniforms[i].texture.bind(); + } + + textureCount++; + } + else if (uniformData[i].type === 'mat3') + { + // check if its pixi matrix.. + if (uniforms[i].a !== undefined) + { + shader.uniforms[i] = uniforms[i].toArray(true); + } + else + { + shader.uniforms[i] = uniforms[i]; + } + } + else if (uniformData[i].type === 'vec2') + { + // check if its a point.. + if (uniforms[i].x !== undefined) + { + const val = shader.uniforms[i] || new Float32Array(2); + + val[0] = uniforms[i].x; + val[1] = uniforms[i].y; + shader.uniforms[i] = val; + } + else + { + shader.uniforms[i] = uniforms[i]; + } + } + else if (uniformData[i].type === 'float') + { + if (shader.uniforms.data[i].value !== uniformData[i]) + { + shader.uniforms[i] = uniforms[i]; + } + } + else + { + shader.uniforms[i] = uniforms[i]; + } + } + } + + /** * Gets a render target from the pool, or creates a new one. * * @param {boolean} clear - Should we clear the render texture when we get it? @@ -442,7 +541,6 @@ renderTarget.resolution = resolution; renderTarget.defaultFrame.width = renderTarget.size.width = minWidth / resolution; renderTarget.defaultFrame.height = renderTarget.size.height = minHeight / resolution; - renderTarget.attachStencilBuffer(); return renderTarget; } diff --git a/src/core/settings.js b/src/core/settings.js index bb08cb0..f4dd087 100644 --- a/src/core/settings.js +++ b/src/core/settings.js @@ -77,11 +77,11 @@ * * @static * @memberof PIXI.settings - * @type {RegExp|string} + * @type {RegExp} * @example `@2x` - * @default /@(.+)x/ + * @default /@([0-9\.]+)x/ */ - RETINA_PREFIX: /@(.+)x/, + RETINA_PREFIX: /@([0-9\.]+)x/, /** * The default render options if none are supplied to {@link PIXI.WebGLRenderer} @@ -175,14 +175,24 @@ SCALE_MODE: 0, /** - * Default specify float precision in shaders. + * Default specify float precision in vertex shader. + * + * @static + * @memberof PIXI.settings + * @type {PIXI.PRECISION} + * @default PIXI.PRECISION.HIGH + */ + PRECISION_VERTEX: 'highp', + + /** + * Default specify float precision in fragment shader. * * @static * @memberof PIXI.settings * @type {PIXI.PRECISION} * @default PIXI.PRECISION.MEDIUM */ - PRECISION: 'mediump', + PRECISION_FRAGMENT: 'mediump', /** * Can we upload the same buffer in a single frame? diff --git a/src/core/sprites/Sprite.js b/src/core/sprites/Sprite.js index 28b0b18..121bf58 100644 --- a/src/core/sprites/Sprite.js +++ b/src/core/sprites/Sprite.js @@ -212,11 +212,11 @@ } else { - w0 = orig.width * (1 - anchor._x); - w1 = orig.width * -anchor._x; + w1 = -anchor._x * orig.width; + w0 = w1 + orig.width; - h0 = orig.height * (1 - anchor._y); - h1 = orig.height * -anchor._y; + h1 = -anchor._y * orig.height; + h0 = h1 + orig.height; } // xy @@ -269,11 +269,11 @@ const tx = wt.tx; const ty = wt.ty; - const w0 = (orig.width) * (1 - anchor._x); - const w1 = (orig.width) * -anchor._x; + const w1 = -anchor._x * orig.width; + const w0 = w1 + orig.width; - const h0 = orig.height * (1 - anchor._y); - const h1 = orig.height * -anchor._y; + const h1 = -anchor._y * orig.height; + const h0 = h1 + orig.height; // xy vertexData[0] = (a * w1) + (c * h1) + tx; @@ -440,7 +440,7 @@ * * @static * @param {number|string|PIXI.BaseTexture|HTMLCanvasElement|HTMLVideoElement} source Source to create texture from - * @return {PIXI.Sprite} The newly created texture + * @return {PIXI.Sprite} The newly created sprite */ static from(source) { @@ -538,8 +538,8 @@ } /** - * The tint applied to the sprite. This is a hex value. A value of - * 0xFFFFFF will remove any tint effect. + * The tint applied to the sprite. This is a hex value. + * A value of 0xFFFFFF will remove any tint effect. * * @member {number} * @default 0xFFFFFF diff --git a/src/core/sprites/canvas/CanvasTinter.js b/src/core/sprites/canvas/CanvasTinter.js index 1cf316b..27e5e0c 100644 --- a/src/core/sprites/canvas/CanvasTinter.js +++ b/src/core/sprites/canvas/CanvasTinter.js @@ -75,8 +75,8 @@ crop.width *= resolution; crop.height *= resolution; - canvas.width = crop.width; - canvas.height = crop.height; + canvas.width = Math.ceil(crop.width); + canvas.height = Math.ceil(crop.height); context.fillStyle = `#${(`00000${(color | 0).toString(16)}`).substr(-6)}`; @@ -130,8 +130,8 @@ crop.width *= resolution; crop.height *= resolution; - canvas.width = crop.width; - canvas.height = crop.height; + canvas.width = Math.ceil(crop.width); + canvas.height = Math.ceil(crop.height); context.globalCompositeOperation = 'copy'; context.fillStyle = `#${(`00000${(color | 0).toString(16)}`).substr(-6)}`; @@ -172,8 +172,8 @@ crop.width *= resolution; crop.height *= resolution; - canvas.width = crop.width; - canvas.height = crop.height; + canvas.width = Math.ceil(crop.width); + canvas.height = Math.ceil(crop.height); context.globalCompositeOperation = 'copy'; context.drawImage( diff --git a/src/core/sprites/webgl/SpriteRenderer.js b/src/core/sprites/webgl/SpriteRenderer.js index 2affcd5..7c352e2 100644 --- a/src/core/sprites/webgl/SpriteRenderer.js +++ b/src/core/sprites/webgl/SpriteRenderer.js @@ -30,7 +30,7 @@ /** * Number of values sent in the vertex buffer. - * positionX, positionY, colorR, colorG, colorB = 5 + * aVertexPosition(2), aTextureCoord(1), aColor(1), aTextureId(1) = 5 * * @member {number} */ @@ -75,7 +75,6 @@ this.shader = null; this.currentIndex = 0; - TICK = 0; this.groups = []; for (let k = 0; k < this.size; k++) diff --git a/src/core/textures/BaseTexture.js b/src/core/textures/BaseTexture.js index 853de7e..aeccf39 100644 --- a/src/core/textures/BaseTexture.js +++ b/src/core/textures/BaseTexture.js @@ -232,16 +232,24 @@ this.realWidth = this.source.naturalWidth || this.source.videoWidth || this.source.width; this.realHeight = this.source.naturalHeight || this.source.videoHeight || this.source.height; - this.width = this.realWidth / this.resolution; - this.height = this.realHeight / this.resolution; - - this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); + this._updateDimensions(); } this.emit('update', this); } /** + * Update dimensions from real values + */ + _updateDimensions() + { + this.width = this.realWidth / this.resolution; + this.height = this.realHeight / this.resolution; + + this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); + } + + /** * Load a source. * * If the source is not-immediately-available, such as an image that needs to be @@ -525,11 +533,7 @@ this.realWidth = Math.round(svgWidth * this.sourceScale); this.realHeight = Math.round(svgHeight * this.sourceScale); - this.width = this.realWidth / this.resolution; - this.height = this.realHeight / this.resolution; - - // Check pow2 after scale - this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); + this._updateDimensions(); // Create a canvas element const canvas = document.createElement('canvas'); @@ -690,4 +694,52 @@ return baseTexture; } + + /** + * Helper function that creates a base texture based on the source you provide. + * The source can be - image url, image element, canvas element. + * + * @static + * @param {string|HTMLImageElement|HTMLCanvasElement} source - The source to create base texture from. + * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values + * @param {number} [sourceScale=(auto)] - Scale for the original image, used with Svg images. + * @return {PIXI.BaseTexture} The new base texture. + */ + static from(source, scaleMode, sourceScale) + { + if (typeof source === 'string') + { + return BaseTexture.fromImage(source, undefined, scaleMode, sourceScale); + } + else if (source instanceof HTMLImageElement) + { + const imageUrl = source.src; + let baseTexture = BaseTextureCache[imageUrl]; + + if (!baseTexture) + { + baseTexture = new BaseTexture(source, scaleMode); + baseTexture.imageUrl = imageUrl; + + if (sourceScale) + { + baseTexture.sourceScale = sourceScale; + } + + // if there is an @2x at the end of the url we are going to assume its a highres image + baseTexture.resolution = getResolutionOfUrl(imageUrl); + + BaseTextureCache[imageUrl] = baseTexture; + } + + return baseTexture; + } + else if (source instanceof HTMLCanvasElement) + { + return BaseTexture.fromCanvas(source, scaleMode); + } + + // lets assume its a base texture! + return source; + } } diff --git a/src/core/textures/Spritesheet.js b/src/core/textures/Spritesheet.js new file mode 100644 index 0000000..d469c62 --- /dev/null +++ b/src/core/textures/Spritesheet.js @@ -0,0 +1,274 @@ +import { Rectangle, Texture } from '../'; +import { getResolutionOfUrl, TextureCache } from '../utils'; + +/** + * Utility class for maintaining reference to a collection + * of Textures on a single Spritesheet. + * + * @class + * @memberof PIXI + */ +export default class Spritesheet +{ + /** + * The maximum number of Textures to build per process. + * + * @type {number} + * @default 1000 + */ + static get BATCH_SIZE() + { + return 1000; + } + + /** + * @param {PIXI.BaseTexture} baseTexture Reference to the source BaseTexture object. + * @param {Object} data - Spritesheet image data. + * @param {string} [resolutionFilename] - The filename to consider when determining + * the resolution of the spritesheet. If not provided, the imageUrl will + * be used on the BaseTexture. + */ + constructor(baseTexture, data, resolutionFilename = null) + { + /** + * Reference to ths source texture + * @type {PIXI.BaseTexture} + */ + this.baseTexture = baseTexture; + + /** + * Map of spritesheet textures. + * @type {Object} + */ + this.textures = {}; + + /** + * Reference to the original JSON data. + * @type {Object} + */ + this.data = data; + + /** + * The resolution of the spritesheet. + * @type {number} + */ + this.resolution = this._updateResolution( + resolutionFilename || this.baseTexture.imageUrl + ); + + /** + * Map of spritesheet frames. + * @type {Object} + * @private + */ + this._frames = this.data.frames; + + /** + * Collection of frame names. + * @type {string[]} + * @private + */ + this._frameKeys = Object.keys(this._frames); + + /** + * Current batch index being processed. + * @type {number} + * @private + */ + this._batchIndex = 0; + + /** + * Callback when parse is completed. + * @type {Function} + * @private + */ + this._callback = null; + } + + /** + * Generate the resolution from the filename or fallback + * to the meta.scale field of the JSON data. + * + * @private + * @param {string} resolutionFilename - The filename to use for resolving + * the default resolution. + * @return {number} Resolution to use for spritesheet. + */ + _updateResolution(resolutionFilename) + { + const scale = this.data.meta.scale; + + // Use a defaultValue of `null` to check if a url-based resolution is set + let resolution = getResolutionOfUrl(resolutionFilename, null); + + // No resolution found via URL + if (resolution === null) + { + // Use the scale value or default to 1 + resolution = scale !== undefined ? parseFloat(scale) : 1; + } + + // For non-1 resolutions, update baseTexture + if (resolution !== 1) + { + this.baseTexture.resolution = resolution; + this.baseTexture.update(); + } + + return resolution; + } + + /** + * Parser spritesheet from loaded data. This is done asynchronously + * to prevent creating too many Texture within a single process. + * + * @param {Function} callback - Callback when complete returns + * a map of the Textures for this spritesheet. + */ + parse(callback) + { + this._batchIndex = 0; + this._callback = callback; + + if (this._frameKeys.length <= Spritesheet.BATCH_SIZE) + { + this._processFrames(0); + this._parseComplete(); + } + else + { + this._nextBatch(); + } + } + + /** + * Process a batch of frames + * + * @private + * @param {number} initialFrameIndex - The index of frame to start. + */ + _processFrames(initialFrameIndex) + { + let frameIndex = initialFrameIndex; + const maxFrames = Spritesheet.BATCH_SIZE; + + while (frameIndex - initialFrameIndex < maxFrames && frameIndex < this._frameKeys.length) + { + const i = this._frameKeys[frameIndex]; + const rect = this._frames[i].frame; + + if (rect) + { + let frame = null; + let trim = null; + const orig = new Rectangle( + 0, + 0, + this._frames[i].sourceSize.w / this.resolution, + this._frames[i].sourceSize.h / this.resolution + ); + + if (this._frames[i].rotated) + { + frame = new Rectangle( + rect.x / this.resolution, + rect.y / this.resolution, + rect.h / this.resolution, + rect.w / this.resolution + ); + } + else + { + frame = new Rectangle( + rect.x / this.resolution, + rect.y / this.resolution, + rect.w / this.resolution, + rect.h / this.resolution + ); + } + + // Check to see if the sprite is trimmed + if (this._frames[i].trimmed) + { + trim = new Rectangle( + this._frames[i].spriteSourceSize.x / this.resolution, + this._frames[i].spriteSourceSize.y / this.resolution, + rect.w / this.resolution, + rect.h / this.resolution + ); + } + + this.textures[i] = new Texture( + this.baseTexture, + frame, + orig, + trim, + this._frames[i].rotated ? 2 : 0 + ); + + // lets also add the frame to pixi's global cache for fromFrame and fromImage functions + TextureCache[i] = this.textures[i]; + } + + frameIndex++; + } + } + + /** + * The parse has completed. + * + * @private + */ + _parseComplete() + { + const callback = this._callback; + + this._callback = null; + this._batchIndex = 0; + callback.call(this, this.textures); + } + + /** + * Begin the next batch of textures. + * + * @private + */ + _nextBatch() + { + this._processFrames(this._batchIndex * Spritesheet.BATCH_SIZE); + this._batchIndex++; + setTimeout(() => + { + if (this._batchIndex * Spritesheet.BATCH_SIZE < this._frameKeys.length) + { + this._nextBatch(); + } + else + { + this._parseComplete(); + } + }, 0); + } + + /** + * Destroy Spritesheet and don't use after this. + * + * @param {boolean} [destroyBase=false] Whether to destroy the base texture as well + */ + destroy(destroyBase = false) + { + for (const i in this.textures) + { + this.textures[i].destroy(); + } + this._frames = null; + this._frameKeys = null; + this.data = null; + this.textures = null; + if (destroyBase) + { + this.baseTexture.destroy(); + } + this.baseTexture = null; + } +} diff --git a/package.json b/package.json index 6a366ba..71ade15 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pixi.js", - "version": "4.3.4", + "version": "4.3.5", "description": "Pixi.js is a fast lightweight 2D library that works across all devices.", "author": "Mat Groves", "contributors": [ @@ -61,7 +61,7 @@ "ismobilejs": "^0.4.0", "object-assign": "^4.0.1", "pixi-gl-core": "^1.0.3", - "resource-loader": "^2.0.4" + "resource-loader": "^2.0.6" }, "devDependencies": { "babel-cli": "^6.18.0", @@ -70,9 +70,9 @@ "babel-preset-es2015": "^6.14.0", "babelify": "^7.3.0", "del": "^2.2.0", - "electron-prebuilt": "^1.3.2", + "electron": "^1.4.15", "eslint": "^3.5.0", - "floss": "^1.2.0", + "floss": "^2.0.1", "gh-pages": "^0.11.0", "jaguarjs-jsdoc": "^1.0.1", "js-md5": "^0.4.1", diff --git a/src/accessibility/AccessibilityManager.js b/src/accessibility/AccessibilityManager.js index df2716a..f16bcb1 100644 --- a/src/accessibility/AccessibilityManager.js +++ b/src/accessibility/AccessibilityManager.js @@ -3,7 +3,7 @@ import accessibleTarget from './accessibleTarget'; // add some extra variables to the container.. -Object.assign( +core.utils.mixins.delayMixin( core.DisplayObject.prototype, accessibleTarget ); diff --git a/src/core/Application.js b/src/core/Application.js index ea0f058..5340578 100644 --- a/src/core/Application.js +++ b/src/core/Application.js @@ -1,6 +1,6 @@ import { autoDetectRenderer } from './autoDetectRenderer'; import Container from './display/Container'; -import Ticker from './ticker/Ticker'; +import { shared, Ticker } from './ticker'; /** * Convenience class to create a new PIXI application. @@ -35,9 +35,9 @@ * @param {boolean} [noWebGL=false] - prevents selection of WebGL renderer, even if such is present * @param {boolean} [options.legacy=false] - If true Pixi will aim to ensure compatibility * with older / less advanced devices. If you experience unexplained flickering try setting this to true. - + * @param {boolean} [useSharedTicker=false] - `true` to use PIXI.ticker.shared, `false` to create new ticker. */ - constructor(width, height, options, noWebGL) + constructor(width, height, options, noWebGL, useSharedTicker = false) { /** * WebGL renderer if available, otherwise CanvasRenderer @@ -46,23 +46,46 @@ this.renderer = autoDetectRenderer(width, height, options, noWebGL); /** - * The root display container that's renderered. + * The root display container that's rendered. * @member {PIXI.Container} */ this.stage = new Container(); /** + * Internal reference to the ticker + * @member {PIXI.ticker.Ticker} + * @private + */ + this._ticker = null; + + /** * Ticker for doing render updates. * @member {PIXI.ticker.Ticker} + * @default PIXI.ticker.shared */ - this.ticker = new Ticker(); - - this.ticker.add(this.render, this); + this.ticker = useSharedTicker ? shared : new Ticker(); // Start the rendering this.start(); } + set ticker(ticker) // eslint-disable-line require-jsdoc + { + if (this._ticker) + { + this._ticker.remove(this.render, this); + } + this._ticker = ticker; + if (ticker) + { + ticker.add(this.render, this); + } + } + get ticker() // eslint-disable-line require-jsdoc + { + return this._ticker; + } + /** * Render the current stage. */ @@ -76,7 +99,7 @@ */ stop() { - this.ticker.stop(); + this._ticker.stop(); } /** @@ -84,7 +107,7 @@ */ start() { - this.ticker.start(); + this._ticker.start(); } /** @@ -98,13 +121,22 @@ } /** + * Reference to the renderer's screen rectangle. Its safe to use as filterArea or hitArea for whole screen + * @member {PIXI.Rectangle} + * @readonly + */ + get screen() + { + return this.renderer.screen; + } + + /** * Destroy and don't use after this. * @param {Boolean} [removeView=false] Automatically remove canvas from DOM. */ destroy(removeView) { this.stop(); - this.ticker.remove(this.render, this); this.ticker = null; this.stage.destroy(); diff --git a/src/core/Shader.js b/src/core/Shader.js new file mode 100644 index 0000000..78b17e6 --- /dev/null +++ b/src/core/Shader.js @@ -0,0 +1,46 @@ +import { GLShader } from 'pixi-gl-core'; +import settings from './settings'; + +function checkPrecision(src, def) +{ + if (src instanceof Array) + { + if (src[0].substring(0, 9) !== 'precision') + { + const copy = src.slice(0); + + copy.unshift(`precision ${def} float;`); + + return copy; + } + } + else if (src.substring(0, 9) !== 'precision') + { + return `precision ${def} float;\n${src}`; + } + + return src; +} + +/** + * Wrapper class, webGL Shader for Pixi. + * Adds precision string if vertexSrc or fragmentSrc have no mention of it. + * + * @class + * @extends GLShader + * @memberof PIXI + */ +export default class Shader extends GLShader +{ + /** + * + * @param {WebGLRenderingContext} gl - The current WebGL rendering context + * @param {string|string[]} vertexSrc - The vertex shader source as an array of strings. + * @param {string|string[]} fragmentSrc - The fragment shader source as an array of strings. + */ + constructor(gl, vertexSrc, fragmentSrc) + { + super(gl, checkPrecision(vertexSrc, settings.PRECISION_VERTEX), + checkPrecision(fragmentSrc, settings.PRECISION_FRAGMENT)); + } +} diff --git a/src/core/const.js b/src/core/const.js index ef6f662..3de0efe 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -188,7 +188,7 @@ * The gc modes that are supported by pixi. * * The {@link PIXI.settings.GC_MODE} Garbage Collection mode for pixi textures is AUTO - * If set to GC_MODE, the renderer will occasianally check textures usage. If they are not + * If set to GC_MODE, the renderer will occasionally check textures usage. If they are not * used for a specified period of time they will be removed from the GPU. They will of course * be uploaded again when they are required. This is a silent behind the scenes process that * should ensure that the GPU does not get filled up. diff --git a/src/core/display/Container.js b/src/core/display/Container.js index ed8cd5f..17fde8d 100644 --- a/src/core/display/Container.js +++ b/src/core/display/Container.js @@ -73,11 +73,12 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.push(child); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -108,11 +109,12 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.splice(index, 0, child); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -225,10 +227,11 @@ if (index === -1) return null; child.parent = null; + // ensure child transform will be recalculated + child.transform._parentID = -1; removeItems(this.children, index, 1); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -249,11 +252,12 @@ { const child = this.getChildAt(index); + // ensure child transform will be recalculated.. child.parent = null; + child.transform._parentID = -1; removeItems(this.children, index, 1); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -284,13 +288,12 @@ for (let i = 0; i < removed.length; ++i) { removed[i].parent = null; + if (removed[i].transform) + { + removed[i].transform._parentID = -1; + } } - // ensure a transform will be recalculated.. - if (this.transform) - { - this.transform._parentID = -1; - } this._boundsID++; this.onChildrenChange(beginIndex); diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index f37f2f5..be4c4fb 100644 --- a/src/core/graphics/Graphics.js +++ b/src/core/graphics/Graphics.js @@ -28,8 +28,9 @@ { /** * + * @param {boolean} [nativeLines=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP */ - constructor() + constructor(nativeLines = false) { super(); @@ -50,6 +51,13 @@ this.lineWidth = 0; /** + * If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * + * @member {boolean} + */ + this.nativeLines = nativeLines; + + /** * The color of any lines drawn. * * @member {string} @@ -757,28 +765,14 @@ if (!this._spriteRect) { - if (!Graphics._SPRITE_TEXTURE) - { - Graphics._SPRITE_TEXTURE = RenderTexture.create(10, 10); - - const canvas = document.createElement('canvas'); - - canvas.width = 10; - canvas.height = 10; - - const context = canvas.getContext('2d'); - - context.fillStyle = 'white'; - context.fillRect(0, 0, 10, 10); - - Graphics._SPRITE_TEXTURE = Texture.fromCanvas(canvas); - } - - this._spriteRect = new Sprite(Graphics._SPRITE_TEXTURE); + this._spriteRect = new Sprite(new Texture(Texture.WHITE)); } + + const sprite = this._spriteRect; + if (this.tint === 0xffffff) { - this._spriteRect.tint = this.graphicsData[0].fillColor; + sprite.tint = this.graphicsData[0].fillColor; } else { @@ -792,20 +786,21 @@ t1[1] *= t2[1]; t1[2] *= t2[2]; - this._spriteRect.tint = rgb2hex(t1); + sprite.tint = rgb2hex(t1); } - this._spriteRect.alpha = this.graphicsData[0].fillAlpha; - this._spriteRect.worldAlpha = this.worldAlpha * this._spriteRect.alpha; + sprite.alpha = this.graphicsData[0].fillAlpha; + sprite.worldAlpha = this.worldAlpha * sprite.alpha; + sprite.blendMode = this.blendMode; - Graphics._SPRITE_TEXTURE._frame.width = rect.width; - Graphics._SPRITE_TEXTURE._frame.height = rect.height; + sprite.texture._frame.width = rect.width; + sprite.texture._frame.height = rect.height; - this._spriteRect.transform.worldTransform = this.transform.worldTransform; + sprite.transform.worldTransform = this.transform.worldTransform; - this._spriteRect.anchor.set(-rect.x / rect.width, -rect.y / rect.height); - this._spriteRect._onAnchorUpdate(); + sprite.anchor.set(-rect.x / rect.width, -rect.y / rect.height); + sprite._onAnchorUpdate(); - this._spriteRect._renderWebGL(renderer); + sprite._renderWebGL(renderer); } /** @@ -1031,6 +1026,7 @@ this.fillColor, this.fillAlpha, this.filling, + this.nativeLines, shape ); diff --git a/src/core/graphics/GraphicsData.js b/src/core/graphics/GraphicsData.js index f5a87ae..b6f03c0 100644 --- a/src/core/graphics/GraphicsData.js +++ b/src/core/graphics/GraphicsData.js @@ -14,14 +14,19 @@ * @param {number} fillColor - the color of the fill * @param {number} fillAlpha - the alpha of the fill * @param {boolean} fill - whether or not the shape is filled with a colour + * @param {boolean} nativeLines - the method for drawing lines * @param {PIXI.Circle|PIXI.Rectangle|PIXI.Ellipse|PIXI.Polygon} shape - The shape object to draw. */ - constructor(lineWidth, lineColor, lineAlpha, fillColor, fillAlpha, fill, shape) + constructor(lineWidth, lineColor, lineAlpha, fillColor, fillAlpha, fill, nativeLines, shape) { /** * @member {number} the width of the line to draw */ this.lineWidth = lineWidth; + /** + * @member {boolean} if true the liens will be draw using LINES instead of TRIANGLE_STRIP + */ + this.nativeLines = nativeLines; /** * @member {number} the color of the line to draw @@ -85,6 +90,7 @@ this.fillColor, this.fillAlpha, this.fill, + this.nativeLines, this.shape ); } diff --git a/src/core/graphics/webgl/GraphicsRenderer.js b/src/core/graphics/webgl/GraphicsRenderer.js index 14083fe..67f5e92 100644 --- a/src/core/graphics/webgl/GraphicsRenderer.js +++ b/src/core/graphics/webgl/GraphicsRenderer.js @@ -102,7 +102,15 @@ shaderTemp.uniforms.alpha = graphics.worldAlpha; renderer.bindVao(webGLData.vao); - webGLData.vao.draw(gl.TRIANGLE_STRIP, webGLData.indices.length); + + if (graphics.nativeLines) + { + gl.drawArrays(gl.LINES, 0, webGLData.points.length / 6); + } + else + { + webGLData.vao.draw(gl.TRIANGLE_STRIP, webGLData.indices.length); + } } } diff --git a/src/core/graphics/webgl/utils/buildLine.js b/src/core/graphics/webgl/utils/buildLine.js index ecf43f7..4d6862e 100644 --- a/src/core/graphics/webgl/utils/buildLine.js +++ b/src/core/graphics/webgl/utils/buildLine.js @@ -11,7 +11,29 @@ * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties * @param {object} webGLData - an object containing all the webGL-specific information to create this shape */ -export default function buildLine(graphicsData, webGLData) +export default function (graphicsData, webGLData) +{ + if (graphicsData.nativeLines) + { + buildNativeLine(graphicsData, webGLData); + } + else + { + buildLine(graphicsData, webGLData); + } +} + +/** + * Builds a line to draw using the poligon method. + * + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @private + * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties + * @param {object} webGLData - an object containing all the webGL-specific information to create this shape + */ +function buildLine(graphicsData, webGLData) { // TODO OPTIMISE! let points = graphicsData.points; @@ -224,3 +246,46 @@ indices.push(indexStart - 1); } + +/** + * Builds a line to draw using the gl.drawArrays(gl.LINES) method + * + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @private + * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties + * @param {object} webGLData - an object containing all the webGL-specific information to create this shape + */ +function buildNativeLine(graphicsData, webGLData) +{ + let i = 0; + const points = graphicsData.points; + + if (points.length === 0) return; + + const verts = webGLData.points; + const length = points.length / 2; + + // sort color + const color = hex2rgb(graphicsData.lineColor); + const alpha = graphicsData.lineAlpha; + const r = color[0] * alpha; + const g = color[1] * alpha; + const b = color[2] * alpha; + + for (i = 1; i < length; i++) + { + const p1x = points[(i - 1) * 2]; + const p1y = points[((i - 1) * 2) + 1]; + + const p2x = points[i * 2]; + const p2y = points[(i * 2) + 1]; + + verts.push(p1x, p1y); + verts.push(r, g, b, alpha); + + verts.push(p2x, p2y); + verts.push(r, g, b, alpha); + } +} diff --git a/src/core/graphics/webgl/utils/buildRoundedRectangle.js b/src/core/graphics/webgl/utils/buildRoundedRectangle.js index 9abc807..a6c8461 100644 --- a/src/core/graphics/webgl/utils/buildRoundedRectangle.js +++ b/src/core/graphics/webgl/utils/buildRoundedRectangle.js @@ -77,6 +77,26 @@ } /** + * Calculate a single point for a quadratic bezier curve. + * Utility function used by quadraticBezierCurve. + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @private + * @param {number} n1 - first number + * @param {number} n2 - second number + * @param {number} perc - percentage + * @return {number} the result + * + */ +function getPt(n1, n2, perc) +{ + const diff = n2 - n1; + + return n1 + (diff * perc); +} + +/** * Calculate the points for a quadratic bezier curve. (helper function..) * Based on: https://stackoverflow.com/questions/785097/how-do-i-implement-a-bezier-curve-in-c * @@ -105,13 +125,6 @@ let x = 0; let y = 0; - function getPt(n1, n2, perc) - { - const diff = n2 - n1; - - return n1 + (diff * perc); - } - for (let i = 0, j = 0; i <= n; ++i) { j = i / n; diff --git a/src/core/index.js b/src/core/index.js index f7a817c..4f414e8 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -30,6 +30,7 @@ export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; export { default as CanvasGraphicsRenderer } from './graphics/canvas/CanvasGraphicsRenderer'; +export { default as Spritesheet } from './textures/Spritesheet'; export { default as Texture } from './textures/Texture'; export { default as BaseTexture } from './textures/BaseTexture'; export { default as RenderTexture } from './textures/RenderTexture'; diff --git a/src/core/math/Matrix.js b/src/core/math/Matrix.js index 0b77477..3679860 100644 --- a/src/core/math/Matrix.js +++ b/src/core/math/Matrix.js @@ -13,45 +13,50 @@ export default class Matrix { /** - * + * @param {number} [a=1] - x scale + * @param {number} [b=0] - y skew + * @param {number} [c=0] - x skew + * @param {number} [d=1] - y scale + * @param {number} [tx=0] - x translation + * @param {number} [ty=0] - y translation */ - constructor() + constructor(a = 1, b = 0, c = 0, d = 1, tx = 0, ty = 0) { /** * @member {number} * @default 1 */ - this.a = 1; + this.a = a; /** * @member {number} * @default 0 */ - this.b = 0; + this.b = b; /** * @member {number} * @default 0 */ - this.c = 0; + this.c = c; /** * @member {number} * @default 1 */ - this.d = 1; + this.d = d; /** * @member {number} * @default 0 */ - this.tx = 0; + this.tx = tx; /** * @member {number} * @default 0 */ - this.ty = 0; + this.ty = ty; this.array = null; } diff --git a/src/core/renderers/SystemRenderer.js b/src/core/renderers/SystemRenderer.js index fa116a6..a24c775 100644 --- a/src/core/renderers/SystemRenderer.js +++ b/src/core/renderers/SystemRenderer.js @@ -1,5 +1,5 @@ import { sayHello, hex2string, hex2rgb } from '../utils'; -import { Matrix } from '../math'; +import { Matrix, Rectangle } from '../math'; import { RENDERER_TYPE } from '../const'; import settings from '../settings'; import Container from '../display/Container'; @@ -21,8 +21,8 @@ { /** * @param {string} system - The name of the system this renderer is for. - * @param {number} [width=800] - the width of the canvas view - * @param {number} [height=600] - the height of the canvas view + * @param {number} [screenWidth=800] - the width of the screen + * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false @@ -37,7 +37,7 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(system, width, height, options) + constructor(system, screenWidth, screenHeight, options) { super(); @@ -69,20 +69,13 @@ this.type = RENDERER_TYPE.UNKNOWN; /** - * The width of the canvas view + * Measurements of the screen. (0, 0, screenWidth, screenHeight) * - * @member {number} - * @default 800 - */ - this.width = width || 800; - - /** - * The height of the canvas view + * Its safe to use as filterArea or hitArea for whole stage * - * @member {number} - * @default 600 + * @member {PIXI.Rectangle} */ - this.height = height || 600; + this.screen = new Rectangle(0, 0, screenWidth || 800, screenHeight || 600); /** * The canvas element that everything is drawn to @@ -107,7 +100,7 @@ this.transparent = options.transparent; /** - * Whether the render view should be resized automatically + * Whether css dimensions of canvas view should be resized to screen dimensions automatically * * @member {boolean} */ @@ -192,23 +185,48 @@ } /** - * Resizes the canvas view to the specified width and height + * Same as view.width, actual number of pixels in the canvas by horizontal * - * @param {number} width - the new width of the canvas view - * @param {number} height - the new height of the canvas view + * @member {number} + * @readonly + * @default 800 */ - resize(width, height) + get width() { - this.width = width * this.resolution; - this.height = height * this.resolution; + return this.view.width; + } - this.view.width = this.width; - this.view.height = this.height; + /** + * Same as view.height, actual number of pixels in the canvas by vertical + * + * @member {number} + * @readonly + * @default 600 + */ + get height() + { + return this.view.height; + } + + /** + * Resizes the screen and canvas to the specified width and height + * Canvas dimensions are multiplied by resolution + * + * @param {number} screenWidth - the new width of the screen + * @param {number} screenHeight - the new height of the screen + */ + resize(screenWidth, screenHeight) + { + this.screen.width = screenWidth; + this.screen.height = screenHeight; + + this.view.width = screenWidth * this.resolution; + this.view.height = screenHeight * this.resolution; if (this.autoResize) { - this.view.style.width = `${this.width / this.resolution}px`; - this.view.style.height = `${this.height / this.resolution}px`; + this.view.style.width = `${screenWidth}px`; + this.view.style.height = `${screenHeight}px`; } } @@ -249,11 +267,10 @@ this.type = RENDERER_TYPE.UNKNOWN; - this.width = 0; - this.height = 0; - this.view = null; + this.screen = null; + this.resolution = 0; this.transparent = false; diff --git a/src/core/renderers/canvas/CanvasRenderer.js b/src/core/renderers/canvas/CanvasRenderer.js index 1bd2d5e..e84e49c 100644 --- a/src/core/renderers/canvas/CanvasRenderer.js +++ b/src/core/renderers/canvas/CanvasRenderer.js @@ -18,8 +18,8 @@ export default class CanvasRenderer extends SystemRenderer { /** - * @param {number} [width=800] - the width of the canvas view - * @param {number} [height=600] - the height of the canvas view + * @param {number} [screenWidth=800] - the width of the screen + * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false @@ -34,9 +34,9 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(width, height, options = {}) + constructor(screenWidth, screenHeight, options = {}) { - super('Canvas', width, height, options); + super('Canvas', screenWidth, screenHeight, options); this.type = RENDERER_TYPE.CANVAS; @@ -96,7 +96,7 @@ this.context = null; this.renderingToScreen = false; - this.resize(width, height); + this.resize(screenWidth, screenHeight); } /** @@ -283,12 +283,12 @@ * * @extends PIXI.SystemRenderer#resize * - * @param {number} width - The new width of the canvas view - * @param {number} height - The new height of the canvas view + * @param {number} screenWidth - the new width of the screen + * @param {number} screenHeight - the new height of the screen */ - resize(width, height) + resize(screenWidth, screenHeight) { - super.resize(width, height); + super.resize(screenWidth, screenHeight); // reset the scale mode.. oddly this seems to be reset when the canvas is resized. // surely a browser bug?? Let pixi fix that for you.. diff --git a/src/core/renderers/webgl/TextureManager.js b/src/core/renderers/webgl/TextureManager.js index b685c1a..8227289 100644 --- a/src/core/renderers/webgl/TextureManager.js +++ b/src/core/renderers/webgl/TextureManager.js @@ -61,7 +61,7 @@ * Updates and/or Creates a WebGL texture for the renderer's context. * * @param {PIXI.BaseTexture|PIXI.Texture} texture - the texture to update - * @param {Number} location - the location the texture will be bound to. + * @param {number} location - the location the texture will be bound to. * @return {GLTexture} The gl texture. */ updateTexture(texture, location) diff --git a/src/core/renderers/webgl/WebGLRenderer.js b/src/core/renderers/webgl/WebGLRenderer.js index e4cd8a0..830b382 100644 --- a/src/core/renderers/webgl/WebGLRenderer.js +++ b/src/core/renderers/webgl/WebGLRenderer.js @@ -31,8 +31,8 @@ { /** * - * @param {number} [width=0] - the width of the canvas view - * @param {number} [height=0] - the height of the canvas view + * @param {number} [screenWidth=800] - the width of the screen + * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false @@ -53,9 +53,9 @@ * @param {boolean} [options.legacy=false] - If true Pixi will aim to ensure compatibility * with older / less advanced devices. If you experiance unexplained flickering try setting this to true. */ - constructor(width, height, options = {}) + constructor(screenWidth, screenHeight, options = {}) { - super('WebGL', width, height, options); + super('WebGL', screenWidth, screenHeight, options); this.legacy = !!options.legacy; @@ -245,7 +245,7 @@ glCore._testingContext = gl; // setup the width/height properties and gl viewport - this.resize(this.width, this.height); + this.resize(this.screen.width, this.screen.height); } /** @@ -343,16 +343,16 @@ /** * Resizes the webGL view to the specified width and height. * - * @param {number} width - the new width of the webGL view - * @param {number} height - the new height of the webGL view + * @param {number} screenWidth - the new width of the screen + * @param {number} screenHeight - the new height of the screen */ - resize(width, height) + resize(screenWidth, screenHeight) { // if(width * this.resolution === this.width && height * this.resolution === this.height)return; - SystemRenderer.prototype.resize.call(this, width, height); + SystemRenderer.prototype.resize.call(this, screenWidth, screenHeight); - this.rootRenderTarget.resize(width, height); + this.rootRenderTarget.resize(screenWidth, screenHeight); if (this._activeRenderTarget === this.rootRenderTarget) { @@ -396,6 +396,26 @@ } /** + * Erases the render texture and fills the drawing area with a colour + * + * @param {PIXI.RenderTexture} renderTexture - The render texture to clear + * @param {number} [clearColor] - The colour + * @return {PIXI.WebGLRenderer} Returns itself. + */ + clearRenderTexture(renderTexture, clearColor) + { + const baseTexture = renderTexture.baseTexture; + const renderTarget = baseTexture._glRenderTargets[this.CONTEXT_UID]; + + if (renderTarget) + { + renderTarget.clear(clearColor); + } + + return this; + } + + /** * Binds a render texture for rendering * * @param {PIXI.RenderTexture} renderTexture - The render texture to render @@ -474,9 +494,10 @@ * Changes the current GLShader to the one given in parameter * * @param {PIXI.glCore.Shader} shader - the new glShader + * @param {boolean} [autoProject=true] - Whether automatically set the projection matrix * @return {PIXI.WebGLRenderer} Returns itself. */ - _bindGLShader(shader) + _bindGLShader(shader, autoProject) { // TODO cache if (this._activeShader !== shader) @@ -484,8 +505,14 @@ this._activeShader = shader; shader.bind(); - // automatically set the projection matrix - shader.uniforms.projectionMatrix = this._activeRenderTarget.projectionMatrix.toArray(true); + // `autoProject` normally would be a default parameter set to true + // but because of how Babel transpiles default parameters + // it hinders the performance of this method. + if (autoProject !== false) + { + // automatically set the projection matrix + shader.uniforms.projectionMatrix = this._activeRenderTarget.projectionMatrix.toArray(true); + } } return this; diff --git a/src/core/renderers/webgl/WebGLState.js b/src/core/renderers/webgl/WebGLState.js index ba6ecc5..6c846d5 100755 --- a/src/core/renderers/webgl/WebGLState.js +++ b/src/core/renderers/webgl/WebGLState.js @@ -82,18 +82,20 @@ push() { // next state.. - let state = this.stack[++this.stackIndex]; + let state = this.stack[this.stackIndex]; if (!state) { state = this.stack[this.stackIndex] = new Uint8Array(16); } + ++this.stackIndex; + // copy state.. // set active state so we can force overrides of gl state for (let i = 0; i < this.activeState.length; i++) { - this.activeState[i] = state[i]; + state[i] = this.activeState[i]; } } diff --git a/src/core/renderers/webgl/filters/Filter.js b/src/core/renderers/webgl/filters/Filter.js index 2c3eaad..4eb6128 100644 --- a/src/core/renderers/webgl/filters/Filter.js +++ b/src/core/renderers/webgl/filters/Filter.js @@ -9,7 +9,7 @@ * @memberof PIXI * @extends PIXI.Shader */ -class Filter extends Shader +export default class Filter extends Shader { /** * @param {string} [vertexSrc] - The source of the vertex shader. @@ -56,15 +56,16 @@ * @param {PIXI.RenderTarget} input - The input render target. * @param {PIXI.RenderTarget} output - The target to output to. * @param {boolean} clear - Should the output be cleared before rendering to it + * @param {object} [currentState] - It's current state of filter. + * There are some useful properties in the currentState : + * target, filters, sourceFrame, destinationFrame, renderTarget, resolution */ - apply(filterManager, input, output, clear) + apply(filterManager, input, output, clear, currentState) // eslint-disable-line no-unused-vars { // do as you please! - filterManager.applyFilter(this, input, output, clear); + filterManager.applyFilter(this, input, output, clear, currentState); // or just do a regular render.. } } - -export default Filter; diff --git a/src/core/renderers/webgl/filters/extractUniformsFromSrc.js b/src/core/renderers/webgl/filters/extractUniformsFromSrc.js index d78a0b1..4893214 100644 --- a/src/core/renderers/webgl/filters/extractUniformsFromSrc.js +++ b/src/core/renderers/webgl/filters/extractUniformsFromSrc.js @@ -12,7 +12,7 @@ function extractUniformsFromString(string) { - const maskRegex = new RegExp('^(projectionMatrix|uSampler|filterArea)$'); + const maskRegex = new RegExp('^(projectionMatrix|uSampler|filterArea|filterClamp)$'); const uniforms = {}; let nameSplit; diff --git a/src/core/renderers/webgl/managers/FilterManager.js b/src/core/renderers/webgl/managers/FilterManager.js index 43fc869..6c7422d 100644 --- a/src/core/renderers/webgl/managers/FilterManager.js +++ b/src/core/renderers/webgl/managers/FilterManager.js @@ -2,6 +2,7 @@ import RenderTarget from '../utils/RenderTarget'; import Quad from '../utils/Quad'; import { Rectangle } from '../../../math'; +import Shader from '../../../Shader'; import * as filterTransforms from '../filters/filterTransforms'; import bitTwiddle from 'bit-twiddle'; @@ -22,8 +23,6 @@ this.filters = []; this.target = null; this.resolution = 1; - - this.firstRun = true; } } @@ -155,7 +154,7 @@ if (filters.length === 1) { - filters[0].apply(this, currentState.renderTarget, lastState.renderTarget, false); + filters[0].apply(this, currentState.renderTarget, lastState.renderTarget, false, currentState); this.freePotRenderTarget(currentState.renderTarget); } else @@ -177,7 +176,7 @@ for (i = 0; i < filters.length - 1; ++i) { - filters[i].apply(this, flip, flop, true); + filters[i].apply(this, flip, flop, true, currentState); const t = flip; @@ -185,7 +184,7 @@ flop = t; } - filters[i].apply(this, flip, lastState.renderTarget, false); + filters[i].apply(this, flip, lastState.renderTarget, false, currentState); this.freePotRenderTarget(flip); this.freePotRenderTarget(flop); @@ -212,16 +211,28 @@ const renderer = this.renderer; const gl = renderer.gl; - renderer.bindShader(filter, true); + let shader = filter.glShaders[renderer.CONTEXT_UID]; - // TODO theres a better way! - // quad can be a mesh and we then should be able to pull this off without accessing the shader directly - const shader = renderer.shaderManager.getGLShader(); - - // TODO change quad to mesh.. - if (!this.firstRun) + // cacheing.. + if (!shader) { - this.firstRun = true; + if (filter.glShaderKey) + { + shader = this.shaderCache[filter.glShaderKey]; + + if (!shader) + { + shader = new Shader(this.gl, filter.vertexSrc, filter.fragmentSrc); + + filter.glShaders[renderer.CONTEXT_UID] = this.shaderCache[filter.glShaderKey] = shader; + } + } + else + { + shader = filter.glShaders[renderer.CONTEXT_UID] = new Shader(this.gl, filter.vertexSrc, filter.fragmentSrc); + } + + // TODO - this only needs to be done once? renderer.bindVao(null); this.quad.initVao(shader); @@ -244,58 +255,146 @@ renderer.maskManager.pushScissorMask(null, renderer.maskManager.scissorData); } - // renderer._bindGLShader(shader); + renderer.bindShader(shader); - let currentState; - const uniforms = filter.uniforms; + // free unit 0 for us, doesn't matter what was there + // don't try to restore it, because syncUniforms can upload it to another slot + // and it'll be a problem + const tex = this.renderer.emptyTextures[0]; - if (uniforms.filterArea) - { - currentState = this.filterData.stack[this.filterData.index]; - - const filterArea = uniforms.filterArea; - - filterArea[0] = currentState.renderTarget.size.width; - filterArea[1] = currentState.renderTarget.size.height; - filterArea[2] = currentState.sourceFrame.x; - filterArea[3] = currentState.sourceFrame.y; - - filter.uniforms.filterArea = filterArea; - } - - // use this to clamp displaced texture coords so they belong to filterArea - // see displacementFilter fragment shader for an example - if (uniforms.filterClamp) - { - currentState = this.filterData.stack[this.filterData.index]; - - const filterClamp = uniforms.filterClamp; - - filterClamp[0] = 0; - filterClamp[1] = 0; - filterClamp[2] = (currentState.sourceFrame.width - 1) / currentState.renderTarget.size.width; - filterClamp[3] = (currentState.sourceFrame.height - 1) / currentState.renderTarget.size.height; - - filter.uniforms.filterClamp = filterClamp; - } - - renderer.shaderManager.setUniforms(filter.uniforms); + this.renderer.boundTextures[0] = tex; + // this syncs the pixi filters uniforms with glsl uniforms + this.syncUniforms(shader, filter); renderer.state.setBlendMode(filter.blendMode); - // temporary bypass cache.. - const tex = this.renderer.boundTextures[0]; - gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, input.texture.texture); this.quad.vao.draw(this.renderer.gl.TRIANGLES, 6, 0); - // restore cache. gl.bindTexture(gl.TEXTURE_2D, tex._glTextures[this.renderer.CONTEXT_UID].texture); } /** + * Uploads the uniforms of the filter. + * + * @param {GLShader} shader - The underlying gl shader. + * @param {PIXI.Filter} filter - The filter we are synchronizing. + */ + syncUniforms(shader, filter) + { + const uniformData = filter.uniformData; + const uniforms = filter.uniforms; + + // 0 is reserved for the pixi texture so we start at 1! + let textureCount = 1; + let currentState; + + // filterArea and filterClamp that are handled by FilterManager directly + // they must not appear in uniformData + + if (shader.uniforms.filterArea) + { + currentState = this.filterData.stack[this.filterData.index]; + + const filterArea = shader.uniforms.filterArea; + + filterArea[0] = currentState.renderTarget.size.width; + filterArea[1] = currentState.renderTarget.size.height; + filterArea[2] = currentState.sourceFrame.x; + filterArea[3] = currentState.sourceFrame.y; + + shader.uniforms.filterArea = filterArea; + } + + // use this to clamp displaced texture coords so they belong to filterArea + // see displacementFilter fragment shader for an example + if (shader.uniforms.filterClamp) + { + currentState = currentState || this.filterData.stack[this.filterData.index]; + + const filterClamp = shader.uniforms.filterClamp; + + filterClamp[0] = 0; + filterClamp[1] = 0; + filterClamp[2] = (currentState.sourceFrame.width - 1) / currentState.renderTarget.size.width; + filterClamp[3] = (currentState.sourceFrame.height - 1) / currentState.renderTarget.size.height; + + shader.uniforms.filterClamp = filterClamp; + } + + // TODO Cacheing layer.. + for (const i in uniformData) + { + if (uniformData[i].type === 'sampler2D' && uniforms[i] !== 0) + { + if (uniforms[i].baseTexture) + { + shader.uniforms[i] = this.renderer.bindTexture(uniforms[i].baseTexture, textureCount); + } + else + { + shader.uniforms[i] = textureCount; + + // TODO + // this is helpful as renderTargets can also be set. + // Although thinking about it, we could probably + // make the filter texture cache return a RenderTexture + // rather than a renderTarget + const gl = this.renderer.gl; + + this.renderer.boundTextures[textureCount] = this.renderer.emptyTextures[textureCount]; + gl.activeTexture(gl.TEXTURE0 + textureCount); + + uniforms[i].texture.bind(); + } + + textureCount++; + } + else if (uniformData[i].type === 'mat3') + { + // check if its pixi matrix.. + if (uniforms[i].a !== undefined) + { + shader.uniforms[i] = uniforms[i].toArray(true); + } + else + { + shader.uniforms[i] = uniforms[i]; + } + } + else if (uniformData[i].type === 'vec2') + { + // check if its a point.. + if (uniforms[i].x !== undefined) + { + const val = shader.uniforms[i] || new Float32Array(2); + + val[0] = uniforms[i].x; + val[1] = uniforms[i].y; + shader.uniforms[i] = val; + } + else + { + shader.uniforms[i] = uniforms[i]; + } + } + else if (uniformData[i].type === 'float') + { + if (shader.uniforms.data[i].value !== uniformData[i]) + { + shader.uniforms[i] = uniforms[i]; + } + } + else + { + shader.uniforms[i] = uniforms[i]; + } + } + } + + /** * Gets a render target from the pool, or creates a new one. * * @param {boolean} clear - Should we clear the render texture when we get it? @@ -442,7 +541,6 @@ renderTarget.resolution = resolution; renderTarget.defaultFrame.width = renderTarget.size.width = minWidth / resolution; renderTarget.defaultFrame.height = renderTarget.size.height = minHeight / resolution; - renderTarget.attachStencilBuffer(); return renderTarget; } diff --git a/src/core/settings.js b/src/core/settings.js index bb08cb0..f4dd087 100644 --- a/src/core/settings.js +++ b/src/core/settings.js @@ -77,11 +77,11 @@ * * @static * @memberof PIXI.settings - * @type {RegExp|string} + * @type {RegExp} * @example `@2x` - * @default /@(.+)x/ + * @default /@([0-9\.]+)x/ */ - RETINA_PREFIX: /@(.+)x/, + RETINA_PREFIX: /@([0-9\.]+)x/, /** * The default render options if none are supplied to {@link PIXI.WebGLRenderer} @@ -175,14 +175,24 @@ SCALE_MODE: 0, /** - * Default specify float precision in shaders. + * Default specify float precision in vertex shader. + * + * @static + * @memberof PIXI.settings + * @type {PIXI.PRECISION} + * @default PIXI.PRECISION.HIGH + */ + PRECISION_VERTEX: 'highp', + + /** + * Default specify float precision in fragment shader. * * @static * @memberof PIXI.settings * @type {PIXI.PRECISION} * @default PIXI.PRECISION.MEDIUM */ - PRECISION: 'mediump', + PRECISION_FRAGMENT: 'mediump', /** * Can we upload the same buffer in a single frame? diff --git a/src/core/sprites/Sprite.js b/src/core/sprites/Sprite.js index 28b0b18..121bf58 100644 --- a/src/core/sprites/Sprite.js +++ b/src/core/sprites/Sprite.js @@ -212,11 +212,11 @@ } else { - w0 = orig.width * (1 - anchor._x); - w1 = orig.width * -anchor._x; + w1 = -anchor._x * orig.width; + w0 = w1 + orig.width; - h0 = orig.height * (1 - anchor._y); - h1 = orig.height * -anchor._y; + h1 = -anchor._y * orig.height; + h0 = h1 + orig.height; } // xy @@ -269,11 +269,11 @@ const tx = wt.tx; const ty = wt.ty; - const w0 = (orig.width) * (1 - anchor._x); - const w1 = (orig.width) * -anchor._x; + const w1 = -anchor._x * orig.width; + const w0 = w1 + orig.width; - const h0 = orig.height * (1 - anchor._y); - const h1 = orig.height * -anchor._y; + const h1 = -anchor._y * orig.height; + const h0 = h1 + orig.height; // xy vertexData[0] = (a * w1) + (c * h1) + tx; @@ -440,7 +440,7 @@ * * @static * @param {number|string|PIXI.BaseTexture|HTMLCanvasElement|HTMLVideoElement} source Source to create texture from - * @return {PIXI.Sprite} The newly created texture + * @return {PIXI.Sprite} The newly created sprite */ static from(source) { @@ -538,8 +538,8 @@ } /** - * The tint applied to the sprite. This is a hex value. A value of - * 0xFFFFFF will remove any tint effect. + * The tint applied to the sprite. This is a hex value. + * A value of 0xFFFFFF will remove any tint effect. * * @member {number} * @default 0xFFFFFF diff --git a/src/core/sprites/canvas/CanvasTinter.js b/src/core/sprites/canvas/CanvasTinter.js index 1cf316b..27e5e0c 100644 --- a/src/core/sprites/canvas/CanvasTinter.js +++ b/src/core/sprites/canvas/CanvasTinter.js @@ -75,8 +75,8 @@ crop.width *= resolution; crop.height *= resolution; - canvas.width = crop.width; - canvas.height = crop.height; + canvas.width = Math.ceil(crop.width); + canvas.height = Math.ceil(crop.height); context.fillStyle = `#${(`00000${(color | 0).toString(16)}`).substr(-6)}`; @@ -130,8 +130,8 @@ crop.width *= resolution; crop.height *= resolution; - canvas.width = crop.width; - canvas.height = crop.height; + canvas.width = Math.ceil(crop.width); + canvas.height = Math.ceil(crop.height); context.globalCompositeOperation = 'copy'; context.fillStyle = `#${(`00000${(color | 0).toString(16)}`).substr(-6)}`; @@ -172,8 +172,8 @@ crop.width *= resolution; crop.height *= resolution; - canvas.width = crop.width; - canvas.height = crop.height; + canvas.width = Math.ceil(crop.width); + canvas.height = Math.ceil(crop.height); context.globalCompositeOperation = 'copy'; context.drawImage( diff --git a/src/core/sprites/webgl/SpriteRenderer.js b/src/core/sprites/webgl/SpriteRenderer.js index 2affcd5..7c352e2 100644 --- a/src/core/sprites/webgl/SpriteRenderer.js +++ b/src/core/sprites/webgl/SpriteRenderer.js @@ -30,7 +30,7 @@ /** * Number of values sent in the vertex buffer. - * positionX, positionY, colorR, colorG, colorB = 5 + * aVertexPosition(2), aTextureCoord(1), aColor(1), aTextureId(1) = 5 * * @member {number} */ @@ -75,7 +75,6 @@ this.shader = null; this.currentIndex = 0; - TICK = 0; this.groups = []; for (let k = 0; k < this.size; k++) diff --git a/src/core/textures/BaseTexture.js b/src/core/textures/BaseTexture.js index 853de7e..aeccf39 100644 --- a/src/core/textures/BaseTexture.js +++ b/src/core/textures/BaseTexture.js @@ -232,16 +232,24 @@ this.realWidth = this.source.naturalWidth || this.source.videoWidth || this.source.width; this.realHeight = this.source.naturalHeight || this.source.videoHeight || this.source.height; - this.width = this.realWidth / this.resolution; - this.height = this.realHeight / this.resolution; - - this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); + this._updateDimensions(); } this.emit('update', this); } /** + * Update dimensions from real values + */ + _updateDimensions() + { + this.width = this.realWidth / this.resolution; + this.height = this.realHeight / this.resolution; + + this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); + } + + /** * Load a source. * * If the source is not-immediately-available, such as an image that needs to be @@ -525,11 +533,7 @@ this.realWidth = Math.round(svgWidth * this.sourceScale); this.realHeight = Math.round(svgHeight * this.sourceScale); - this.width = this.realWidth / this.resolution; - this.height = this.realHeight / this.resolution; - - // Check pow2 after scale - this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); + this._updateDimensions(); // Create a canvas element const canvas = document.createElement('canvas'); @@ -690,4 +694,52 @@ return baseTexture; } + + /** + * Helper function that creates a base texture based on the source you provide. + * The source can be - image url, image element, canvas element. + * + * @static + * @param {string|HTMLImageElement|HTMLCanvasElement} source - The source to create base texture from. + * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values + * @param {number} [sourceScale=(auto)] - Scale for the original image, used with Svg images. + * @return {PIXI.BaseTexture} The new base texture. + */ + static from(source, scaleMode, sourceScale) + { + if (typeof source === 'string') + { + return BaseTexture.fromImage(source, undefined, scaleMode, sourceScale); + } + else if (source instanceof HTMLImageElement) + { + const imageUrl = source.src; + let baseTexture = BaseTextureCache[imageUrl]; + + if (!baseTexture) + { + baseTexture = new BaseTexture(source, scaleMode); + baseTexture.imageUrl = imageUrl; + + if (sourceScale) + { + baseTexture.sourceScale = sourceScale; + } + + // if there is an @2x at the end of the url we are going to assume its a highres image + baseTexture.resolution = getResolutionOfUrl(imageUrl); + + BaseTextureCache[imageUrl] = baseTexture; + } + + return baseTexture; + } + else if (source instanceof HTMLCanvasElement) + { + return BaseTexture.fromCanvas(source, scaleMode); + } + + // lets assume its a base texture! + return source; + } } diff --git a/src/core/textures/Spritesheet.js b/src/core/textures/Spritesheet.js new file mode 100644 index 0000000..d469c62 --- /dev/null +++ b/src/core/textures/Spritesheet.js @@ -0,0 +1,274 @@ +import { Rectangle, Texture } from '../'; +import { getResolutionOfUrl, TextureCache } from '../utils'; + +/** + * Utility class for maintaining reference to a collection + * of Textures on a single Spritesheet. + * + * @class + * @memberof PIXI + */ +export default class Spritesheet +{ + /** + * The maximum number of Textures to build per process. + * + * @type {number} + * @default 1000 + */ + static get BATCH_SIZE() + { + return 1000; + } + + /** + * @param {PIXI.BaseTexture} baseTexture Reference to the source BaseTexture object. + * @param {Object} data - Spritesheet image data. + * @param {string} [resolutionFilename] - The filename to consider when determining + * the resolution of the spritesheet. If not provided, the imageUrl will + * be used on the BaseTexture. + */ + constructor(baseTexture, data, resolutionFilename = null) + { + /** + * Reference to ths source texture + * @type {PIXI.BaseTexture} + */ + this.baseTexture = baseTexture; + + /** + * Map of spritesheet textures. + * @type {Object} + */ + this.textures = {}; + + /** + * Reference to the original JSON data. + * @type {Object} + */ + this.data = data; + + /** + * The resolution of the spritesheet. + * @type {number} + */ + this.resolution = this._updateResolution( + resolutionFilename || this.baseTexture.imageUrl + ); + + /** + * Map of spritesheet frames. + * @type {Object} + * @private + */ + this._frames = this.data.frames; + + /** + * Collection of frame names. + * @type {string[]} + * @private + */ + this._frameKeys = Object.keys(this._frames); + + /** + * Current batch index being processed. + * @type {number} + * @private + */ + this._batchIndex = 0; + + /** + * Callback when parse is completed. + * @type {Function} + * @private + */ + this._callback = null; + } + + /** + * Generate the resolution from the filename or fallback + * to the meta.scale field of the JSON data. + * + * @private + * @param {string} resolutionFilename - The filename to use for resolving + * the default resolution. + * @return {number} Resolution to use for spritesheet. + */ + _updateResolution(resolutionFilename) + { + const scale = this.data.meta.scale; + + // Use a defaultValue of `null` to check if a url-based resolution is set + let resolution = getResolutionOfUrl(resolutionFilename, null); + + // No resolution found via URL + if (resolution === null) + { + // Use the scale value or default to 1 + resolution = scale !== undefined ? parseFloat(scale) : 1; + } + + // For non-1 resolutions, update baseTexture + if (resolution !== 1) + { + this.baseTexture.resolution = resolution; + this.baseTexture.update(); + } + + return resolution; + } + + /** + * Parser spritesheet from loaded data. This is done asynchronously + * to prevent creating too many Texture within a single process. + * + * @param {Function} callback - Callback when complete returns + * a map of the Textures for this spritesheet. + */ + parse(callback) + { + this._batchIndex = 0; + this._callback = callback; + + if (this._frameKeys.length <= Spritesheet.BATCH_SIZE) + { + this._processFrames(0); + this._parseComplete(); + } + else + { + this._nextBatch(); + } + } + + /** + * Process a batch of frames + * + * @private + * @param {number} initialFrameIndex - The index of frame to start. + */ + _processFrames(initialFrameIndex) + { + let frameIndex = initialFrameIndex; + const maxFrames = Spritesheet.BATCH_SIZE; + + while (frameIndex - initialFrameIndex < maxFrames && frameIndex < this._frameKeys.length) + { + const i = this._frameKeys[frameIndex]; + const rect = this._frames[i].frame; + + if (rect) + { + let frame = null; + let trim = null; + const orig = new Rectangle( + 0, + 0, + this._frames[i].sourceSize.w / this.resolution, + this._frames[i].sourceSize.h / this.resolution + ); + + if (this._frames[i].rotated) + { + frame = new Rectangle( + rect.x / this.resolution, + rect.y / this.resolution, + rect.h / this.resolution, + rect.w / this.resolution + ); + } + else + { + frame = new Rectangle( + rect.x / this.resolution, + rect.y / this.resolution, + rect.w / this.resolution, + rect.h / this.resolution + ); + } + + // Check to see if the sprite is trimmed + if (this._frames[i].trimmed) + { + trim = new Rectangle( + this._frames[i].spriteSourceSize.x / this.resolution, + this._frames[i].spriteSourceSize.y / this.resolution, + rect.w / this.resolution, + rect.h / this.resolution + ); + } + + this.textures[i] = new Texture( + this.baseTexture, + frame, + orig, + trim, + this._frames[i].rotated ? 2 : 0 + ); + + // lets also add the frame to pixi's global cache for fromFrame and fromImage functions + TextureCache[i] = this.textures[i]; + } + + frameIndex++; + } + } + + /** + * The parse has completed. + * + * @private + */ + _parseComplete() + { + const callback = this._callback; + + this._callback = null; + this._batchIndex = 0; + callback.call(this, this.textures); + } + + /** + * Begin the next batch of textures. + * + * @private + */ + _nextBatch() + { + this._processFrames(this._batchIndex * Spritesheet.BATCH_SIZE); + this._batchIndex++; + setTimeout(() => + { + if (this._batchIndex * Spritesheet.BATCH_SIZE < this._frameKeys.length) + { + this._nextBatch(); + } + else + { + this._parseComplete(); + } + }, 0); + } + + /** + * Destroy Spritesheet and don't use after this. + * + * @param {boolean} [destroyBase=false] Whether to destroy the base texture as well + */ + destroy(destroyBase = false) + { + for (const i in this.textures) + { + this.textures[i].destroy(); + } + this._frames = null; + this._frameKeys = null; + this.data = null; + this.textures = null; + if (destroyBase) + { + this.baseTexture.destroy(); + } + this.baseTexture = null; + } +} diff --git a/src/core/textures/Texture.js b/src/core/textures/Texture.js index 6403d4f..afe4254 100644 --- a/src/core/textures/Texture.js +++ b/src/core/textures/Texture.js @@ -3,7 +3,7 @@ import TextureUvs from './TextureUvs'; import EventEmitter from 'eventemitter3'; import { Rectangle } from '../math'; -import { TextureCache, BaseTextureCache } from '../utils'; +import { TextureCache, BaseTextureCache, getResolutionOfUrl } from '../utils'; /** * A texture stores the information that represents an image or part of an image. It cannot be added @@ -365,7 +365,8 @@ * The source can be - frame id, image url, video url, canvas element, video element, base texture * * @static - * @param {number|string|PIXI.BaseTexture|HTMLCanvasElement|HTMLVideoElement} source - Source to create texture from + * @param {number|string|HTMLImageElement|HTMLCanvasElement|HTMLVideoElement|PIXI.BaseTexture} + * source - Source to create texture from * @return {PIXI.Texture} The newly created texture */ static from(source) @@ -393,7 +394,7 @@ } else if (source instanceof HTMLImageElement) { - return new Texture(new BaseTexture(source)); + return new Texture(BaseTexture.from(source)); } else if (source instanceof HTMLCanvasElement) { @@ -413,6 +414,43 @@ } /** + * Create a texture from a source and add to the cache. + * + * @static + * @param {HTMLImageElement|HTMLCanvasElement} source - The input source. + * @param {String} imageUrl - File name of texture, for cache and resolving resolution. + * @param {String} [name] - Human readible name for the texture cache. If no name is + * specified, only `imageUrl` will be used as the cache ID. + * @return {PIXI.Texture} Output texture + */ + static fromLoader(source, imageUrl, name) + { + const baseTexture = new BaseTexture(source, null, getResolutionOfUrl(imageUrl)); + const texture = new Texture(baseTexture); + + baseTexture.imageUrl = imageUrl; + + // No name, use imageUrl instead + if (!name) + { + name = imageUrl; + } + + // lets also add the frame to pixi's global cache for fromFrame and fromImage fucntions + BaseTextureCache[name] = baseTexture; + TextureCache[name] = texture; + + // also add references by url if they are different. + if (name !== imageUrl) + { + BaseTextureCache[imageUrl] = baseTexture; + TextureCache[imageUrl] = texture; + } + + return texture; + } + + /** * Adds a texture to the global TextureCache. This cache is shared across the whole PIXI object. * * @static @@ -522,6 +560,29 @@ } } +function createWhiteTexture() +{ + const canvas = document.createElement('canvas'); + + canvas.width = 10; + canvas.height = 10; + + const context = canvas.getContext('2d'); + + context.fillStyle = 'white'; + context.fillRect(0, 0, 10, 10); + + return new Texture(new BaseTexture(canvas)); +} + +function removeAllHandlers(tex) +{ + tex.destroy = function _emptyDestroy() { /* empty */ }; + tex.on = function _emptyOn() { /* empty */ }; + tex.once = function _emptyOnce() { /* empty */ }; + tex.emit = function _emptyEmit() { /* empty */ }; +} + /** * An empty texture, used often to not have to create multiple empty textures. * Can not be destroyed. @@ -530,7 +591,14 @@ * @constant */ Texture.EMPTY = new Texture(new BaseTexture()); -Texture.EMPTY.destroy = function _emptyDestroy() { /* empty */ }; -Texture.EMPTY.on = function _emptyOn() { /* empty */ }; -Texture.EMPTY.once = function _emptyOnce() { /* empty */ }; -Texture.EMPTY.emit = function _emptyEmit() { /* empty */ }; +removeAllHandlers(Texture.EMPTY); + +/** + * A white texture of 10x10 size, used for graphics and other things + * Can not be destroyed. + * + * @static + * @constant + */ +Texture.WHITE = createWhiteTexture(); +removeAllHandlers(Texture.WHITE); diff --git a/package.json b/package.json index 6a366ba..71ade15 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pixi.js", - "version": "4.3.4", + "version": "4.3.5", "description": "Pixi.js is a fast lightweight 2D library that works across all devices.", "author": "Mat Groves", "contributors": [ @@ -61,7 +61,7 @@ "ismobilejs": "^0.4.0", "object-assign": "^4.0.1", "pixi-gl-core": "^1.0.3", - "resource-loader": "^2.0.4" + "resource-loader": "^2.0.6" }, "devDependencies": { "babel-cli": "^6.18.0", @@ -70,9 +70,9 @@ "babel-preset-es2015": "^6.14.0", "babelify": "^7.3.0", "del": "^2.2.0", - "electron-prebuilt": "^1.3.2", + "electron": "^1.4.15", "eslint": "^3.5.0", - "floss": "^1.2.0", + "floss": "^2.0.1", "gh-pages": "^0.11.0", "jaguarjs-jsdoc": "^1.0.1", "js-md5": "^0.4.1", diff --git a/src/accessibility/AccessibilityManager.js b/src/accessibility/AccessibilityManager.js index df2716a..f16bcb1 100644 --- a/src/accessibility/AccessibilityManager.js +++ b/src/accessibility/AccessibilityManager.js @@ -3,7 +3,7 @@ import accessibleTarget from './accessibleTarget'; // add some extra variables to the container.. -Object.assign( +core.utils.mixins.delayMixin( core.DisplayObject.prototype, accessibleTarget ); diff --git a/src/core/Application.js b/src/core/Application.js index ea0f058..5340578 100644 --- a/src/core/Application.js +++ b/src/core/Application.js @@ -1,6 +1,6 @@ import { autoDetectRenderer } from './autoDetectRenderer'; import Container from './display/Container'; -import Ticker from './ticker/Ticker'; +import { shared, Ticker } from './ticker'; /** * Convenience class to create a new PIXI application. @@ -35,9 +35,9 @@ * @param {boolean} [noWebGL=false] - prevents selection of WebGL renderer, even if such is present * @param {boolean} [options.legacy=false] - If true Pixi will aim to ensure compatibility * with older / less advanced devices. If you experience unexplained flickering try setting this to true. - + * @param {boolean} [useSharedTicker=false] - `true` to use PIXI.ticker.shared, `false` to create new ticker. */ - constructor(width, height, options, noWebGL) + constructor(width, height, options, noWebGL, useSharedTicker = false) { /** * WebGL renderer if available, otherwise CanvasRenderer @@ -46,23 +46,46 @@ this.renderer = autoDetectRenderer(width, height, options, noWebGL); /** - * The root display container that's renderered. + * The root display container that's rendered. * @member {PIXI.Container} */ this.stage = new Container(); /** + * Internal reference to the ticker + * @member {PIXI.ticker.Ticker} + * @private + */ + this._ticker = null; + + /** * Ticker for doing render updates. * @member {PIXI.ticker.Ticker} + * @default PIXI.ticker.shared */ - this.ticker = new Ticker(); - - this.ticker.add(this.render, this); + this.ticker = useSharedTicker ? shared : new Ticker(); // Start the rendering this.start(); } + set ticker(ticker) // eslint-disable-line require-jsdoc + { + if (this._ticker) + { + this._ticker.remove(this.render, this); + } + this._ticker = ticker; + if (ticker) + { + ticker.add(this.render, this); + } + } + get ticker() // eslint-disable-line require-jsdoc + { + return this._ticker; + } + /** * Render the current stage. */ @@ -76,7 +99,7 @@ */ stop() { - this.ticker.stop(); + this._ticker.stop(); } /** @@ -84,7 +107,7 @@ */ start() { - this.ticker.start(); + this._ticker.start(); } /** @@ -98,13 +121,22 @@ } /** + * Reference to the renderer's screen rectangle. Its safe to use as filterArea or hitArea for whole screen + * @member {PIXI.Rectangle} + * @readonly + */ + get screen() + { + return this.renderer.screen; + } + + /** * Destroy and don't use after this. * @param {Boolean} [removeView=false] Automatically remove canvas from DOM. */ destroy(removeView) { this.stop(); - this.ticker.remove(this.render, this); this.ticker = null; this.stage.destroy(); diff --git a/src/core/Shader.js b/src/core/Shader.js new file mode 100644 index 0000000..78b17e6 --- /dev/null +++ b/src/core/Shader.js @@ -0,0 +1,46 @@ +import { GLShader } from 'pixi-gl-core'; +import settings from './settings'; + +function checkPrecision(src, def) +{ + if (src instanceof Array) + { + if (src[0].substring(0, 9) !== 'precision') + { + const copy = src.slice(0); + + copy.unshift(`precision ${def} float;`); + + return copy; + } + } + else if (src.substring(0, 9) !== 'precision') + { + return `precision ${def} float;\n${src}`; + } + + return src; +} + +/** + * Wrapper class, webGL Shader for Pixi. + * Adds precision string if vertexSrc or fragmentSrc have no mention of it. + * + * @class + * @extends GLShader + * @memberof PIXI + */ +export default class Shader extends GLShader +{ + /** + * + * @param {WebGLRenderingContext} gl - The current WebGL rendering context + * @param {string|string[]} vertexSrc - The vertex shader source as an array of strings. + * @param {string|string[]} fragmentSrc - The fragment shader source as an array of strings. + */ + constructor(gl, vertexSrc, fragmentSrc) + { + super(gl, checkPrecision(vertexSrc, settings.PRECISION_VERTEX), + checkPrecision(fragmentSrc, settings.PRECISION_FRAGMENT)); + } +} diff --git a/src/core/const.js b/src/core/const.js index ef6f662..3de0efe 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -188,7 +188,7 @@ * The gc modes that are supported by pixi. * * The {@link PIXI.settings.GC_MODE} Garbage Collection mode for pixi textures is AUTO - * If set to GC_MODE, the renderer will occasianally check textures usage. If they are not + * If set to GC_MODE, the renderer will occasionally check textures usage. If they are not * used for a specified period of time they will be removed from the GPU. They will of course * be uploaded again when they are required. This is a silent behind the scenes process that * should ensure that the GPU does not get filled up. diff --git a/src/core/display/Container.js b/src/core/display/Container.js index ed8cd5f..17fde8d 100644 --- a/src/core/display/Container.js +++ b/src/core/display/Container.js @@ -73,11 +73,12 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.push(child); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -108,11 +109,12 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.splice(index, 0, child); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -225,10 +227,11 @@ if (index === -1) return null; child.parent = null; + // ensure child transform will be recalculated + child.transform._parentID = -1; removeItems(this.children, index, 1); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -249,11 +252,12 @@ { const child = this.getChildAt(index); + // ensure child transform will be recalculated.. child.parent = null; + child.transform._parentID = -1; removeItems(this.children, index, 1); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -284,13 +288,12 @@ for (let i = 0; i < removed.length; ++i) { removed[i].parent = null; + if (removed[i].transform) + { + removed[i].transform._parentID = -1; + } } - // ensure a transform will be recalculated.. - if (this.transform) - { - this.transform._parentID = -1; - } this._boundsID++; this.onChildrenChange(beginIndex); diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index f37f2f5..be4c4fb 100644 --- a/src/core/graphics/Graphics.js +++ b/src/core/graphics/Graphics.js @@ -28,8 +28,9 @@ { /** * + * @param {boolean} [nativeLines=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP */ - constructor() + constructor(nativeLines = false) { super(); @@ -50,6 +51,13 @@ this.lineWidth = 0; /** + * If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * + * @member {boolean} + */ + this.nativeLines = nativeLines; + + /** * The color of any lines drawn. * * @member {string} @@ -757,28 +765,14 @@ if (!this._spriteRect) { - if (!Graphics._SPRITE_TEXTURE) - { - Graphics._SPRITE_TEXTURE = RenderTexture.create(10, 10); - - const canvas = document.createElement('canvas'); - - canvas.width = 10; - canvas.height = 10; - - const context = canvas.getContext('2d'); - - context.fillStyle = 'white'; - context.fillRect(0, 0, 10, 10); - - Graphics._SPRITE_TEXTURE = Texture.fromCanvas(canvas); - } - - this._spriteRect = new Sprite(Graphics._SPRITE_TEXTURE); + this._spriteRect = new Sprite(new Texture(Texture.WHITE)); } + + const sprite = this._spriteRect; + if (this.tint === 0xffffff) { - this._spriteRect.tint = this.graphicsData[0].fillColor; + sprite.tint = this.graphicsData[0].fillColor; } else { @@ -792,20 +786,21 @@ t1[1] *= t2[1]; t1[2] *= t2[2]; - this._spriteRect.tint = rgb2hex(t1); + sprite.tint = rgb2hex(t1); } - this._spriteRect.alpha = this.graphicsData[0].fillAlpha; - this._spriteRect.worldAlpha = this.worldAlpha * this._spriteRect.alpha; + sprite.alpha = this.graphicsData[0].fillAlpha; + sprite.worldAlpha = this.worldAlpha * sprite.alpha; + sprite.blendMode = this.blendMode; - Graphics._SPRITE_TEXTURE._frame.width = rect.width; - Graphics._SPRITE_TEXTURE._frame.height = rect.height; + sprite.texture._frame.width = rect.width; + sprite.texture._frame.height = rect.height; - this._spriteRect.transform.worldTransform = this.transform.worldTransform; + sprite.transform.worldTransform = this.transform.worldTransform; - this._spriteRect.anchor.set(-rect.x / rect.width, -rect.y / rect.height); - this._spriteRect._onAnchorUpdate(); + sprite.anchor.set(-rect.x / rect.width, -rect.y / rect.height); + sprite._onAnchorUpdate(); - this._spriteRect._renderWebGL(renderer); + sprite._renderWebGL(renderer); } /** @@ -1031,6 +1026,7 @@ this.fillColor, this.fillAlpha, this.filling, + this.nativeLines, shape ); diff --git a/src/core/graphics/GraphicsData.js b/src/core/graphics/GraphicsData.js index f5a87ae..b6f03c0 100644 --- a/src/core/graphics/GraphicsData.js +++ b/src/core/graphics/GraphicsData.js @@ -14,14 +14,19 @@ * @param {number} fillColor - the color of the fill * @param {number} fillAlpha - the alpha of the fill * @param {boolean} fill - whether or not the shape is filled with a colour + * @param {boolean} nativeLines - the method for drawing lines * @param {PIXI.Circle|PIXI.Rectangle|PIXI.Ellipse|PIXI.Polygon} shape - The shape object to draw. */ - constructor(lineWidth, lineColor, lineAlpha, fillColor, fillAlpha, fill, shape) + constructor(lineWidth, lineColor, lineAlpha, fillColor, fillAlpha, fill, nativeLines, shape) { /** * @member {number} the width of the line to draw */ this.lineWidth = lineWidth; + /** + * @member {boolean} if true the liens will be draw using LINES instead of TRIANGLE_STRIP + */ + this.nativeLines = nativeLines; /** * @member {number} the color of the line to draw @@ -85,6 +90,7 @@ this.fillColor, this.fillAlpha, this.fill, + this.nativeLines, this.shape ); } diff --git a/src/core/graphics/webgl/GraphicsRenderer.js b/src/core/graphics/webgl/GraphicsRenderer.js index 14083fe..67f5e92 100644 --- a/src/core/graphics/webgl/GraphicsRenderer.js +++ b/src/core/graphics/webgl/GraphicsRenderer.js @@ -102,7 +102,15 @@ shaderTemp.uniforms.alpha = graphics.worldAlpha; renderer.bindVao(webGLData.vao); - webGLData.vao.draw(gl.TRIANGLE_STRIP, webGLData.indices.length); + + if (graphics.nativeLines) + { + gl.drawArrays(gl.LINES, 0, webGLData.points.length / 6); + } + else + { + webGLData.vao.draw(gl.TRIANGLE_STRIP, webGLData.indices.length); + } } } diff --git a/src/core/graphics/webgl/utils/buildLine.js b/src/core/graphics/webgl/utils/buildLine.js index ecf43f7..4d6862e 100644 --- a/src/core/graphics/webgl/utils/buildLine.js +++ b/src/core/graphics/webgl/utils/buildLine.js @@ -11,7 +11,29 @@ * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties * @param {object} webGLData - an object containing all the webGL-specific information to create this shape */ -export default function buildLine(graphicsData, webGLData) +export default function (graphicsData, webGLData) +{ + if (graphicsData.nativeLines) + { + buildNativeLine(graphicsData, webGLData); + } + else + { + buildLine(graphicsData, webGLData); + } +} + +/** + * Builds a line to draw using the poligon method. + * + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @private + * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties + * @param {object} webGLData - an object containing all the webGL-specific information to create this shape + */ +function buildLine(graphicsData, webGLData) { // TODO OPTIMISE! let points = graphicsData.points; @@ -224,3 +246,46 @@ indices.push(indexStart - 1); } + +/** + * Builds a line to draw using the gl.drawArrays(gl.LINES) method + * + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @private + * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties + * @param {object} webGLData - an object containing all the webGL-specific information to create this shape + */ +function buildNativeLine(graphicsData, webGLData) +{ + let i = 0; + const points = graphicsData.points; + + if (points.length === 0) return; + + const verts = webGLData.points; + const length = points.length / 2; + + // sort color + const color = hex2rgb(graphicsData.lineColor); + const alpha = graphicsData.lineAlpha; + const r = color[0] * alpha; + const g = color[1] * alpha; + const b = color[2] * alpha; + + for (i = 1; i < length; i++) + { + const p1x = points[(i - 1) * 2]; + const p1y = points[((i - 1) * 2) + 1]; + + const p2x = points[i * 2]; + const p2y = points[(i * 2) + 1]; + + verts.push(p1x, p1y); + verts.push(r, g, b, alpha); + + verts.push(p2x, p2y); + verts.push(r, g, b, alpha); + } +} diff --git a/src/core/graphics/webgl/utils/buildRoundedRectangle.js b/src/core/graphics/webgl/utils/buildRoundedRectangle.js index 9abc807..a6c8461 100644 --- a/src/core/graphics/webgl/utils/buildRoundedRectangle.js +++ b/src/core/graphics/webgl/utils/buildRoundedRectangle.js @@ -77,6 +77,26 @@ } /** + * Calculate a single point for a quadratic bezier curve. + * Utility function used by quadraticBezierCurve. + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @private + * @param {number} n1 - first number + * @param {number} n2 - second number + * @param {number} perc - percentage + * @return {number} the result + * + */ +function getPt(n1, n2, perc) +{ + const diff = n2 - n1; + + return n1 + (diff * perc); +} + +/** * Calculate the points for a quadratic bezier curve. (helper function..) * Based on: https://stackoverflow.com/questions/785097/how-do-i-implement-a-bezier-curve-in-c * @@ -105,13 +125,6 @@ let x = 0; let y = 0; - function getPt(n1, n2, perc) - { - const diff = n2 - n1; - - return n1 + (diff * perc); - } - for (let i = 0, j = 0; i <= n; ++i) { j = i / n; diff --git a/src/core/index.js b/src/core/index.js index f7a817c..4f414e8 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -30,6 +30,7 @@ export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; export { default as CanvasGraphicsRenderer } from './graphics/canvas/CanvasGraphicsRenderer'; +export { default as Spritesheet } from './textures/Spritesheet'; export { default as Texture } from './textures/Texture'; export { default as BaseTexture } from './textures/BaseTexture'; export { default as RenderTexture } from './textures/RenderTexture'; diff --git a/src/core/math/Matrix.js b/src/core/math/Matrix.js index 0b77477..3679860 100644 --- a/src/core/math/Matrix.js +++ b/src/core/math/Matrix.js @@ -13,45 +13,50 @@ export default class Matrix { /** - * + * @param {number} [a=1] - x scale + * @param {number} [b=0] - y skew + * @param {number} [c=0] - x skew + * @param {number} [d=1] - y scale + * @param {number} [tx=0] - x translation + * @param {number} [ty=0] - y translation */ - constructor() + constructor(a = 1, b = 0, c = 0, d = 1, tx = 0, ty = 0) { /** * @member {number} * @default 1 */ - this.a = 1; + this.a = a; /** * @member {number} * @default 0 */ - this.b = 0; + this.b = b; /** * @member {number} * @default 0 */ - this.c = 0; + this.c = c; /** * @member {number} * @default 1 */ - this.d = 1; + this.d = d; /** * @member {number} * @default 0 */ - this.tx = 0; + this.tx = tx; /** * @member {number} * @default 0 */ - this.ty = 0; + this.ty = ty; this.array = null; } diff --git a/src/core/renderers/SystemRenderer.js b/src/core/renderers/SystemRenderer.js index fa116a6..a24c775 100644 --- a/src/core/renderers/SystemRenderer.js +++ b/src/core/renderers/SystemRenderer.js @@ -1,5 +1,5 @@ import { sayHello, hex2string, hex2rgb } from '../utils'; -import { Matrix } from '../math'; +import { Matrix, Rectangle } from '../math'; import { RENDERER_TYPE } from '../const'; import settings from '../settings'; import Container from '../display/Container'; @@ -21,8 +21,8 @@ { /** * @param {string} system - The name of the system this renderer is for. - * @param {number} [width=800] - the width of the canvas view - * @param {number} [height=600] - the height of the canvas view + * @param {number} [screenWidth=800] - the width of the screen + * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false @@ -37,7 +37,7 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(system, width, height, options) + constructor(system, screenWidth, screenHeight, options) { super(); @@ -69,20 +69,13 @@ this.type = RENDERER_TYPE.UNKNOWN; /** - * The width of the canvas view + * Measurements of the screen. (0, 0, screenWidth, screenHeight) * - * @member {number} - * @default 800 - */ - this.width = width || 800; - - /** - * The height of the canvas view + * Its safe to use as filterArea or hitArea for whole stage * - * @member {number} - * @default 600 + * @member {PIXI.Rectangle} */ - this.height = height || 600; + this.screen = new Rectangle(0, 0, screenWidth || 800, screenHeight || 600); /** * The canvas element that everything is drawn to @@ -107,7 +100,7 @@ this.transparent = options.transparent; /** - * Whether the render view should be resized automatically + * Whether css dimensions of canvas view should be resized to screen dimensions automatically * * @member {boolean} */ @@ -192,23 +185,48 @@ } /** - * Resizes the canvas view to the specified width and height + * Same as view.width, actual number of pixels in the canvas by horizontal * - * @param {number} width - the new width of the canvas view - * @param {number} height - the new height of the canvas view + * @member {number} + * @readonly + * @default 800 */ - resize(width, height) + get width() { - this.width = width * this.resolution; - this.height = height * this.resolution; + return this.view.width; + } - this.view.width = this.width; - this.view.height = this.height; + /** + * Same as view.height, actual number of pixels in the canvas by vertical + * + * @member {number} + * @readonly + * @default 600 + */ + get height() + { + return this.view.height; + } + + /** + * Resizes the screen and canvas to the specified width and height + * Canvas dimensions are multiplied by resolution + * + * @param {number} screenWidth - the new width of the screen + * @param {number} screenHeight - the new height of the screen + */ + resize(screenWidth, screenHeight) + { + this.screen.width = screenWidth; + this.screen.height = screenHeight; + + this.view.width = screenWidth * this.resolution; + this.view.height = screenHeight * this.resolution; if (this.autoResize) { - this.view.style.width = `${this.width / this.resolution}px`; - this.view.style.height = `${this.height / this.resolution}px`; + this.view.style.width = `${screenWidth}px`; + this.view.style.height = `${screenHeight}px`; } } @@ -249,11 +267,10 @@ this.type = RENDERER_TYPE.UNKNOWN; - this.width = 0; - this.height = 0; - this.view = null; + this.screen = null; + this.resolution = 0; this.transparent = false; diff --git a/src/core/renderers/canvas/CanvasRenderer.js b/src/core/renderers/canvas/CanvasRenderer.js index 1bd2d5e..e84e49c 100644 --- a/src/core/renderers/canvas/CanvasRenderer.js +++ b/src/core/renderers/canvas/CanvasRenderer.js @@ -18,8 +18,8 @@ export default class CanvasRenderer extends SystemRenderer { /** - * @param {number} [width=800] - the width of the canvas view - * @param {number} [height=600] - the height of the canvas view + * @param {number} [screenWidth=800] - the width of the screen + * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false @@ -34,9 +34,9 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(width, height, options = {}) + constructor(screenWidth, screenHeight, options = {}) { - super('Canvas', width, height, options); + super('Canvas', screenWidth, screenHeight, options); this.type = RENDERER_TYPE.CANVAS; @@ -96,7 +96,7 @@ this.context = null; this.renderingToScreen = false; - this.resize(width, height); + this.resize(screenWidth, screenHeight); } /** @@ -283,12 +283,12 @@ * * @extends PIXI.SystemRenderer#resize * - * @param {number} width - The new width of the canvas view - * @param {number} height - The new height of the canvas view + * @param {number} screenWidth - the new width of the screen + * @param {number} screenHeight - the new height of the screen */ - resize(width, height) + resize(screenWidth, screenHeight) { - super.resize(width, height); + super.resize(screenWidth, screenHeight); // reset the scale mode.. oddly this seems to be reset when the canvas is resized. // surely a browser bug?? Let pixi fix that for you.. diff --git a/src/core/renderers/webgl/TextureManager.js b/src/core/renderers/webgl/TextureManager.js index b685c1a..8227289 100644 --- a/src/core/renderers/webgl/TextureManager.js +++ b/src/core/renderers/webgl/TextureManager.js @@ -61,7 +61,7 @@ * Updates and/or Creates a WebGL texture for the renderer's context. * * @param {PIXI.BaseTexture|PIXI.Texture} texture - the texture to update - * @param {Number} location - the location the texture will be bound to. + * @param {number} location - the location the texture will be bound to. * @return {GLTexture} The gl texture. */ updateTexture(texture, location) diff --git a/src/core/renderers/webgl/WebGLRenderer.js b/src/core/renderers/webgl/WebGLRenderer.js index e4cd8a0..830b382 100644 --- a/src/core/renderers/webgl/WebGLRenderer.js +++ b/src/core/renderers/webgl/WebGLRenderer.js @@ -31,8 +31,8 @@ { /** * - * @param {number} [width=0] - the width of the canvas view - * @param {number} [height=0] - the height of the canvas view + * @param {number} [screenWidth=800] - the width of the screen + * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false @@ -53,9 +53,9 @@ * @param {boolean} [options.legacy=false] - If true Pixi will aim to ensure compatibility * with older / less advanced devices. If you experiance unexplained flickering try setting this to true. */ - constructor(width, height, options = {}) + constructor(screenWidth, screenHeight, options = {}) { - super('WebGL', width, height, options); + super('WebGL', screenWidth, screenHeight, options); this.legacy = !!options.legacy; @@ -245,7 +245,7 @@ glCore._testingContext = gl; // setup the width/height properties and gl viewport - this.resize(this.width, this.height); + this.resize(this.screen.width, this.screen.height); } /** @@ -343,16 +343,16 @@ /** * Resizes the webGL view to the specified width and height. * - * @param {number} width - the new width of the webGL view - * @param {number} height - the new height of the webGL view + * @param {number} screenWidth - the new width of the screen + * @param {number} screenHeight - the new height of the screen */ - resize(width, height) + resize(screenWidth, screenHeight) { // if(width * this.resolution === this.width && height * this.resolution === this.height)return; - SystemRenderer.prototype.resize.call(this, width, height); + SystemRenderer.prototype.resize.call(this, screenWidth, screenHeight); - this.rootRenderTarget.resize(width, height); + this.rootRenderTarget.resize(screenWidth, screenHeight); if (this._activeRenderTarget === this.rootRenderTarget) { @@ -396,6 +396,26 @@ } /** + * Erases the render texture and fills the drawing area with a colour + * + * @param {PIXI.RenderTexture} renderTexture - The render texture to clear + * @param {number} [clearColor] - The colour + * @return {PIXI.WebGLRenderer} Returns itself. + */ + clearRenderTexture(renderTexture, clearColor) + { + const baseTexture = renderTexture.baseTexture; + const renderTarget = baseTexture._glRenderTargets[this.CONTEXT_UID]; + + if (renderTarget) + { + renderTarget.clear(clearColor); + } + + return this; + } + + /** * Binds a render texture for rendering * * @param {PIXI.RenderTexture} renderTexture - The render texture to render @@ -474,9 +494,10 @@ * Changes the current GLShader to the one given in parameter * * @param {PIXI.glCore.Shader} shader - the new glShader + * @param {boolean} [autoProject=true] - Whether automatically set the projection matrix * @return {PIXI.WebGLRenderer} Returns itself. */ - _bindGLShader(shader) + _bindGLShader(shader, autoProject) { // TODO cache if (this._activeShader !== shader) @@ -484,8 +505,14 @@ this._activeShader = shader; shader.bind(); - // automatically set the projection matrix - shader.uniforms.projectionMatrix = this._activeRenderTarget.projectionMatrix.toArray(true); + // `autoProject` normally would be a default parameter set to true + // but because of how Babel transpiles default parameters + // it hinders the performance of this method. + if (autoProject !== false) + { + // automatically set the projection matrix + shader.uniforms.projectionMatrix = this._activeRenderTarget.projectionMatrix.toArray(true); + } } return this; diff --git a/src/core/renderers/webgl/WebGLState.js b/src/core/renderers/webgl/WebGLState.js index ba6ecc5..6c846d5 100755 --- a/src/core/renderers/webgl/WebGLState.js +++ b/src/core/renderers/webgl/WebGLState.js @@ -82,18 +82,20 @@ push() { // next state.. - let state = this.stack[++this.stackIndex]; + let state = this.stack[this.stackIndex]; if (!state) { state = this.stack[this.stackIndex] = new Uint8Array(16); } + ++this.stackIndex; + // copy state.. // set active state so we can force overrides of gl state for (let i = 0; i < this.activeState.length; i++) { - this.activeState[i] = state[i]; + state[i] = this.activeState[i]; } } diff --git a/src/core/renderers/webgl/filters/Filter.js b/src/core/renderers/webgl/filters/Filter.js index 2c3eaad..4eb6128 100644 --- a/src/core/renderers/webgl/filters/Filter.js +++ b/src/core/renderers/webgl/filters/Filter.js @@ -9,7 +9,7 @@ * @memberof PIXI * @extends PIXI.Shader */ -class Filter extends Shader +export default class Filter extends Shader { /** * @param {string} [vertexSrc] - The source of the vertex shader. @@ -56,15 +56,16 @@ * @param {PIXI.RenderTarget} input - The input render target. * @param {PIXI.RenderTarget} output - The target to output to. * @param {boolean} clear - Should the output be cleared before rendering to it + * @param {object} [currentState] - It's current state of filter. + * There are some useful properties in the currentState : + * target, filters, sourceFrame, destinationFrame, renderTarget, resolution */ - apply(filterManager, input, output, clear) + apply(filterManager, input, output, clear, currentState) // eslint-disable-line no-unused-vars { // do as you please! - filterManager.applyFilter(this, input, output, clear); + filterManager.applyFilter(this, input, output, clear, currentState); // or just do a regular render.. } } - -export default Filter; diff --git a/src/core/renderers/webgl/filters/extractUniformsFromSrc.js b/src/core/renderers/webgl/filters/extractUniformsFromSrc.js index d78a0b1..4893214 100644 --- a/src/core/renderers/webgl/filters/extractUniformsFromSrc.js +++ b/src/core/renderers/webgl/filters/extractUniformsFromSrc.js @@ -12,7 +12,7 @@ function extractUniformsFromString(string) { - const maskRegex = new RegExp('^(projectionMatrix|uSampler|filterArea)$'); + const maskRegex = new RegExp('^(projectionMatrix|uSampler|filterArea|filterClamp)$'); const uniforms = {}; let nameSplit; diff --git a/src/core/renderers/webgl/managers/FilterManager.js b/src/core/renderers/webgl/managers/FilterManager.js index 43fc869..6c7422d 100644 --- a/src/core/renderers/webgl/managers/FilterManager.js +++ b/src/core/renderers/webgl/managers/FilterManager.js @@ -2,6 +2,7 @@ import RenderTarget from '../utils/RenderTarget'; import Quad from '../utils/Quad'; import { Rectangle } from '../../../math'; +import Shader from '../../../Shader'; import * as filterTransforms from '../filters/filterTransforms'; import bitTwiddle from 'bit-twiddle'; @@ -22,8 +23,6 @@ this.filters = []; this.target = null; this.resolution = 1; - - this.firstRun = true; } } @@ -155,7 +154,7 @@ if (filters.length === 1) { - filters[0].apply(this, currentState.renderTarget, lastState.renderTarget, false); + filters[0].apply(this, currentState.renderTarget, lastState.renderTarget, false, currentState); this.freePotRenderTarget(currentState.renderTarget); } else @@ -177,7 +176,7 @@ for (i = 0; i < filters.length - 1; ++i) { - filters[i].apply(this, flip, flop, true); + filters[i].apply(this, flip, flop, true, currentState); const t = flip; @@ -185,7 +184,7 @@ flop = t; } - filters[i].apply(this, flip, lastState.renderTarget, false); + filters[i].apply(this, flip, lastState.renderTarget, false, currentState); this.freePotRenderTarget(flip); this.freePotRenderTarget(flop); @@ -212,16 +211,28 @@ const renderer = this.renderer; const gl = renderer.gl; - renderer.bindShader(filter, true); + let shader = filter.glShaders[renderer.CONTEXT_UID]; - // TODO theres a better way! - // quad can be a mesh and we then should be able to pull this off without accessing the shader directly - const shader = renderer.shaderManager.getGLShader(); - - // TODO change quad to mesh.. - if (!this.firstRun) + // cacheing.. + if (!shader) { - this.firstRun = true; + if (filter.glShaderKey) + { + shader = this.shaderCache[filter.glShaderKey]; + + if (!shader) + { + shader = new Shader(this.gl, filter.vertexSrc, filter.fragmentSrc); + + filter.glShaders[renderer.CONTEXT_UID] = this.shaderCache[filter.glShaderKey] = shader; + } + } + else + { + shader = filter.glShaders[renderer.CONTEXT_UID] = new Shader(this.gl, filter.vertexSrc, filter.fragmentSrc); + } + + // TODO - this only needs to be done once? renderer.bindVao(null); this.quad.initVao(shader); @@ -244,58 +255,146 @@ renderer.maskManager.pushScissorMask(null, renderer.maskManager.scissorData); } - // renderer._bindGLShader(shader); + renderer.bindShader(shader); - let currentState; - const uniforms = filter.uniforms; + // free unit 0 for us, doesn't matter what was there + // don't try to restore it, because syncUniforms can upload it to another slot + // and it'll be a problem + const tex = this.renderer.emptyTextures[0]; - if (uniforms.filterArea) - { - currentState = this.filterData.stack[this.filterData.index]; - - const filterArea = uniforms.filterArea; - - filterArea[0] = currentState.renderTarget.size.width; - filterArea[1] = currentState.renderTarget.size.height; - filterArea[2] = currentState.sourceFrame.x; - filterArea[3] = currentState.sourceFrame.y; - - filter.uniforms.filterArea = filterArea; - } - - // use this to clamp displaced texture coords so they belong to filterArea - // see displacementFilter fragment shader for an example - if (uniforms.filterClamp) - { - currentState = this.filterData.stack[this.filterData.index]; - - const filterClamp = uniforms.filterClamp; - - filterClamp[0] = 0; - filterClamp[1] = 0; - filterClamp[2] = (currentState.sourceFrame.width - 1) / currentState.renderTarget.size.width; - filterClamp[3] = (currentState.sourceFrame.height - 1) / currentState.renderTarget.size.height; - - filter.uniforms.filterClamp = filterClamp; - } - - renderer.shaderManager.setUniforms(filter.uniforms); + this.renderer.boundTextures[0] = tex; + // this syncs the pixi filters uniforms with glsl uniforms + this.syncUniforms(shader, filter); renderer.state.setBlendMode(filter.blendMode); - // temporary bypass cache.. - const tex = this.renderer.boundTextures[0]; - gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, input.texture.texture); this.quad.vao.draw(this.renderer.gl.TRIANGLES, 6, 0); - // restore cache. gl.bindTexture(gl.TEXTURE_2D, tex._glTextures[this.renderer.CONTEXT_UID].texture); } /** + * Uploads the uniforms of the filter. + * + * @param {GLShader} shader - The underlying gl shader. + * @param {PIXI.Filter} filter - The filter we are synchronizing. + */ + syncUniforms(shader, filter) + { + const uniformData = filter.uniformData; + const uniforms = filter.uniforms; + + // 0 is reserved for the pixi texture so we start at 1! + let textureCount = 1; + let currentState; + + // filterArea and filterClamp that are handled by FilterManager directly + // they must not appear in uniformData + + if (shader.uniforms.filterArea) + { + currentState = this.filterData.stack[this.filterData.index]; + + const filterArea = shader.uniforms.filterArea; + + filterArea[0] = currentState.renderTarget.size.width; + filterArea[1] = currentState.renderTarget.size.height; + filterArea[2] = currentState.sourceFrame.x; + filterArea[3] = currentState.sourceFrame.y; + + shader.uniforms.filterArea = filterArea; + } + + // use this to clamp displaced texture coords so they belong to filterArea + // see displacementFilter fragment shader for an example + if (shader.uniforms.filterClamp) + { + currentState = currentState || this.filterData.stack[this.filterData.index]; + + const filterClamp = shader.uniforms.filterClamp; + + filterClamp[0] = 0; + filterClamp[1] = 0; + filterClamp[2] = (currentState.sourceFrame.width - 1) / currentState.renderTarget.size.width; + filterClamp[3] = (currentState.sourceFrame.height - 1) / currentState.renderTarget.size.height; + + shader.uniforms.filterClamp = filterClamp; + } + + // TODO Cacheing layer.. + for (const i in uniformData) + { + if (uniformData[i].type === 'sampler2D' && uniforms[i] !== 0) + { + if (uniforms[i].baseTexture) + { + shader.uniforms[i] = this.renderer.bindTexture(uniforms[i].baseTexture, textureCount); + } + else + { + shader.uniforms[i] = textureCount; + + // TODO + // this is helpful as renderTargets can also be set. + // Although thinking about it, we could probably + // make the filter texture cache return a RenderTexture + // rather than a renderTarget + const gl = this.renderer.gl; + + this.renderer.boundTextures[textureCount] = this.renderer.emptyTextures[textureCount]; + gl.activeTexture(gl.TEXTURE0 + textureCount); + + uniforms[i].texture.bind(); + } + + textureCount++; + } + else if (uniformData[i].type === 'mat3') + { + // check if its pixi matrix.. + if (uniforms[i].a !== undefined) + { + shader.uniforms[i] = uniforms[i].toArray(true); + } + else + { + shader.uniforms[i] = uniforms[i]; + } + } + else if (uniformData[i].type === 'vec2') + { + // check if its a point.. + if (uniforms[i].x !== undefined) + { + const val = shader.uniforms[i] || new Float32Array(2); + + val[0] = uniforms[i].x; + val[1] = uniforms[i].y; + shader.uniforms[i] = val; + } + else + { + shader.uniforms[i] = uniforms[i]; + } + } + else if (uniformData[i].type === 'float') + { + if (shader.uniforms.data[i].value !== uniformData[i]) + { + shader.uniforms[i] = uniforms[i]; + } + } + else + { + shader.uniforms[i] = uniforms[i]; + } + } + } + + /** * Gets a render target from the pool, or creates a new one. * * @param {boolean} clear - Should we clear the render texture when we get it? @@ -442,7 +541,6 @@ renderTarget.resolution = resolution; renderTarget.defaultFrame.width = renderTarget.size.width = minWidth / resolution; renderTarget.defaultFrame.height = renderTarget.size.height = minHeight / resolution; - renderTarget.attachStencilBuffer(); return renderTarget; } diff --git a/src/core/settings.js b/src/core/settings.js index bb08cb0..f4dd087 100644 --- a/src/core/settings.js +++ b/src/core/settings.js @@ -77,11 +77,11 @@ * * @static * @memberof PIXI.settings - * @type {RegExp|string} + * @type {RegExp} * @example `@2x` - * @default /@(.+)x/ + * @default /@([0-9\.]+)x/ */ - RETINA_PREFIX: /@(.+)x/, + RETINA_PREFIX: /@([0-9\.]+)x/, /** * The default render options if none are supplied to {@link PIXI.WebGLRenderer} @@ -175,14 +175,24 @@ SCALE_MODE: 0, /** - * Default specify float precision in shaders. + * Default specify float precision in vertex shader. + * + * @static + * @memberof PIXI.settings + * @type {PIXI.PRECISION} + * @default PIXI.PRECISION.HIGH + */ + PRECISION_VERTEX: 'highp', + + /** + * Default specify float precision in fragment shader. * * @static * @memberof PIXI.settings * @type {PIXI.PRECISION} * @default PIXI.PRECISION.MEDIUM */ - PRECISION: 'mediump', + PRECISION_FRAGMENT: 'mediump', /** * Can we upload the same buffer in a single frame? diff --git a/src/core/sprites/Sprite.js b/src/core/sprites/Sprite.js index 28b0b18..121bf58 100644 --- a/src/core/sprites/Sprite.js +++ b/src/core/sprites/Sprite.js @@ -212,11 +212,11 @@ } else { - w0 = orig.width * (1 - anchor._x); - w1 = orig.width * -anchor._x; + w1 = -anchor._x * orig.width; + w0 = w1 + orig.width; - h0 = orig.height * (1 - anchor._y); - h1 = orig.height * -anchor._y; + h1 = -anchor._y * orig.height; + h0 = h1 + orig.height; } // xy @@ -269,11 +269,11 @@ const tx = wt.tx; const ty = wt.ty; - const w0 = (orig.width) * (1 - anchor._x); - const w1 = (orig.width) * -anchor._x; + const w1 = -anchor._x * orig.width; + const w0 = w1 + orig.width; - const h0 = orig.height * (1 - anchor._y); - const h1 = orig.height * -anchor._y; + const h1 = -anchor._y * orig.height; + const h0 = h1 + orig.height; // xy vertexData[0] = (a * w1) + (c * h1) + tx; @@ -440,7 +440,7 @@ * * @static * @param {number|string|PIXI.BaseTexture|HTMLCanvasElement|HTMLVideoElement} source Source to create texture from - * @return {PIXI.Sprite} The newly created texture + * @return {PIXI.Sprite} The newly created sprite */ static from(source) { @@ -538,8 +538,8 @@ } /** - * The tint applied to the sprite. This is a hex value. A value of - * 0xFFFFFF will remove any tint effect. + * The tint applied to the sprite. This is a hex value. + * A value of 0xFFFFFF will remove any tint effect. * * @member {number} * @default 0xFFFFFF diff --git a/src/core/sprites/canvas/CanvasTinter.js b/src/core/sprites/canvas/CanvasTinter.js index 1cf316b..27e5e0c 100644 --- a/src/core/sprites/canvas/CanvasTinter.js +++ b/src/core/sprites/canvas/CanvasTinter.js @@ -75,8 +75,8 @@ crop.width *= resolution; crop.height *= resolution; - canvas.width = crop.width; - canvas.height = crop.height; + canvas.width = Math.ceil(crop.width); + canvas.height = Math.ceil(crop.height); context.fillStyle = `#${(`00000${(color | 0).toString(16)}`).substr(-6)}`; @@ -130,8 +130,8 @@ crop.width *= resolution; crop.height *= resolution; - canvas.width = crop.width; - canvas.height = crop.height; + canvas.width = Math.ceil(crop.width); + canvas.height = Math.ceil(crop.height); context.globalCompositeOperation = 'copy'; context.fillStyle = `#${(`00000${(color | 0).toString(16)}`).substr(-6)}`; @@ -172,8 +172,8 @@ crop.width *= resolution; crop.height *= resolution; - canvas.width = crop.width; - canvas.height = crop.height; + canvas.width = Math.ceil(crop.width); + canvas.height = Math.ceil(crop.height); context.globalCompositeOperation = 'copy'; context.drawImage( diff --git a/src/core/sprites/webgl/SpriteRenderer.js b/src/core/sprites/webgl/SpriteRenderer.js index 2affcd5..7c352e2 100644 --- a/src/core/sprites/webgl/SpriteRenderer.js +++ b/src/core/sprites/webgl/SpriteRenderer.js @@ -30,7 +30,7 @@ /** * Number of values sent in the vertex buffer. - * positionX, positionY, colorR, colorG, colorB = 5 + * aVertexPosition(2), aTextureCoord(1), aColor(1), aTextureId(1) = 5 * * @member {number} */ @@ -75,7 +75,6 @@ this.shader = null; this.currentIndex = 0; - TICK = 0; this.groups = []; for (let k = 0; k < this.size; k++) diff --git a/src/core/textures/BaseTexture.js b/src/core/textures/BaseTexture.js index 853de7e..aeccf39 100644 --- a/src/core/textures/BaseTexture.js +++ b/src/core/textures/BaseTexture.js @@ -232,16 +232,24 @@ this.realWidth = this.source.naturalWidth || this.source.videoWidth || this.source.width; this.realHeight = this.source.naturalHeight || this.source.videoHeight || this.source.height; - this.width = this.realWidth / this.resolution; - this.height = this.realHeight / this.resolution; - - this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); + this._updateDimensions(); } this.emit('update', this); } /** + * Update dimensions from real values + */ + _updateDimensions() + { + this.width = this.realWidth / this.resolution; + this.height = this.realHeight / this.resolution; + + this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); + } + + /** * Load a source. * * If the source is not-immediately-available, such as an image that needs to be @@ -525,11 +533,7 @@ this.realWidth = Math.round(svgWidth * this.sourceScale); this.realHeight = Math.round(svgHeight * this.sourceScale); - this.width = this.realWidth / this.resolution; - this.height = this.realHeight / this.resolution; - - // Check pow2 after scale - this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); + this._updateDimensions(); // Create a canvas element const canvas = document.createElement('canvas'); @@ -690,4 +694,52 @@ return baseTexture; } + + /** + * Helper function that creates a base texture based on the source you provide. + * The source can be - image url, image element, canvas element. + * + * @static + * @param {string|HTMLImageElement|HTMLCanvasElement} source - The source to create base texture from. + * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values + * @param {number} [sourceScale=(auto)] - Scale for the original image, used with Svg images. + * @return {PIXI.BaseTexture} The new base texture. + */ + static from(source, scaleMode, sourceScale) + { + if (typeof source === 'string') + { + return BaseTexture.fromImage(source, undefined, scaleMode, sourceScale); + } + else if (source instanceof HTMLImageElement) + { + const imageUrl = source.src; + let baseTexture = BaseTextureCache[imageUrl]; + + if (!baseTexture) + { + baseTexture = new BaseTexture(source, scaleMode); + baseTexture.imageUrl = imageUrl; + + if (sourceScale) + { + baseTexture.sourceScale = sourceScale; + } + + // if there is an @2x at the end of the url we are going to assume its a highres image + baseTexture.resolution = getResolutionOfUrl(imageUrl); + + BaseTextureCache[imageUrl] = baseTexture; + } + + return baseTexture; + } + else if (source instanceof HTMLCanvasElement) + { + return BaseTexture.fromCanvas(source, scaleMode); + } + + // lets assume its a base texture! + return source; + } } diff --git a/src/core/textures/Spritesheet.js b/src/core/textures/Spritesheet.js new file mode 100644 index 0000000..d469c62 --- /dev/null +++ b/src/core/textures/Spritesheet.js @@ -0,0 +1,274 @@ +import { Rectangle, Texture } from '../'; +import { getResolutionOfUrl, TextureCache } from '../utils'; + +/** + * Utility class for maintaining reference to a collection + * of Textures on a single Spritesheet. + * + * @class + * @memberof PIXI + */ +export default class Spritesheet +{ + /** + * The maximum number of Textures to build per process. + * + * @type {number} + * @default 1000 + */ + static get BATCH_SIZE() + { + return 1000; + } + + /** + * @param {PIXI.BaseTexture} baseTexture Reference to the source BaseTexture object. + * @param {Object} data - Spritesheet image data. + * @param {string} [resolutionFilename] - The filename to consider when determining + * the resolution of the spritesheet. If not provided, the imageUrl will + * be used on the BaseTexture. + */ + constructor(baseTexture, data, resolutionFilename = null) + { + /** + * Reference to ths source texture + * @type {PIXI.BaseTexture} + */ + this.baseTexture = baseTexture; + + /** + * Map of spritesheet textures. + * @type {Object} + */ + this.textures = {}; + + /** + * Reference to the original JSON data. + * @type {Object} + */ + this.data = data; + + /** + * The resolution of the spritesheet. + * @type {number} + */ + this.resolution = this._updateResolution( + resolutionFilename || this.baseTexture.imageUrl + ); + + /** + * Map of spritesheet frames. + * @type {Object} + * @private + */ + this._frames = this.data.frames; + + /** + * Collection of frame names. + * @type {string[]} + * @private + */ + this._frameKeys = Object.keys(this._frames); + + /** + * Current batch index being processed. + * @type {number} + * @private + */ + this._batchIndex = 0; + + /** + * Callback when parse is completed. + * @type {Function} + * @private + */ + this._callback = null; + } + + /** + * Generate the resolution from the filename or fallback + * to the meta.scale field of the JSON data. + * + * @private + * @param {string} resolutionFilename - The filename to use for resolving + * the default resolution. + * @return {number} Resolution to use for spritesheet. + */ + _updateResolution(resolutionFilename) + { + const scale = this.data.meta.scale; + + // Use a defaultValue of `null` to check if a url-based resolution is set + let resolution = getResolutionOfUrl(resolutionFilename, null); + + // No resolution found via URL + if (resolution === null) + { + // Use the scale value or default to 1 + resolution = scale !== undefined ? parseFloat(scale) : 1; + } + + // For non-1 resolutions, update baseTexture + if (resolution !== 1) + { + this.baseTexture.resolution = resolution; + this.baseTexture.update(); + } + + return resolution; + } + + /** + * Parser spritesheet from loaded data. This is done asynchronously + * to prevent creating too many Texture within a single process. + * + * @param {Function} callback - Callback when complete returns + * a map of the Textures for this spritesheet. + */ + parse(callback) + { + this._batchIndex = 0; + this._callback = callback; + + if (this._frameKeys.length <= Spritesheet.BATCH_SIZE) + { + this._processFrames(0); + this._parseComplete(); + } + else + { + this._nextBatch(); + } + } + + /** + * Process a batch of frames + * + * @private + * @param {number} initialFrameIndex - The index of frame to start. + */ + _processFrames(initialFrameIndex) + { + let frameIndex = initialFrameIndex; + const maxFrames = Spritesheet.BATCH_SIZE; + + while (frameIndex - initialFrameIndex < maxFrames && frameIndex < this._frameKeys.length) + { + const i = this._frameKeys[frameIndex]; + const rect = this._frames[i].frame; + + if (rect) + { + let frame = null; + let trim = null; + const orig = new Rectangle( + 0, + 0, + this._frames[i].sourceSize.w / this.resolution, + this._frames[i].sourceSize.h / this.resolution + ); + + if (this._frames[i].rotated) + { + frame = new Rectangle( + rect.x / this.resolution, + rect.y / this.resolution, + rect.h / this.resolution, + rect.w / this.resolution + ); + } + else + { + frame = new Rectangle( + rect.x / this.resolution, + rect.y / this.resolution, + rect.w / this.resolution, + rect.h / this.resolution + ); + } + + // Check to see if the sprite is trimmed + if (this._frames[i].trimmed) + { + trim = new Rectangle( + this._frames[i].spriteSourceSize.x / this.resolution, + this._frames[i].spriteSourceSize.y / this.resolution, + rect.w / this.resolution, + rect.h / this.resolution + ); + } + + this.textures[i] = new Texture( + this.baseTexture, + frame, + orig, + trim, + this._frames[i].rotated ? 2 : 0 + ); + + // lets also add the frame to pixi's global cache for fromFrame and fromImage functions + TextureCache[i] = this.textures[i]; + } + + frameIndex++; + } + } + + /** + * The parse has completed. + * + * @private + */ + _parseComplete() + { + const callback = this._callback; + + this._callback = null; + this._batchIndex = 0; + callback.call(this, this.textures); + } + + /** + * Begin the next batch of textures. + * + * @private + */ + _nextBatch() + { + this._processFrames(this._batchIndex * Spritesheet.BATCH_SIZE); + this._batchIndex++; + setTimeout(() => + { + if (this._batchIndex * Spritesheet.BATCH_SIZE < this._frameKeys.length) + { + this._nextBatch(); + } + else + { + this._parseComplete(); + } + }, 0); + } + + /** + * Destroy Spritesheet and don't use after this. + * + * @param {boolean} [destroyBase=false] Whether to destroy the base texture as well + */ + destroy(destroyBase = false) + { + for (const i in this.textures) + { + this.textures[i].destroy(); + } + this._frames = null; + this._frameKeys = null; + this.data = null; + this.textures = null; + if (destroyBase) + { + this.baseTexture.destroy(); + } + this.baseTexture = null; + } +} diff --git a/src/core/textures/Texture.js b/src/core/textures/Texture.js index 6403d4f..afe4254 100644 --- a/src/core/textures/Texture.js +++ b/src/core/textures/Texture.js @@ -3,7 +3,7 @@ import TextureUvs from './TextureUvs'; import EventEmitter from 'eventemitter3'; import { Rectangle } from '../math'; -import { TextureCache, BaseTextureCache } from '../utils'; +import { TextureCache, BaseTextureCache, getResolutionOfUrl } from '../utils'; /** * A texture stores the information that represents an image or part of an image. It cannot be added @@ -365,7 +365,8 @@ * The source can be - frame id, image url, video url, canvas element, video element, base texture * * @static - * @param {number|string|PIXI.BaseTexture|HTMLCanvasElement|HTMLVideoElement} source - Source to create texture from + * @param {number|string|HTMLImageElement|HTMLCanvasElement|HTMLVideoElement|PIXI.BaseTexture} + * source - Source to create texture from * @return {PIXI.Texture} The newly created texture */ static from(source) @@ -393,7 +394,7 @@ } else if (source instanceof HTMLImageElement) { - return new Texture(new BaseTexture(source)); + return new Texture(BaseTexture.from(source)); } else if (source instanceof HTMLCanvasElement) { @@ -413,6 +414,43 @@ } /** + * Create a texture from a source and add to the cache. + * + * @static + * @param {HTMLImageElement|HTMLCanvasElement} source - The input source. + * @param {String} imageUrl - File name of texture, for cache and resolving resolution. + * @param {String} [name] - Human readible name for the texture cache. If no name is + * specified, only `imageUrl` will be used as the cache ID. + * @return {PIXI.Texture} Output texture + */ + static fromLoader(source, imageUrl, name) + { + const baseTexture = new BaseTexture(source, null, getResolutionOfUrl(imageUrl)); + const texture = new Texture(baseTexture); + + baseTexture.imageUrl = imageUrl; + + // No name, use imageUrl instead + if (!name) + { + name = imageUrl; + } + + // lets also add the frame to pixi's global cache for fromFrame and fromImage fucntions + BaseTextureCache[name] = baseTexture; + TextureCache[name] = texture; + + // also add references by url if they are different. + if (name !== imageUrl) + { + BaseTextureCache[imageUrl] = baseTexture; + TextureCache[imageUrl] = texture; + } + + return texture; + } + + /** * Adds a texture to the global TextureCache. This cache is shared across the whole PIXI object. * * @static @@ -522,6 +560,29 @@ } } +function createWhiteTexture() +{ + const canvas = document.createElement('canvas'); + + canvas.width = 10; + canvas.height = 10; + + const context = canvas.getContext('2d'); + + context.fillStyle = 'white'; + context.fillRect(0, 0, 10, 10); + + return new Texture(new BaseTexture(canvas)); +} + +function removeAllHandlers(tex) +{ + tex.destroy = function _emptyDestroy() { /* empty */ }; + tex.on = function _emptyOn() { /* empty */ }; + tex.once = function _emptyOnce() { /* empty */ }; + tex.emit = function _emptyEmit() { /* empty */ }; +} + /** * An empty texture, used often to not have to create multiple empty textures. * Can not be destroyed. @@ -530,7 +591,14 @@ * @constant */ Texture.EMPTY = new Texture(new BaseTexture()); -Texture.EMPTY.destroy = function _emptyDestroy() { /* empty */ }; -Texture.EMPTY.on = function _emptyOn() { /* empty */ }; -Texture.EMPTY.once = function _emptyOnce() { /* empty */ }; -Texture.EMPTY.emit = function _emptyEmit() { /* empty */ }; +removeAllHandlers(Texture.EMPTY); + +/** + * A white texture of 10x10 size, used for graphics and other things + * Can not be destroyed. + * + * @static + * @constant + */ +Texture.WHITE = createWhiteTexture(); +removeAllHandlers(Texture.WHITE); diff --git a/src/core/ticker/Ticker.js b/src/core/ticker/Ticker.js index d98a1f0..17a1517 100644 --- a/src/core/ticker/Ticker.js +++ b/src/core/ticker/Ticker.js @@ -67,11 +67,12 @@ * is based, this value is neither capped nor scaled. * If the platform supports DOMHighResTimeStamp, * this value will have a precision of 1 µs. + * Defaults to target frame time * * @member {number} - * @default 1 / TARGET_FPMS + * @default 16.66 */ - this.elapsedMS = 1 / settings.TARGET_FPMS; // default to target frame time + this.elapsedMS = 1 / settings.TARGET_FPMS; /** * The last time {@link PIXI.ticker.Ticker#update} was invoked. diff --git a/package.json b/package.json index 6a366ba..71ade15 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pixi.js", - "version": "4.3.4", + "version": "4.3.5", "description": "Pixi.js is a fast lightweight 2D library that works across all devices.", "author": "Mat Groves", "contributors": [ @@ -61,7 +61,7 @@ "ismobilejs": "^0.4.0", "object-assign": "^4.0.1", "pixi-gl-core": "^1.0.3", - "resource-loader": "^2.0.4" + "resource-loader": "^2.0.6" }, "devDependencies": { "babel-cli": "^6.18.0", @@ -70,9 +70,9 @@ "babel-preset-es2015": "^6.14.0", "babelify": "^7.3.0", "del": "^2.2.0", - "electron-prebuilt": "^1.3.2", + "electron": "^1.4.15", "eslint": "^3.5.0", - "floss": "^1.2.0", + "floss": "^2.0.1", "gh-pages": "^0.11.0", "jaguarjs-jsdoc": "^1.0.1", "js-md5": "^0.4.1", diff --git a/src/accessibility/AccessibilityManager.js b/src/accessibility/AccessibilityManager.js index df2716a..f16bcb1 100644 --- a/src/accessibility/AccessibilityManager.js +++ b/src/accessibility/AccessibilityManager.js @@ -3,7 +3,7 @@ import accessibleTarget from './accessibleTarget'; // add some extra variables to the container.. -Object.assign( +core.utils.mixins.delayMixin( core.DisplayObject.prototype, accessibleTarget ); diff --git a/src/core/Application.js b/src/core/Application.js index ea0f058..5340578 100644 --- a/src/core/Application.js +++ b/src/core/Application.js @@ -1,6 +1,6 @@ import { autoDetectRenderer } from './autoDetectRenderer'; import Container from './display/Container'; -import Ticker from './ticker/Ticker'; +import { shared, Ticker } from './ticker'; /** * Convenience class to create a new PIXI application. @@ -35,9 +35,9 @@ * @param {boolean} [noWebGL=false] - prevents selection of WebGL renderer, even if such is present * @param {boolean} [options.legacy=false] - If true Pixi will aim to ensure compatibility * with older / less advanced devices. If you experience unexplained flickering try setting this to true. - + * @param {boolean} [useSharedTicker=false] - `true` to use PIXI.ticker.shared, `false` to create new ticker. */ - constructor(width, height, options, noWebGL) + constructor(width, height, options, noWebGL, useSharedTicker = false) { /** * WebGL renderer if available, otherwise CanvasRenderer @@ -46,23 +46,46 @@ this.renderer = autoDetectRenderer(width, height, options, noWebGL); /** - * The root display container that's renderered. + * The root display container that's rendered. * @member {PIXI.Container} */ this.stage = new Container(); /** + * Internal reference to the ticker + * @member {PIXI.ticker.Ticker} + * @private + */ + this._ticker = null; + + /** * Ticker for doing render updates. * @member {PIXI.ticker.Ticker} + * @default PIXI.ticker.shared */ - this.ticker = new Ticker(); - - this.ticker.add(this.render, this); + this.ticker = useSharedTicker ? shared : new Ticker(); // Start the rendering this.start(); } + set ticker(ticker) // eslint-disable-line require-jsdoc + { + if (this._ticker) + { + this._ticker.remove(this.render, this); + } + this._ticker = ticker; + if (ticker) + { + ticker.add(this.render, this); + } + } + get ticker() // eslint-disable-line require-jsdoc + { + return this._ticker; + } + /** * Render the current stage. */ @@ -76,7 +99,7 @@ */ stop() { - this.ticker.stop(); + this._ticker.stop(); } /** @@ -84,7 +107,7 @@ */ start() { - this.ticker.start(); + this._ticker.start(); } /** @@ -98,13 +121,22 @@ } /** + * Reference to the renderer's screen rectangle. Its safe to use as filterArea or hitArea for whole screen + * @member {PIXI.Rectangle} + * @readonly + */ + get screen() + { + return this.renderer.screen; + } + + /** * Destroy and don't use after this. * @param {Boolean} [removeView=false] Automatically remove canvas from DOM. */ destroy(removeView) { this.stop(); - this.ticker.remove(this.render, this); this.ticker = null; this.stage.destroy(); diff --git a/src/core/Shader.js b/src/core/Shader.js new file mode 100644 index 0000000..78b17e6 --- /dev/null +++ b/src/core/Shader.js @@ -0,0 +1,46 @@ +import { GLShader } from 'pixi-gl-core'; +import settings from './settings'; + +function checkPrecision(src, def) +{ + if (src instanceof Array) + { + if (src[0].substring(0, 9) !== 'precision') + { + const copy = src.slice(0); + + copy.unshift(`precision ${def} float;`); + + return copy; + } + } + else if (src.substring(0, 9) !== 'precision') + { + return `precision ${def} float;\n${src}`; + } + + return src; +} + +/** + * Wrapper class, webGL Shader for Pixi. + * Adds precision string if vertexSrc or fragmentSrc have no mention of it. + * + * @class + * @extends GLShader + * @memberof PIXI + */ +export default class Shader extends GLShader +{ + /** + * + * @param {WebGLRenderingContext} gl - The current WebGL rendering context + * @param {string|string[]} vertexSrc - The vertex shader source as an array of strings. + * @param {string|string[]} fragmentSrc - The fragment shader source as an array of strings. + */ + constructor(gl, vertexSrc, fragmentSrc) + { + super(gl, checkPrecision(vertexSrc, settings.PRECISION_VERTEX), + checkPrecision(fragmentSrc, settings.PRECISION_FRAGMENT)); + } +} diff --git a/src/core/const.js b/src/core/const.js index ef6f662..3de0efe 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -188,7 +188,7 @@ * The gc modes that are supported by pixi. * * The {@link PIXI.settings.GC_MODE} Garbage Collection mode for pixi textures is AUTO - * If set to GC_MODE, the renderer will occasianally check textures usage. If they are not + * If set to GC_MODE, the renderer will occasionally check textures usage. If they are not * used for a specified period of time they will be removed from the GPU. They will of course * be uploaded again when they are required. This is a silent behind the scenes process that * should ensure that the GPU does not get filled up. diff --git a/src/core/display/Container.js b/src/core/display/Container.js index ed8cd5f..17fde8d 100644 --- a/src/core/display/Container.js +++ b/src/core/display/Container.js @@ -73,11 +73,12 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.push(child); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -108,11 +109,12 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.splice(index, 0, child); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -225,10 +227,11 @@ if (index === -1) return null; child.parent = null; + // ensure child transform will be recalculated + child.transform._parentID = -1; removeItems(this.children, index, 1); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -249,11 +252,12 @@ { const child = this.getChildAt(index); + // ensure child transform will be recalculated.. child.parent = null; + child.transform._parentID = -1; removeItems(this.children, index, 1); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -284,13 +288,12 @@ for (let i = 0; i < removed.length; ++i) { removed[i].parent = null; + if (removed[i].transform) + { + removed[i].transform._parentID = -1; + } } - // ensure a transform will be recalculated.. - if (this.transform) - { - this.transform._parentID = -1; - } this._boundsID++; this.onChildrenChange(beginIndex); diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index f37f2f5..be4c4fb 100644 --- a/src/core/graphics/Graphics.js +++ b/src/core/graphics/Graphics.js @@ -28,8 +28,9 @@ { /** * + * @param {boolean} [nativeLines=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP */ - constructor() + constructor(nativeLines = false) { super(); @@ -50,6 +51,13 @@ this.lineWidth = 0; /** + * If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * + * @member {boolean} + */ + this.nativeLines = nativeLines; + + /** * The color of any lines drawn. * * @member {string} @@ -757,28 +765,14 @@ if (!this._spriteRect) { - if (!Graphics._SPRITE_TEXTURE) - { - Graphics._SPRITE_TEXTURE = RenderTexture.create(10, 10); - - const canvas = document.createElement('canvas'); - - canvas.width = 10; - canvas.height = 10; - - const context = canvas.getContext('2d'); - - context.fillStyle = 'white'; - context.fillRect(0, 0, 10, 10); - - Graphics._SPRITE_TEXTURE = Texture.fromCanvas(canvas); - } - - this._spriteRect = new Sprite(Graphics._SPRITE_TEXTURE); + this._spriteRect = new Sprite(new Texture(Texture.WHITE)); } + + const sprite = this._spriteRect; + if (this.tint === 0xffffff) { - this._spriteRect.tint = this.graphicsData[0].fillColor; + sprite.tint = this.graphicsData[0].fillColor; } else { @@ -792,20 +786,21 @@ t1[1] *= t2[1]; t1[2] *= t2[2]; - this._spriteRect.tint = rgb2hex(t1); + sprite.tint = rgb2hex(t1); } - this._spriteRect.alpha = this.graphicsData[0].fillAlpha; - this._spriteRect.worldAlpha = this.worldAlpha * this._spriteRect.alpha; + sprite.alpha = this.graphicsData[0].fillAlpha; + sprite.worldAlpha = this.worldAlpha * sprite.alpha; + sprite.blendMode = this.blendMode; - Graphics._SPRITE_TEXTURE._frame.width = rect.width; - Graphics._SPRITE_TEXTURE._frame.height = rect.height; + sprite.texture._frame.width = rect.width; + sprite.texture._frame.height = rect.height; - this._spriteRect.transform.worldTransform = this.transform.worldTransform; + sprite.transform.worldTransform = this.transform.worldTransform; - this._spriteRect.anchor.set(-rect.x / rect.width, -rect.y / rect.height); - this._spriteRect._onAnchorUpdate(); + sprite.anchor.set(-rect.x / rect.width, -rect.y / rect.height); + sprite._onAnchorUpdate(); - this._spriteRect._renderWebGL(renderer); + sprite._renderWebGL(renderer); } /** @@ -1031,6 +1026,7 @@ this.fillColor, this.fillAlpha, this.filling, + this.nativeLines, shape ); diff --git a/src/core/graphics/GraphicsData.js b/src/core/graphics/GraphicsData.js index f5a87ae..b6f03c0 100644 --- a/src/core/graphics/GraphicsData.js +++ b/src/core/graphics/GraphicsData.js @@ -14,14 +14,19 @@ * @param {number} fillColor - the color of the fill * @param {number} fillAlpha - the alpha of the fill * @param {boolean} fill - whether or not the shape is filled with a colour + * @param {boolean} nativeLines - the method for drawing lines * @param {PIXI.Circle|PIXI.Rectangle|PIXI.Ellipse|PIXI.Polygon} shape - The shape object to draw. */ - constructor(lineWidth, lineColor, lineAlpha, fillColor, fillAlpha, fill, shape) + constructor(lineWidth, lineColor, lineAlpha, fillColor, fillAlpha, fill, nativeLines, shape) { /** * @member {number} the width of the line to draw */ this.lineWidth = lineWidth; + /** + * @member {boolean} if true the liens will be draw using LINES instead of TRIANGLE_STRIP + */ + this.nativeLines = nativeLines; /** * @member {number} the color of the line to draw @@ -85,6 +90,7 @@ this.fillColor, this.fillAlpha, this.fill, + this.nativeLines, this.shape ); } diff --git a/src/core/graphics/webgl/GraphicsRenderer.js b/src/core/graphics/webgl/GraphicsRenderer.js index 14083fe..67f5e92 100644 --- a/src/core/graphics/webgl/GraphicsRenderer.js +++ b/src/core/graphics/webgl/GraphicsRenderer.js @@ -102,7 +102,15 @@ shaderTemp.uniforms.alpha = graphics.worldAlpha; renderer.bindVao(webGLData.vao); - webGLData.vao.draw(gl.TRIANGLE_STRIP, webGLData.indices.length); + + if (graphics.nativeLines) + { + gl.drawArrays(gl.LINES, 0, webGLData.points.length / 6); + } + else + { + webGLData.vao.draw(gl.TRIANGLE_STRIP, webGLData.indices.length); + } } } diff --git a/src/core/graphics/webgl/utils/buildLine.js b/src/core/graphics/webgl/utils/buildLine.js index ecf43f7..4d6862e 100644 --- a/src/core/graphics/webgl/utils/buildLine.js +++ b/src/core/graphics/webgl/utils/buildLine.js @@ -11,7 +11,29 @@ * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties * @param {object} webGLData - an object containing all the webGL-specific information to create this shape */ -export default function buildLine(graphicsData, webGLData) +export default function (graphicsData, webGLData) +{ + if (graphicsData.nativeLines) + { + buildNativeLine(graphicsData, webGLData); + } + else + { + buildLine(graphicsData, webGLData); + } +} + +/** + * Builds a line to draw using the poligon method. + * + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @private + * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties + * @param {object} webGLData - an object containing all the webGL-specific information to create this shape + */ +function buildLine(graphicsData, webGLData) { // TODO OPTIMISE! let points = graphicsData.points; @@ -224,3 +246,46 @@ indices.push(indexStart - 1); } + +/** + * Builds a line to draw using the gl.drawArrays(gl.LINES) method + * + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @private + * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties + * @param {object} webGLData - an object containing all the webGL-specific information to create this shape + */ +function buildNativeLine(graphicsData, webGLData) +{ + let i = 0; + const points = graphicsData.points; + + if (points.length === 0) return; + + const verts = webGLData.points; + const length = points.length / 2; + + // sort color + const color = hex2rgb(graphicsData.lineColor); + const alpha = graphicsData.lineAlpha; + const r = color[0] * alpha; + const g = color[1] * alpha; + const b = color[2] * alpha; + + for (i = 1; i < length; i++) + { + const p1x = points[(i - 1) * 2]; + const p1y = points[((i - 1) * 2) + 1]; + + const p2x = points[i * 2]; + const p2y = points[(i * 2) + 1]; + + verts.push(p1x, p1y); + verts.push(r, g, b, alpha); + + verts.push(p2x, p2y); + verts.push(r, g, b, alpha); + } +} diff --git a/src/core/graphics/webgl/utils/buildRoundedRectangle.js b/src/core/graphics/webgl/utils/buildRoundedRectangle.js index 9abc807..a6c8461 100644 --- a/src/core/graphics/webgl/utils/buildRoundedRectangle.js +++ b/src/core/graphics/webgl/utils/buildRoundedRectangle.js @@ -77,6 +77,26 @@ } /** + * Calculate a single point for a quadratic bezier curve. + * Utility function used by quadraticBezierCurve. + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @private + * @param {number} n1 - first number + * @param {number} n2 - second number + * @param {number} perc - percentage + * @return {number} the result + * + */ +function getPt(n1, n2, perc) +{ + const diff = n2 - n1; + + return n1 + (diff * perc); +} + +/** * Calculate the points for a quadratic bezier curve. (helper function..) * Based on: https://stackoverflow.com/questions/785097/how-do-i-implement-a-bezier-curve-in-c * @@ -105,13 +125,6 @@ let x = 0; let y = 0; - function getPt(n1, n2, perc) - { - const diff = n2 - n1; - - return n1 + (diff * perc); - } - for (let i = 0, j = 0; i <= n; ++i) { j = i / n; diff --git a/src/core/index.js b/src/core/index.js index f7a817c..4f414e8 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -30,6 +30,7 @@ export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; export { default as CanvasGraphicsRenderer } from './graphics/canvas/CanvasGraphicsRenderer'; +export { default as Spritesheet } from './textures/Spritesheet'; export { default as Texture } from './textures/Texture'; export { default as BaseTexture } from './textures/BaseTexture'; export { default as RenderTexture } from './textures/RenderTexture'; diff --git a/src/core/math/Matrix.js b/src/core/math/Matrix.js index 0b77477..3679860 100644 --- a/src/core/math/Matrix.js +++ b/src/core/math/Matrix.js @@ -13,45 +13,50 @@ export default class Matrix { /** - * + * @param {number} [a=1] - x scale + * @param {number} [b=0] - y skew + * @param {number} [c=0] - x skew + * @param {number} [d=1] - y scale + * @param {number} [tx=0] - x translation + * @param {number} [ty=0] - y translation */ - constructor() + constructor(a = 1, b = 0, c = 0, d = 1, tx = 0, ty = 0) { /** * @member {number} * @default 1 */ - this.a = 1; + this.a = a; /** * @member {number} * @default 0 */ - this.b = 0; + this.b = b; /** * @member {number} * @default 0 */ - this.c = 0; + this.c = c; /** * @member {number} * @default 1 */ - this.d = 1; + this.d = d; /** * @member {number} * @default 0 */ - this.tx = 0; + this.tx = tx; /** * @member {number} * @default 0 */ - this.ty = 0; + this.ty = ty; this.array = null; } diff --git a/src/core/renderers/SystemRenderer.js b/src/core/renderers/SystemRenderer.js index fa116a6..a24c775 100644 --- a/src/core/renderers/SystemRenderer.js +++ b/src/core/renderers/SystemRenderer.js @@ -1,5 +1,5 @@ import { sayHello, hex2string, hex2rgb } from '../utils'; -import { Matrix } from '../math'; +import { Matrix, Rectangle } from '../math'; import { RENDERER_TYPE } from '../const'; import settings from '../settings'; import Container from '../display/Container'; @@ -21,8 +21,8 @@ { /** * @param {string} system - The name of the system this renderer is for. - * @param {number} [width=800] - the width of the canvas view - * @param {number} [height=600] - the height of the canvas view + * @param {number} [screenWidth=800] - the width of the screen + * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false @@ -37,7 +37,7 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(system, width, height, options) + constructor(system, screenWidth, screenHeight, options) { super(); @@ -69,20 +69,13 @@ this.type = RENDERER_TYPE.UNKNOWN; /** - * The width of the canvas view + * Measurements of the screen. (0, 0, screenWidth, screenHeight) * - * @member {number} - * @default 800 - */ - this.width = width || 800; - - /** - * The height of the canvas view + * Its safe to use as filterArea or hitArea for whole stage * - * @member {number} - * @default 600 + * @member {PIXI.Rectangle} */ - this.height = height || 600; + this.screen = new Rectangle(0, 0, screenWidth || 800, screenHeight || 600); /** * The canvas element that everything is drawn to @@ -107,7 +100,7 @@ this.transparent = options.transparent; /** - * Whether the render view should be resized automatically + * Whether css dimensions of canvas view should be resized to screen dimensions automatically * * @member {boolean} */ @@ -192,23 +185,48 @@ } /** - * Resizes the canvas view to the specified width and height + * Same as view.width, actual number of pixels in the canvas by horizontal * - * @param {number} width - the new width of the canvas view - * @param {number} height - the new height of the canvas view + * @member {number} + * @readonly + * @default 800 */ - resize(width, height) + get width() { - this.width = width * this.resolution; - this.height = height * this.resolution; + return this.view.width; + } - this.view.width = this.width; - this.view.height = this.height; + /** + * Same as view.height, actual number of pixels in the canvas by vertical + * + * @member {number} + * @readonly + * @default 600 + */ + get height() + { + return this.view.height; + } + + /** + * Resizes the screen and canvas to the specified width and height + * Canvas dimensions are multiplied by resolution + * + * @param {number} screenWidth - the new width of the screen + * @param {number} screenHeight - the new height of the screen + */ + resize(screenWidth, screenHeight) + { + this.screen.width = screenWidth; + this.screen.height = screenHeight; + + this.view.width = screenWidth * this.resolution; + this.view.height = screenHeight * this.resolution; if (this.autoResize) { - this.view.style.width = `${this.width / this.resolution}px`; - this.view.style.height = `${this.height / this.resolution}px`; + this.view.style.width = `${screenWidth}px`; + this.view.style.height = `${screenHeight}px`; } } @@ -249,11 +267,10 @@ this.type = RENDERER_TYPE.UNKNOWN; - this.width = 0; - this.height = 0; - this.view = null; + this.screen = null; + this.resolution = 0; this.transparent = false; diff --git a/src/core/renderers/canvas/CanvasRenderer.js b/src/core/renderers/canvas/CanvasRenderer.js index 1bd2d5e..e84e49c 100644 --- a/src/core/renderers/canvas/CanvasRenderer.js +++ b/src/core/renderers/canvas/CanvasRenderer.js @@ -18,8 +18,8 @@ export default class CanvasRenderer extends SystemRenderer { /** - * @param {number} [width=800] - the width of the canvas view - * @param {number} [height=600] - the height of the canvas view + * @param {number} [screenWidth=800] - the width of the screen + * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false @@ -34,9 +34,9 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(width, height, options = {}) + constructor(screenWidth, screenHeight, options = {}) { - super('Canvas', width, height, options); + super('Canvas', screenWidth, screenHeight, options); this.type = RENDERER_TYPE.CANVAS; @@ -96,7 +96,7 @@ this.context = null; this.renderingToScreen = false; - this.resize(width, height); + this.resize(screenWidth, screenHeight); } /** @@ -283,12 +283,12 @@ * * @extends PIXI.SystemRenderer#resize * - * @param {number} width - The new width of the canvas view - * @param {number} height - The new height of the canvas view + * @param {number} screenWidth - the new width of the screen + * @param {number} screenHeight - the new height of the screen */ - resize(width, height) + resize(screenWidth, screenHeight) { - super.resize(width, height); + super.resize(screenWidth, screenHeight); // reset the scale mode.. oddly this seems to be reset when the canvas is resized. // surely a browser bug?? Let pixi fix that for you.. diff --git a/src/core/renderers/webgl/TextureManager.js b/src/core/renderers/webgl/TextureManager.js index b685c1a..8227289 100644 --- a/src/core/renderers/webgl/TextureManager.js +++ b/src/core/renderers/webgl/TextureManager.js @@ -61,7 +61,7 @@ * Updates and/or Creates a WebGL texture for the renderer's context. * * @param {PIXI.BaseTexture|PIXI.Texture} texture - the texture to update - * @param {Number} location - the location the texture will be bound to. + * @param {number} location - the location the texture will be bound to. * @return {GLTexture} The gl texture. */ updateTexture(texture, location) diff --git a/src/core/renderers/webgl/WebGLRenderer.js b/src/core/renderers/webgl/WebGLRenderer.js index e4cd8a0..830b382 100644 --- a/src/core/renderers/webgl/WebGLRenderer.js +++ b/src/core/renderers/webgl/WebGLRenderer.js @@ -31,8 +31,8 @@ { /** * - * @param {number} [width=0] - the width of the canvas view - * @param {number} [height=0] - the height of the canvas view + * @param {number} [screenWidth=800] - the width of the screen + * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false @@ -53,9 +53,9 @@ * @param {boolean} [options.legacy=false] - If true Pixi will aim to ensure compatibility * with older / less advanced devices. If you experiance unexplained flickering try setting this to true. */ - constructor(width, height, options = {}) + constructor(screenWidth, screenHeight, options = {}) { - super('WebGL', width, height, options); + super('WebGL', screenWidth, screenHeight, options); this.legacy = !!options.legacy; @@ -245,7 +245,7 @@ glCore._testingContext = gl; // setup the width/height properties and gl viewport - this.resize(this.width, this.height); + this.resize(this.screen.width, this.screen.height); } /** @@ -343,16 +343,16 @@ /** * Resizes the webGL view to the specified width and height. * - * @param {number} width - the new width of the webGL view - * @param {number} height - the new height of the webGL view + * @param {number} screenWidth - the new width of the screen + * @param {number} screenHeight - the new height of the screen */ - resize(width, height) + resize(screenWidth, screenHeight) { // if(width * this.resolution === this.width && height * this.resolution === this.height)return; - SystemRenderer.prototype.resize.call(this, width, height); + SystemRenderer.prototype.resize.call(this, screenWidth, screenHeight); - this.rootRenderTarget.resize(width, height); + this.rootRenderTarget.resize(screenWidth, screenHeight); if (this._activeRenderTarget === this.rootRenderTarget) { @@ -396,6 +396,26 @@ } /** + * Erases the render texture and fills the drawing area with a colour + * + * @param {PIXI.RenderTexture} renderTexture - The render texture to clear + * @param {number} [clearColor] - The colour + * @return {PIXI.WebGLRenderer} Returns itself. + */ + clearRenderTexture(renderTexture, clearColor) + { + const baseTexture = renderTexture.baseTexture; + const renderTarget = baseTexture._glRenderTargets[this.CONTEXT_UID]; + + if (renderTarget) + { + renderTarget.clear(clearColor); + } + + return this; + } + + /** * Binds a render texture for rendering * * @param {PIXI.RenderTexture} renderTexture - The render texture to render @@ -474,9 +494,10 @@ * Changes the current GLShader to the one given in parameter * * @param {PIXI.glCore.Shader} shader - the new glShader + * @param {boolean} [autoProject=true] - Whether automatically set the projection matrix * @return {PIXI.WebGLRenderer} Returns itself. */ - _bindGLShader(shader) + _bindGLShader(shader, autoProject) { // TODO cache if (this._activeShader !== shader) @@ -484,8 +505,14 @@ this._activeShader = shader; shader.bind(); - // automatically set the projection matrix - shader.uniforms.projectionMatrix = this._activeRenderTarget.projectionMatrix.toArray(true); + // `autoProject` normally would be a default parameter set to true + // but because of how Babel transpiles default parameters + // it hinders the performance of this method. + if (autoProject !== false) + { + // automatically set the projection matrix + shader.uniforms.projectionMatrix = this._activeRenderTarget.projectionMatrix.toArray(true); + } } return this; diff --git a/src/core/renderers/webgl/WebGLState.js b/src/core/renderers/webgl/WebGLState.js index ba6ecc5..6c846d5 100755 --- a/src/core/renderers/webgl/WebGLState.js +++ b/src/core/renderers/webgl/WebGLState.js @@ -82,18 +82,20 @@ push() { // next state.. - let state = this.stack[++this.stackIndex]; + let state = this.stack[this.stackIndex]; if (!state) { state = this.stack[this.stackIndex] = new Uint8Array(16); } + ++this.stackIndex; + // copy state.. // set active state so we can force overrides of gl state for (let i = 0; i < this.activeState.length; i++) { - this.activeState[i] = state[i]; + state[i] = this.activeState[i]; } } diff --git a/src/core/renderers/webgl/filters/Filter.js b/src/core/renderers/webgl/filters/Filter.js index 2c3eaad..4eb6128 100644 --- a/src/core/renderers/webgl/filters/Filter.js +++ b/src/core/renderers/webgl/filters/Filter.js @@ -9,7 +9,7 @@ * @memberof PIXI * @extends PIXI.Shader */ -class Filter extends Shader +export default class Filter extends Shader { /** * @param {string} [vertexSrc] - The source of the vertex shader. @@ -56,15 +56,16 @@ * @param {PIXI.RenderTarget} input - The input render target. * @param {PIXI.RenderTarget} output - The target to output to. * @param {boolean} clear - Should the output be cleared before rendering to it + * @param {object} [currentState] - It's current state of filter. + * There are some useful properties in the currentState : + * target, filters, sourceFrame, destinationFrame, renderTarget, resolution */ - apply(filterManager, input, output, clear) + apply(filterManager, input, output, clear, currentState) // eslint-disable-line no-unused-vars { // do as you please! - filterManager.applyFilter(this, input, output, clear); + filterManager.applyFilter(this, input, output, clear, currentState); // or just do a regular render.. } } - -export default Filter; diff --git a/src/core/renderers/webgl/filters/extractUniformsFromSrc.js b/src/core/renderers/webgl/filters/extractUniformsFromSrc.js index d78a0b1..4893214 100644 --- a/src/core/renderers/webgl/filters/extractUniformsFromSrc.js +++ b/src/core/renderers/webgl/filters/extractUniformsFromSrc.js @@ -12,7 +12,7 @@ function extractUniformsFromString(string) { - const maskRegex = new RegExp('^(projectionMatrix|uSampler|filterArea)$'); + const maskRegex = new RegExp('^(projectionMatrix|uSampler|filterArea|filterClamp)$'); const uniforms = {}; let nameSplit; diff --git a/src/core/renderers/webgl/managers/FilterManager.js b/src/core/renderers/webgl/managers/FilterManager.js index 43fc869..6c7422d 100644 --- a/src/core/renderers/webgl/managers/FilterManager.js +++ b/src/core/renderers/webgl/managers/FilterManager.js @@ -2,6 +2,7 @@ import RenderTarget from '../utils/RenderTarget'; import Quad from '../utils/Quad'; import { Rectangle } from '../../../math'; +import Shader from '../../../Shader'; import * as filterTransforms from '../filters/filterTransforms'; import bitTwiddle from 'bit-twiddle'; @@ -22,8 +23,6 @@ this.filters = []; this.target = null; this.resolution = 1; - - this.firstRun = true; } } @@ -155,7 +154,7 @@ if (filters.length === 1) { - filters[0].apply(this, currentState.renderTarget, lastState.renderTarget, false); + filters[0].apply(this, currentState.renderTarget, lastState.renderTarget, false, currentState); this.freePotRenderTarget(currentState.renderTarget); } else @@ -177,7 +176,7 @@ for (i = 0; i < filters.length - 1; ++i) { - filters[i].apply(this, flip, flop, true); + filters[i].apply(this, flip, flop, true, currentState); const t = flip; @@ -185,7 +184,7 @@ flop = t; } - filters[i].apply(this, flip, lastState.renderTarget, false); + filters[i].apply(this, flip, lastState.renderTarget, false, currentState); this.freePotRenderTarget(flip); this.freePotRenderTarget(flop); @@ -212,16 +211,28 @@ const renderer = this.renderer; const gl = renderer.gl; - renderer.bindShader(filter, true); + let shader = filter.glShaders[renderer.CONTEXT_UID]; - // TODO theres a better way! - // quad can be a mesh and we then should be able to pull this off without accessing the shader directly - const shader = renderer.shaderManager.getGLShader(); - - // TODO change quad to mesh.. - if (!this.firstRun) + // cacheing.. + if (!shader) { - this.firstRun = true; + if (filter.glShaderKey) + { + shader = this.shaderCache[filter.glShaderKey]; + + if (!shader) + { + shader = new Shader(this.gl, filter.vertexSrc, filter.fragmentSrc); + + filter.glShaders[renderer.CONTEXT_UID] = this.shaderCache[filter.glShaderKey] = shader; + } + } + else + { + shader = filter.glShaders[renderer.CONTEXT_UID] = new Shader(this.gl, filter.vertexSrc, filter.fragmentSrc); + } + + // TODO - this only needs to be done once? renderer.bindVao(null); this.quad.initVao(shader); @@ -244,58 +255,146 @@ renderer.maskManager.pushScissorMask(null, renderer.maskManager.scissorData); } - // renderer._bindGLShader(shader); + renderer.bindShader(shader); - let currentState; - const uniforms = filter.uniforms; + // free unit 0 for us, doesn't matter what was there + // don't try to restore it, because syncUniforms can upload it to another slot + // and it'll be a problem + const tex = this.renderer.emptyTextures[0]; - if (uniforms.filterArea) - { - currentState = this.filterData.stack[this.filterData.index]; - - const filterArea = uniforms.filterArea; - - filterArea[0] = currentState.renderTarget.size.width; - filterArea[1] = currentState.renderTarget.size.height; - filterArea[2] = currentState.sourceFrame.x; - filterArea[3] = currentState.sourceFrame.y; - - filter.uniforms.filterArea = filterArea; - } - - // use this to clamp displaced texture coords so they belong to filterArea - // see displacementFilter fragment shader for an example - if (uniforms.filterClamp) - { - currentState = this.filterData.stack[this.filterData.index]; - - const filterClamp = uniforms.filterClamp; - - filterClamp[0] = 0; - filterClamp[1] = 0; - filterClamp[2] = (currentState.sourceFrame.width - 1) / currentState.renderTarget.size.width; - filterClamp[3] = (currentState.sourceFrame.height - 1) / currentState.renderTarget.size.height; - - filter.uniforms.filterClamp = filterClamp; - } - - renderer.shaderManager.setUniforms(filter.uniforms); + this.renderer.boundTextures[0] = tex; + // this syncs the pixi filters uniforms with glsl uniforms + this.syncUniforms(shader, filter); renderer.state.setBlendMode(filter.blendMode); - // temporary bypass cache.. - const tex = this.renderer.boundTextures[0]; - gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, input.texture.texture); this.quad.vao.draw(this.renderer.gl.TRIANGLES, 6, 0); - // restore cache. gl.bindTexture(gl.TEXTURE_2D, tex._glTextures[this.renderer.CONTEXT_UID].texture); } /** + * Uploads the uniforms of the filter. + * + * @param {GLShader} shader - The underlying gl shader. + * @param {PIXI.Filter} filter - The filter we are synchronizing. + */ + syncUniforms(shader, filter) + { + const uniformData = filter.uniformData; + const uniforms = filter.uniforms; + + // 0 is reserved for the pixi texture so we start at 1! + let textureCount = 1; + let currentState; + + // filterArea and filterClamp that are handled by FilterManager directly + // they must not appear in uniformData + + if (shader.uniforms.filterArea) + { + currentState = this.filterData.stack[this.filterData.index]; + + const filterArea = shader.uniforms.filterArea; + + filterArea[0] = currentState.renderTarget.size.width; + filterArea[1] = currentState.renderTarget.size.height; + filterArea[2] = currentState.sourceFrame.x; + filterArea[3] = currentState.sourceFrame.y; + + shader.uniforms.filterArea = filterArea; + } + + // use this to clamp displaced texture coords so they belong to filterArea + // see displacementFilter fragment shader for an example + if (shader.uniforms.filterClamp) + { + currentState = currentState || this.filterData.stack[this.filterData.index]; + + const filterClamp = shader.uniforms.filterClamp; + + filterClamp[0] = 0; + filterClamp[1] = 0; + filterClamp[2] = (currentState.sourceFrame.width - 1) / currentState.renderTarget.size.width; + filterClamp[3] = (currentState.sourceFrame.height - 1) / currentState.renderTarget.size.height; + + shader.uniforms.filterClamp = filterClamp; + } + + // TODO Cacheing layer.. + for (const i in uniformData) + { + if (uniformData[i].type === 'sampler2D' && uniforms[i] !== 0) + { + if (uniforms[i].baseTexture) + { + shader.uniforms[i] = this.renderer.bindTexture(uniforms[i].baseTexture, textureCount); + } + else + { + shader.uniforms[i] = textureCount; + + // TODO + // this is helpful as renderTargets can also be set. + // Although thinking about it, we could probably + // make the filter texture cache return a RenderTexture + // rather than a renderTarget + const gl = this.renderer.gl; + + this.renderer.boundTextures[textureCount] = this.renderer.emptyTextures[textureCount]; + gl.activeTexture(gl.TEXTURE0 + textureCount); + + uniforms[i].texture.bind(); + } + + textureCount++; + } + else if (uniformData[i].type === 'mat3') + { + // check if its pixi matrix.. + if (uniforms[i].a !== undefined) + { + shader.uniforms[i] = uniforms[i].toArray(true); + } + else + { + shader.uniforms[i] = uniforms[i]; + } + } + else if (uniformData[i].type === 'vec2') + { + // check if its a point.. + if (uniforms[i].x !== undefined) + { + const val = shader.uniforms[i] || new Float32Array(2); + + val[0] = uniforms[i].x; + val[1] = uniforms[i].y; + shader.uniforms[i] = val; + } + else + { + shader.uniforms[i] = uniforms[i]; + } + } + else if (uniformData[i].type === 'float') + { + if (shader.uniforms.data[i].value !== uniformData[i]) + { + shader.uniforms[i] = uniforms[i]; + } + } + else + { + shader.uniforms[i] = uniforms[i]; + } + } + } + + /** * Gets a render target from the pool, or creates a new one. * * @param {boolean} clear - Should we clear the render texture when we get it? @@ -442,7 +541,6 @@ renderTarget.resolution = resolution; renderTarget.defaultFrame.width = renderTarget.size.width = minWidth / resolution; renderTarget.defaultFrame.height = renderTarget.size.height = minHeight / resolution; - renderTarget.attachStencilBuffer(); return renderTarget; } diff --git a/src/core/settings.js b/src/core/settings.js index bb08cb0..f4dd087 100644 --- a/src/core/settings.js +++ b/src/core/settings.js @@ -77,11 +77,11 @@ * * @static * @memberof PIXI.settings - * @type {RegExp|string} + * @type {RegExp} * @example `@2x` - * @default /@(.+)x/ + * @default /@([0-9\.]+)x/ */ - RETINA_PREFIX: /@(.+)x/, + RETINA_PREFIX: /@([0-9\.]+)x/, /** * The default render options if none are supplied to {@link PIXI.WebGLRenderer} @@ -175,14 +175,24 @@ SCALE_MODE: 0, /** - * Default specify float precision in shaders. + * Default specify float precision in vertex shader. + * + * @static + * @memberof PIXI.settings + * @type {PIXI.PRECISION} + * @default PIXI.PRECISION.HIGH + */ + PRECISION_VERTEX: 'highp', + + /** + * Default specify float precision in fragment shader. * * @static * @memberof PIXI.settings * @type {PIXI.PRECISION} * @default PIXI.PRECISION.MEDIUM */ - PRECISION: 'mediump', + PRECISION_FRAGMENT: 'mediump', /** * Can we upload the same buffer in a single frame? diff --git a/src/core/sprites/Sprite.js b/src/core/sprites/Sprite.js index 28b0b18..121bf58 100644 --- a/src/core/sprites/Sprite.js +++ b/src/core/sprites/Sprite.js @@ -212,11 +212,11 @@ } else { - w0 = orig.width * (1 - anchor._x); - w1 = orig.width * -anchor._x; + w1 = -anchor._x * orig.width; + w0 = w1 + orig.width; - h0 = orig.height * (1 - anchor._y); - h1 = orig.height * -anchor._y; + h1 = -anchor._y * orig.height; + h0 = h1 + orig.height; } // xy @@ -269,11 +269,11 @@ const tx = wt.tx; const ty = wt.ty; - const w0 = (orig.width) * (1 - anchor._x); - const w1 = (orig.width) * -anchor._x; + const w1 = -anchor._x * orig.width; + const w0 = w1 + orig.width; - const h0 = orig.height * (1 - anchor._y); - const h1 = orig.height * -anchor._y; + const h1 = -anchor._y * orig.height; + const h0 = h1 + orig.height; // xy vertexData[0] = (a * w1) + (c * h1) + tx; @@ -440,7 +440,7 @@ * * @static * @param {number|string|PIXI.BaseTexture|HTMLCanvasElement|HTMLVideoElement} source Source to create texture from - * @return {PIXI.Sprite} The newly created texture + * @return {PIXI.Sprite} The newly created sprite */ static from(source) { @@ -538,8 +538,8 @@ } /** - * The tint applied to the sprite. This is a hex value. A value of - * 0xFFFFFF will remove any tint effect. + * The tint applied to the sprite. This is a hex value. + * A value of 0xFFFFFF will remove any tint effect. * * @member {number} * @default 0xFFFFFF diff --git a/src/core/sprites/canvas/CanvasTinter.js b/src/core/sprites/canvas/CanvasTinter.js index 1cf316b..27e5e0c 100644 --- a/src/core/sprites/canvas/CanvasTinter.js +++ b/src/core/sprites/canvas/CanvasTinter.js @@ -75,8 +75,8 @@ crop.width *= resolution; crop.height *= resolution; - canvas.width = crop.width; - canvas.height = crop.height; + canvas.width = Math.ceil(crop.width); + canvas.height = Math.ceil(crop.height); context.fillStyle = `#${(`00000${(color | 0).toString(16)}`).substr(-6)}`; @@ -130,8 +130,8 @@ crop.width *= resolution; crop.height *= resolution; - canvas.width = crop.width; - canvas.height = crop.height; + canvas.width = Math.ceil(crop.width); + canvas.height = Math.ceil(crop.height); context.globalCompositeOperation = 'copy'; context.fillStyle = `#${(`00000${(color | 0).toString(16)}`).substr(-6)}`; @@ -172,8 +172,8 @@ crop.width *= resolution; crop.height *= resolution; - canvas.width = crop.width; - canvas.height = crop.height; + canvas.width = Math.ceil(crop.width); + canvas.height = Math.ceil(crop.height); context.globalCompositeOperation = 'copy'; context.drawImage( diff --git a/src/core/sprites/webgl/SpriteRenderer.js b/src/core/sprites/webgl/SpriteRenderer.js index 2affcd5..7c352e2 100644 --- a/src/core/sprites/webgl/SpriteRenderer.js +++ b/src/core/sprites/webgl/SpriteRenderer.js @@ -30,7 +30,7 @@ /** * Number of values sent in the vertex buffer. - * positionX, positionY, colorR, colorG, colorB = 5 + * aVertexPosition(2), aTextureCoord(1), aColor(1), aTextureId(1) = 5 * * @member {number} */ @@ -75,7 +75,6 @@ this.shader = null; this.currentIndex = 0; - TICK = 0; this.groups = []; for (let k = 0; k < this.size; k++) diff --git a/src/core/textures/BaseTexture.js b/src/core/textures/BaseTexture.js index 853de7e..aeccf39 100644 --- a/src/core/textures/BaseTexture.js +++ b/src/core/textures/BaseTexture.js @@ -232,16 +232,24 @@ this.realWidth = this.source.naturalWidth || this.source.videoWidth || this.source.width; this.realHeight = this.source.naturalHeight || this.source.videoHeight || this.source.height; - this.width = this.realWidth / this.resolution; - this.height = this.realHeight / this.resolution; - - this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); + this._updateDimensions(); } this.emit('update', this); } /** + * Update dimensions from real values + */ + _updateDimensions() + { + this.width = this.realWidth / this.resolution; + this.height = this.realHeight / this.resolution; + + this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); + } + + /** * Load a source. * * If the source is not-immediately-available, such as an image that needs to be @@ -525,11 +533,7 @@ this.realWidth = Math.round(svgWidth * this.sourceScale); this.realHeight = Math.round(svgHeight * this.sourceScale); - this.width = this.realWidth / this.resolution; - this.height = this.realHeight / this.resolution; - - // Check pow2 after scale - this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); + this._updateDimensions(); // Create a canvas element const canvas = document.createElement('canvas'); @@ -690,4 +694,52 @@ return baseTexture; } + + /** + * Helper function that creates a base texture based on the source you provide. + * The source can be - image url, image element, canvas element. + * + * @static + * @param {string|HTMLImageElement|HTMLCanvasElement} source - The source to create base texture from. + * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values + * @param {number} [sourceScale=(auto)] - Scale for the original image, used with Svg images. + * @return {PIXI.BaseTexture} The new base texture. + */ + static from(source, scaleMode, sourceScale) + { + if (typeof source === 'string') + { + return BaseTexture.fromImage(source, undefined, scaleMode, sourceScale); + } + else if (source instanceof HTMLImageElement) + { + const imageUrl = source.src; + let baseTexture = BaseTextureCache[imageUrl]; + + if (!baseTexture) + { + baseTexture = new BaseTexture(source, scaleMode); + baseTexture.imageUrl = imageUrl; + + if (sourceScale) + { + baseTexture.sourceScale = sourceScale; + } + + // if there is an @2x at the end of the url we are going to assume its a highres image + baseTexture.resolution = getResolutionOfUrl(imageUrl); + + BaseTextureCache[imageUrl] = baseTexture; + } + + return baseTexture; + } + else if (source instanceof HTMLCanvasElement) + { + return BaseTexture.fromCanvas(source, scaleMode); + } + + // lets assume its a base texture! + return source; + } } diff --git a/src/core/textures/Spritesheet.js b/src/core/textures/Spritesheet.js new file mode 100644 index 0000000..d469c62 --- /dev/null +++ b/src/core/textures/Spritesheet.js @@ -0,0 +1,274 @@ +import { Rectangle, Texture } from '../'; +import { getResolutionOfUrl, TextureCache } from '../utils'; + +/** + * Utility class for maintaining reference to a collection + * of Textures on a single Spritesheet. + * + * @class + * @memberof PIXI + */ +export default class Spritesheet +{ + /** + * The maximum number of Textures to build per process. + * + * @type {number} + * @default 1000 + */ + static get BATCH_SIZE() + { + return 1000; + } + + /** + * @param {PIXI.BaseTexture} baseTexture Reference to the source BaseTexture object. + * @param {Object} data - Spritesheet image data. + * @param {string} [resolutionFilename] - The filename to consider when determining + * the resolution of the spritesheet. If not provided, the imageUrl will + * be used on the BaseTexture. + */ + constructor(baseTexture, data, resolutionFilename = null) + { + /** + * Reference to ths source texture + * @type {PIXI.BaseTexture} + */ + this.baseTexture = baseTexture; + + /** + * Map of spritesheet textures. + * @type {Object} + */ + this.textures = {}; + + /** + * Reference to the original JSON data. + * @type {Object} + */ + this.data = data; + + /** + * The resolution of the spritesheet. + * @type {number} + */ + this.resolution = this._updateResolution( + resolutionFilename || this.baseTexture.imageUrl + ); + + /** + * Map of spritesheet frames. + * @type {Object} + * @private + */ + this._frames = this.data.frames; + + /** + * Collection of frame names. + * @type {string[]} + * @private + */ + this._frameKeys = Object.keys(this._frames); + + /** + * Current batch index being processed. + * @type {number} + * @private + */ + this._batchIndex = 0; + + /** + * Callback when parse is completed. + * @type {Function} + * @private + */ + this._callback = null; + } + + /** + * Generate the resolution from the filename or fallback + * to the meta.scale field of the JSON data. + * + * @private + * @param {string} resolutionFilename - The filename to use for resolving + * the default resolution. + * @return {number} Resolution to use for spritesheet. + */ + _updateResolution(resolutionFilename) + { + const scale = this.data.meta.scale; + + // Use a defaultValue of `null` to check if a url-based resolution is set + let resolution = getResolutionOfUrl(resolutionFilename, null); + + // No resolution found via URL + if (resolution === null) + { + // Use the scale value or default to 1 + resolution = scale !== undefined ? parseFloat(scale) : 1; + } + + // For non-1 resolutions, update baseTexture + if (resolution !== 1) + { + this.baseTexture.resolution = resolution; + this.baseTexture.update(); + } + + return resolution; + } + + /** + * Parser spritesheet from loaded data. This is done asynchronously + * to prevent creating too many Texture within a single process. + * + * @param {Function} callback - Callback when complete returns + * a map of the Textures for this spritesheet. + */ + parse(callback) + { + this._batchIndex = 0; + this._callback = callback; + + if (this._frameKeys.length <= Spritesheet.BATCH_SIZE) + { + this._processFrames(0); + this._parseComplete(); + } + else + { + this._nextBatch(); + } + } + + /** + * Process a batch of frames + * + * @private + * @param {number} initialFrameIndex - The index of frame to start. + */ + _processFrames(initialFrameIndex) + { + let frameIndex = initialFrameIndex; + const maxFrames = Spritesheet.BATCH_SIZE; + + while (frameIndex - initialFrameIndex < maxFrames && frameIndex < this._frameKeys.length) + { + const i = this._frameKeys[frameIndex]; + const rect = this._frames[i].frame; + + if (rect) + { + let frame = null; + let trim = null; + const orig = new Rectangle( + 0, + 0, + this._frames[i].sourceSize.w / this.resolution, + this._frames[i].sourceSize.h / this.resolution + ); + + if (this._frames[i].rotated) + { + frame = new Rectangle( + rect.x / this.resolution, + rect.y / this.resolution, + rect.h / this.resolution, + rect.w / this.resolution + ); + } + else + { + frame = new Rectangle( + rect.x / this.resolution, + rect.y / this.resolution, + rect.w / this.resolution, + rect.h / this.resolution + ); + } + + // Check to see if the sprite is trimmed + if (this._frames[i].trimmed) + { + trim = new Rectangle( + this._frames[i].spriteSourceSize.x / this.resolution, + this._frames[i].spriteSourceSize.y / this.resolution, + rect.w / this.resolution, + rect.h / this.resolution + ); + } + + this.textures[i] = new Texture( + this.baseTexture, + frame, + orig, + trim, + this._frames[i].rotated ? 2 : 0 + ); + + // lets also add the frame to pixi's global cache for fromFrame and fromImage functions + TextureCache[i] = this.textures[i]; + } + + frameIndex++; + } + } + + /** + * The parse has completed. + * + * @private + */ + _parseComplete() + { + const callback = this._callback; + + this._callback = null; + this._batchIndex = 0; + callback.call(this, this.textures); + } + + /** + * Begin the next batch of textures. + * + * @private + */ + _nextBatch() + { + this._processFrames(this._batchIndex * Spritesheet.BATCH_SIZE); + this._batchIndex++; + setTimeout(() => + { + if (this._batchIndex * Spritesheet.BATCH_SIZE < this._frameKeys.length) + { + this._nextBatch(); + } + else + { + this._parseComplete(); + } + }, 0); + } + + /** + * Destroy Spritesheet and don't use after this. + * + * @param {boolean} [destroyBase=false] Whether to destroy the base texture as well + */ + destroy(destroyBase = false) + { + for (const i in this.textures) + { + this.textures[i].destroy(); + } + this._frames = null; + this._frameKeys = null; + this.data = null; + this.textures = null; + if (destroyBase) + { + this.baseTexture.destroy(); + } + this.baseTexture = null; + } +} diff --git a/src/core/textures/Texture.js b/src/core/textures/Texture.js index 6403d4f..afe4254 100644 --- a/src/core/textures/Texture.js +++ b/src/core/textures/Texture.js @@ -3,7 +3,7 @@ import TextureUvs from './TextureUvs'; import EventEmitter from 'eventemitter3'; import { Rectangle } from '../math'; -import { TextureCache, BaseTextureCache } from '../utils'; +import { TextureCache, BaseTextureCache, getResolutionOfUrl } from '../utils'; /** * A texture stores the information that represents an image or part of an image. It cannot be added @@ -365,7 +365,8 @@ * The source can be - frame id, image url, video url, canvas element, video element, base texture * * @static - * @param {number|string|PIXI.BaseTexture|HTMLCanvasElement|HTMLVideoElement} source - Source to create texture from + * @param {number|string|HTMLImageElement|HTMLCanvasElement|HTMLVideoElement|PIXI.BaseTexture} + * source - Source to create texture from * @return {PIXI.Texture} The newly created texture */ static from(source) @@ -393,7 +394,7 @@ } else if (source instanceof HTMLImageElement) { - return new Texture(new BaseTexture(source)); + return new Texture(BaseTexture.from(source)); } else if (source instanceof HTMLCanvasElement) { @@ -413,6 +414,43 @@ } /** + * Create a texture from a source and add to the cache. + * + * @static + * @param {HTMLImageElement|HTMLCanvasElement} source - The input source. + * @param {String} imageUrl - File name of texture, for cache and resolving resolution. + * @param {String} [name] - Human readible name for the texture cache. If no name is + * specified, only `imageUrl` will be used as the cache ID. + * @return {PIXI.Texture} Output texture + */ + static fromLoader(source, imageUrl, name) + { + const baseTexture = new BaseTexture(source, null, getResolutionOfUrl(imageUrl)); + const texture = new Texture(baseTexture); + + baseTexture.imageUrl = imageUrl; + + // No name, use imageUrl instead + if (!name) + { + name = imageUrl; + } + + // lets also add the frame to pixi's global cache for fromFrame and fromImage fucntions + BaseTextureCache[name] = baseTexture; + TextureCache[name] = texture; + + // also add references by url if they are different. + if (name !== imageUrl) + { + BaseTextureCache[imageUrl] = baseTexture; + TextureCache[imageUrl] = texture; + } + + return texture; + } + + /** * Adds a texture to the global TextureCache. This cache is shared across the whole PIXI object. * * @static @@ -522,6 +560,29 @@ } } +function createWhiteTexture() +{ + const canvas = document.createElement('canvas'); + + canvas.width = 10; + canvas.height = 10; + + const context = canvas.getContext('2d'); + + context.fillStyle = 'white'; + context.fillRect(0, 0, 10, 10); + + return new Texture(new BaseTexture(canvas)); +} + +function removeAllHandlers(tex) +{ + tex.destroy = function _emptyDestroy() { /* empty */ }; + tex.on = function _emptyOn() { /* empty */ }; + tex.once = function _emptyOnce() { /* empty */ }; + tex.emit = function _emptyEmit() { /* empty */ }; +} + /** * An empty texture, used often to not have to create multiple empty textures. * Can not be destroyed. @@ -530,7 +591,14 @@ * @constant */ Texture.EMPTY = new Texture(new BaseTexture()); -Texture.EMPTY.destroy = function _emptyDestroy() { /* empty */ }; -Texture.EMPTY.on = function _emptyOn() { /* empty */ }; -Texture.EMPTY.once = function _emptyOnce() { /* empty */ }; -Texture.EMPTY.emit = function _emptyEmit() { /* empty */ }; +removeAllHandlers(Texture.EMPTY); + +/** + * A white texture of 10x10 size, used for graphics and other things + * Can not be destroyed. + * + * @static + * @constant + */ +Texture.WHITE = createWhiteTexture(); +removeAllHandlers(Texture.WHITE); diff --git a/src/core/ticker/Ticker.js b/src/core/ticker/Ticker.js index d98a1f0..17a1517 100644 --- a/src/core/ticker/Ticker.js +++ b/src/core/ticker/Ticker.js @@ -67,11 +67,12 @@ * is based, this value is neither capped nor scaled. * If the platform supports DOMHighResTimeStamp, * this value will have a precision of 1 µs. + * Defaults to target frame time * * @member {number} - * @default 1 / TARGET_FPMS + * @default 16.66 */ - this.elapsedMS = 1 / settings.TARGET_FPMS; // default to target frame time + this.elapsedMS = 1 / settings.TARGET_FPMS; /** * The last time {@link PIXI.ticker.Ticker#update} was invoked. diff --git a/src/core/utils/index.js b/src/core/utils/index.js index 6ff609f..be44750 100644 --- a/src/core/utils/index.js +++ b/src/core/utils/index.js @@ -2,6 +2,7 @@ import settings from '../settings'; import EventEmitter from 'eventemitter3'; import pluginTarget from './pluginTarget'; +import * as mixins from './mixin'; import * as isMobile from 'ismobilejs'; let nextUid = 0; @@ -33,6 +34,7 @@ * @type {mixin} */ pluginTarget, + mixins, }; /** @@ -93,7 +95,7 @@ */ export function rgb2hex(rgb) { - return (((rgb[0] * 255) << 16) + ((rgb[1] * 255) << 8) + (rgb[2] * 255)); + return (((rgb[0] * 255) << 16) + ((rgb[1] * 255) << 8) + (rgb[2] * 255 | 0)); } /** @@ -368,3 +370,43 @@ * @private */ export const ProgramCache = {}; + +/** + * Destroys all texture in the cache + * + * @memberof PIXI.utils + * @function destroyTextureCache + */ +export function destroyTextureCache() +{ + let key; + + for (key in TextureCache) + { + TextureCache[key].destroy(); + } + for (key in BaseTextureCache) + { + BaseTextureCache[key].destroy(); + } +} + +/** + * Removes all textures from cache, but does not destroy them + * + * @memberof PIXI.utils + * @function clearTextureCache + */ +export function clearTextureCache() +{ + let key; + + for (key in TextureCache) + { + delete TextureCache[key]; + } + for (key in BaseTextureCache) + { + delete BaseTextureCache[key]; + } +} diff --git a/package.json b/package.json index 6a366ba..71ade15 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pixi.js", - "version": "4.3.4", + "version": "4.3.5", "description": "Pixi.js is a fast lightweight 2D library that works across all devices.", "author": "Mat Groves", "contributors": [ @@ -61,7 +61,7 @@ "ismobilejs": "^0.4.0", "object-assign": "^4.0.1", "pixi-gl-core": "^1.0.3", - "resource-loader": "^2.0.4" + "resource-loader": "^2.0.6" }, "devDependencies": { "babel-cli": "^6.18.0", @@ -70,9 +70,9 @@ "babel-preset-es2015": "^6.14.0", "babelify": "^7.3.0", "del": "^2.2.0", - "electron-prebuilt": "^1.3.2", + "electron": "^1.4.15", "eslint": "^3.5.0", - "floss": "^1.2.0", + "floss": "^2.0.1", "gh-pages": "^0.11.0", "jaguarjs-jsdoc": "^1.0.1", "js-md5": "^0.4.1", diff --git a/src/accessibility/AccessibilityManager.js b/src/accessibility/AccessibilityManager.js index df2716a..f16bcb1 100644 --- a/src/accessibility/AccessibilityManager.js +++ b/src/accessibility/AccessibilityManager.js @@ -3,7 +3,7 @@ import accessibleTarget from './accessibleTarget'; // add some extra variables to the container.. -Object.assign( +core.utils.mixins.delayMixin( core.DisplayObject.prototype, accessibleTarget ); diff --git a/src/core/Application.js b/src/core/Application.js index ea0f058..5340578 100644 --- a/src/core/Application.js +++ b/src/core/Application.js @@ -1,6 +1,6 @@ import { autoDetectRenderer } from './autoDetectRenderer'; import Container from './display/Container'; -import Ticker from './ticker/Ticker'; +import { shared, Ticker } from './ticker'; /** * Convenience class to create a new PIXI application. @@ -35,9 +35,9 @@ * @param {boolean} [noWebGL=false] - prevents selection of WebGL renderer, even if such is present * @param {boolean} [options.legacy=false] - If true Pixi will aim to ensure compatibility * with older / less advanced devices. If you experience unexplained flickering try setting this to true. - + * @param {boolean} [useSharedTicker=false] - `true` to use PIXI.ticker.shared, `false` to create new ticker. */ - constructor(width, height, options, noWebGL) + constructor(width, height, options, noWebGL, useSharedTicker = false) { /** * WebGL renderer if available, otherwise CanvasRenderer @@ -46,23 +46,46 @@ this.renderer = autoDetectRenderer(width, height, options, noWebGL); /** - * The root display container that's renderered. + * The root display container that's rendered. * @member {PIXI.Container} */ this.stage = new Container(); /** + * Internal reference to the ticker + * @member {PIXI.ticker.Ticker} + * @private + */ + this._ticker = null; + + /** * Ticker for doing render updates. * @member {PIXI.ticker.Ticker} + * @default PIXI.ticker.shared */ - this.ticker = new Ticker(); - - this.ticker.add(this.render, this); + this.ticker = useSharedTicker ? shared : new Ticker(); // Start the rendering this.start(); } + set ticker(ticker) // eslint-disable-line require-jsdoc + { + if (this._ticker) + { + this._ticker.remove(this.render, this); + } + this._ticker = ticker; + if (ticker) + { + ticker.add(this.render, this); + } + } + get ticker() // eslint-disable-line require-jsdoc + { + return this._ticker; + } + /** * Render the current stage. */ @@ -76,7 +99,7 @@ */ stop() { - this.ticker.stop(); + this._ticker.stop(); } /** @@ -84,7 +107,7 @@ */ start() { - this.ticker.start(); + this._ticker.start(); } /** @@ -98,13 +121,22 @@ } /** + * Reference to the renderer's screen rectangle. Its safe to use as filterArea or hitArea for whole screen + * @member {PIXI.Rectangle} + * @readonly + */ + get screen() + { + return this.renderer.screen; + } + + /** * Destroy and don't use after this. * @param {Boolean} [removeView=false] Automatically remove canvas from DOM. */ destroy(removeView) { this.stop(); - this.ticker.remove(this.render, this); this.ticker = null; this.stage.destroy(); diff --git a/src/core/Shader.js b/src/core/Shader.js new file mode 100644 index 0000000..78b17e6 --- /dev/null +++ b/src/core/Shader.js @@ -0,0 +1,46 @@ +import { GLShader } from 'pixi-gl-core'; +import settings from './settings'; + +function checkPrecision(src, def) +{ + if (src instanceof Array) + { + if (src[0].substring(0, 9) !== 'precision') + { + const copy = src.slice(0); + + copy.unshift(`precision ${def} float;`); + + return copy; + } + } + else if (src.substring(0, 9) !== 'precision') + { + return `precision ${def} float;\n${src}`; + } + + return src; +} + +/** + * Wrapper class, webGL Shader for Pixi. + * Adds precision string if vertexSrc or fragmentSrc have no mention of it. + * + * @class + * @extends GLShader + * @memberof PIXI + */ +export default class Shader extends GLShader +{ + /** + * + * @param {WebGLRenderingContext} gl - The current WebGL rendering context + * @param {string|string[]} vertexSrc - The vertex shader source as an array of strings. + * @param {string|string[]} fragmentSrc - The fragment shader source as an array of strings. + */ + constructor(gl, vertexSrc, fragmentSrc) + { + super(gl, checkPrecision(vertexSrc, settings.PRECISION_VERTEX), + checkPrecision(fragmentSrc, settings.PRECISION_FRAGMENT)); + } +} diff --git a/src/core/const.js b/src/core/const.js index ef6f662..3de0efe 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -188,7 +188,7 @@ * The gc modes that are supported by pixi. * * The {@link PIXI.settings.GC_MODE} Garbage Collection mode for pixi textures is AUTO - * If set to GC_MODE, the renderer will occasianally check textures usage. If they are not + * If set to GC_MODE, the renderer will occasionally check textures usage. If they are not * used for a specified period of time they will be removed from the GPU. They will of course * be uploaded again when they are required. This is a silent behind the scenes process that * should ensure that the GPU does not get filled up. diff --git a/src/core/display/Container.js b/src/core/display/Container.js index ed8cd5f..17fde8d 100644 --- a/src/core/display/Container.js +++ b/src/core/display/Container.js @@ -73,11 +73,12 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.push(child); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -108,11 +109,12 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.splice(index, 0, child); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -225,10 +227,11 @@ if (index === -1) return null; child.parent = null; + // ensure child transform will be recalculated + child.transform._parentID = -1; removeItems(this.children, index, 1); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -249,11 +252,12 @@ { const child = this.getChildAt(index); + // ensure child transform will be recalculated.. child.parent = null; + child.transform._parentID = -1; removeItems(this.children, index, 1); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -284,13 +288,12 @@ for (let i = 0; i < removed.length; ++i) { removed[i].parent = null; + if (removed[i].transform) + { + removed[i].transform._parentID = -1; + } } - // ensure a transform will be recalculated.. - if (this.transform) - { - this.transform._parentID = -1; - } this._boundsID++; this.onChildrenChange(beginIndex); diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index f37f2f5..be4c4fb 100644 --- a/src/core/graphics/Graphics.js +++ b/src/core/graphics/Graphics.js @@ -28,8 +28,9 @@ { /** * + * @param {boolean} [nativeLines=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP */ - constructor() + constructor(nativeLines = false) { super(); @@ -50,6 +51,13 @@ this.lineWidth = 0; /** + * If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * + * @member {boolean} + */ + this.nativeLines = nativeLines; + + /** * The color of any lines drawn. * * @member {string} @@ -757,28 +765,14 @@ if (!this._spriteRect) { - if (!Graphics._SPRITE_TEXTURE) - { - Graphics._SPRITE_TEXTURE = RenderTexture.create(10, 10); - - const canvas = document.createElement('canvas'); - - canvas.width = 10; - canvas.height = 10; - - const context = canvas.getContext('2d'); - - context.fillStyle = 'white'; - context.fillRect(0, 0, 10, 10); - - Graphics._SPRITE_TEXTURE = Texture.fromCanvas(canvas); - } - - this._spriteRect = new Sprite(Graphics._SPRITE_TEXTURE); + this._spriteRect = new Sprite(new Texture(Texture.WHITE)); } + + const sprite = this._spriteRect; + if (this.tint === 0xffffff) { - this._spriteRect.tint = this.graphicsData[0].fillColor; + sprite.tint = this.graphicsData[0].fillColor; } else { @@ -792,20 +786,21 @@ t1[1] *= t2[1]; t1[2] *= t2[2]; - this._spriteRect.tint = rgb2hex(t1); + sprite.tint = rgb2hex(t1); } - this._spriteRect.alpha = this.graphicsData[0].fillAlpha; - this._spriteRect.worldAlpha = this.worldAlpha * this._spriteRect.alpha; + sprite.alpha = this.graphicsData[0].fillAlpha; + sprite.worldAlpha = this.worldAlpha * sprite.alpha; + sprite.blendMode = this.blendMode; - Graphics._SPRITE_TEXTURE._frame.width = rect.width; - Graphics._SPRITE_TEXTURE._frame.height = rect.height; + sprite.texture._frame.width = rect.width; + sprite.texture._frame.height = rect.height; - this._spriteRect.transform.worldTransform = this.transform.worldTransform; + sprite.transform.worldTransform = this.transform.worldTransform; - this._spriteRect.anchor.set(-rect.x / rect.width, -rect.y / rect.height); - this._spriteRect._onAnchorUpdate(); + sprite.anchor.set(-rect.x / rect.width, -rect.y / rect.height); + sprite._onAnchorUpdate(); - this._spriteRect._renderWebGL(renderer); + sprite._renderWebGL(renderer); } /** @@ -1031,6 +1026,7 @@ this.fillColor, this.fillAlpha, this.filling, + this.nativeLines, shape ); diff --git a/src/core/graphics/GraphicsData.js b/src/core/graphics/GraphicsData.js index f5a87ae..b6f03c0 100644 --- a/src/core/graphics/GraphicsData.js +++ b/src/core/graphics/GraphicsData.js @@ -14,14 +14,19 @@ * @param {number} fillColor - the color of the fill * @param {number} fillAlpha - the alpha of the fill * @param {boolean} fill - whether or not the shape is filled with a colour + * @param {boolean} nativeLines - the method for drawing lines * @param {PIXI.Circle|PIXI.Rectangle|PIXI.Ellipse|PIXI.Polygon} shape - The shape object to draw. */ - constructor(lineWidth, lineColor, lineAlpha, fillColor, fillAlpha, fill, shape) + constructor(lineWidth, lineColor, lineAlpha, fillColor, fillAlpha, fill, nativeLines, shape) { /** * @member {number} the width of the line to draw */ this.lineWidth = lineWidth; + /** + * @member {boolean} if true the liens will be draw using LINES instead of TRIANGLE_STRIP + */ + this.nativeLines = nativeLines; /** * @member {number} the color of the line to draw @@ -85,6 +90,7 @@ this.fillColor, this.fillAlpha, this.fill, + this.nativeLines, this.shape ); } diff --git a/src/core/graphics/webgl/GraphicsRenderer.js b/src/core/graphics/webgl/GraphicsRenderer.js index 14083fe..67f5e92 100644 --- a/src/core/graphics/webgl/GraphicsRenderer.js +++ b/src/core/graphics/webgl/GraphicsRenderer.js @@ -102,7 +102,15 @@ shaderTemp.uniforms.alpha = graphics.worldAlpha; renderer.bindVao(webGLData.vao); - webGLData.vao.draw(gl.TRIANGLE_STRIP, webGLData.indices.length); + + if (graphics.nativeLines) + { + gl.drawArrays(gl.LINES, 0, webGLData.points.length / 6); + } + else + { + webGLData.vao.draw(gl.TRIANGLE_STRIP, webGLData.indices.length); + } } } diff --git a/src/core/graphics/webgl/utils/buildLine.js b/src/core/graphics/webgl/utils/buildLine.js index ecf43f7..4d6862e 100644 --- a/src/core/graphics/webgl/utils/buildLine.js +++ b/src/core/graphics/webgl/utils/buildLine.js @@ -11,7 +11,29 @@ * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties * @param {object} webGLData - an object containing all the webGL-specific information to create this shape */ -export default function buildLine(graphicsData, webGLData) +export default function (graphicsData, webGLData) +{ + if (graphicsData.nativeLines) + { + buildNativeLine(graphicsData, webGLData); + } + else + { + buildLine(graphicsData, webGLData); + } +} + +/** + * Builds a line to draw using the poligon method. + * + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @private + * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties + * @param {object} webGLData - an object containing all the webGL-specific information to create this shape + */ +function buildLine(graphicsData, webGLData) { // TODO OPTIMISE! let points = graphicsData.points; @@ -224,3 +246,46 @@ indices.push(indexStart - 1); } + +/** + * Builds a line to draw using the gl.drawArrays(gl.LINES) method + * + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @private + * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties + * @param {object} webGLData - an object containing all the webGL-specific information to create this shape + */ +function buildNativeLine(graphicsData, webGLData) +{ + let i = 0; + const points = graphicsData.points; + + if (points.length === 0) return; + + const verts = webGLData.points; + const length = points.length / 2; + + // sort color + const color = hex2rgb(graphicsData.lineColor); + const alpha = graphicsData.lineAlpha; + const r = color[0] * alpha; + const g = color[1] * alpha; + const b = color[2] * alpha; + + for (i = 1; i < length; i++) + { + const p1x = points[(i - 1) * 2]; + const p1y = points[((i - 1) * 2) + 1]; + + const p2x = points[i * 2]; + const p2y = points[(i * 2) + 1]; + + verts.push(p1x, p1y); + verts.push(r, g, b, alpha); + + verts.push(p2x, p2y); + verts.push(r, g, b, alpha); + } +} diff --git a/src/core/graphics/webgl/utils/buildRoundedRectangle.js b/src/core/graphics/webgl/utils/buildRoundedRectangle.js index 9abc807..a6c8461 100644 --- a/src/core/graphics/webgl/utils/buildRoundedRectangle.js +++ b/src/core/graphics/webgl/utils/buildRoundedRectangle.js @@ -77,6 +77,26 @@ } /** + * Calculate a single point for a quadratic bezier curve. + * Utility function used by quadraticBezierCurve. + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @private + * @param {number} n1 - first number + * @param {number} n2 - second number + * @param {number} perc - percentage + * @return {number} the result + * + */ +function getPt(n1, n2, perc) +{ + const diff = n2 - n1; + + return n1 + (diff * perc); +} + +/** * Calculate the points for a quadratic bezier curve. (helper function..) * Based on: https://stackoverflow.com/questions/785097/how-do-i-implement-a-bezier-curve-in-c * @@ -105,13 +125,6 @@ let x = 0; let y = 0; - function getPt(n1, n2, perc) - { - const diff = n2 - n1; - - return n1 + (diff * perc); - } - for (let i = 0, j = 0; i <= n; ++i) { j = i / n; diff --git a/src/core/index.js b/src/core/index.js index f7a817c..4f414e8 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -30,6 +30,7 @@ export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; export { default as CanvasGraphicsRenderer } from './graphics/canvas/CanvasGraphicsRenderer'; +export { default as Spritesheet } from './textures/Spritesheet'; export { default as Texture } from './textures/Texture'; export { default as BaseTexture } from './textures/BaseTexture'; export { default as RenderTexture } from './textures/RenderTexture'; diff --git a/src/core/math/Matrix.js b/src/core/math/Matrix.js index 0b77477..3679860 100644 --- a/src/core/math/Matrix.js +++ b/src/core/math/Matrix.js @@ -13,45 +13,50 @@ export default class Matrix { /** - * + * @param {number} [a=1] - x scale + * @param {number} [b=0] - y skew + * @param {number} [c=0] - x skew + * @param {number} [d=1] - y scale + * @param {number} [tx=0] - x translation + * @param {number} [ty=0] - y translation */ - constructor() + constructor(a = 1, b = 0, c = 0, d = 1, tx = 0, ty = 0) { /** * @member {number} * @default 1 */ - this.a = 1; + this.a = a; /** * @member {number} * @default 0 */ - this.b = 0; + this.b = b; /** * @member {number} * @default 0 */ - this.c = 0; + this.c = c; /** * @member {number} * @default 1 */ - this.d = 1; + this.d = d; /** * @member {number} * @default 0 */ - this.tx = 0; + this.tx = tx; /** * @member {number} * @default 0 */ - this.ty = 0; + this.ty = ty; this.array = null; } diff --git a/src/core/renderers/SystemRenderer.js b/src/core/renderers/SystemRenderer.js index fa116a6..a24c775 100644 --- a/src/core/renderers/SystemRenderer.js +++ b/src/core/renderers/SystemRenderer.js @@ -1,5 +1,5 @@ import { sayHello, hex2string, hex2rgb } from '../utils'; -import { Matrix } from '../math'; +import { Matrix, Rectangle } from '../math'; import { RENDERER_TYPE } from '../const'; import settings from '../settings'; import Container from '../display/Container'; @@ -21,8 +21,8 @@ { /** * @param {string} system - The name of the system this renderer is for. - * @param {number} [width=800] - the width of the canvas view - * @param {number} [height=600] - the height of the canvas view + * @param {number} [screenWidth=800] - the width of the screen + * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false @@ -37,7 +37,7 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(system, width, height, options) + constructor(system, screenWidth, screenHeight, options) { super(); @@ -69,20 +69,13 @@ this.type = RENDERER_TYPE.UNKNOWN; /** - * The width of the canvas view + * Measurements of the screen. (0, 0, screenWidth, screenHeight) * - * @member {number} - * @default 800 - */ - this.width = width || 800; - - /** - * The height of the canvas view + * Its safe to use as filterArea or hitArea for whole stage * - * @member {number} - * @default 600 + * @member {PIXI.Rectangle} */ - this.height = height || 600; + this.screen = new Rectangle(0, 0, screenWidth || 800, screenHeight || 600); /** * The canvas element that everything is drawn to @@ -107,7 +100,7 @@ this.transparent = options.transparent; /** - * Whether the render view should be resized automatically + * Whether css dimensions of canvas view should be resized to screen dimensions automatically * * @member {boolean} */ @@ -192,23 +185,48 @@ } /** - * Resizes the canvas view to the specified width and height + * Same as view.width, actual number of pixels in the canvas by horizontal * - * @param {number} width - the new width of the canvas view - * @param {number} height - the new height of the canvas view + * @member {number} + * @readonly + * @default 800 */ - resize(width, height) + get width() { - this.width = width * this.resolution; - this.height = height * this.resolution; + return this.view.width; + } - this.view.width = this.width; - this.view.height = this.height; + /** + * Same as view.height, actual number of pixels in the canvas by vertical + * + * @member {number} + * @readonly + * @default 600 + */ + get height() + { + return this.view.height; + } + + /** + * Resizes the screen and canvas to the specified width and height + * Canvas dimensions are multiplied by resolution + * + * @param {number} screenWidth - the new width of the screen + * @param {number} screenHeight - the new height of the screen + */ + resize(screenWidth, screenHeight) + { + this.screen.width = screenWidth; + this.screen.height = screenHeight; + + this.view.width = screenWidth * this.resolution; + this.view.height = screenHeight * this.resolution; if (this.autoResize) { - this.view.style.width = `${this.width / this.resolution}px`; - this.view.style.height = `${this.height / this.resolution}px`; + this.view.style.width = `${screenWidth}px`; + this.view.style.height = `${screenHeight}px`; } } @@ -249,11 +267,10 @@ this.type = RENDERER_TYPE.UNKNOWN; - this.width = 0; - this.height = 0; - this.view = null; + this.screen = null; + this.resolution = 0; this.transparent = false; diff --git a/src/core/renderers/canvas/CanvasRenderer.js b/src/core/renderers/canvas/CanvasRenderer.js index 1bd2d5e..e84e49c 100644 --- a/src/core/renderers/canvas/CanvasRenderer.js +++ b/src/core/renderers/canvas/CanvasRenderer.js @@ -18,8 +18,8 @@ export default class CanvasRenderer extends SystemRenderer { /** - * @param {number} [width=800] - the width of the canvas view - * @param {number} [height=600] - the height of the canvas view + * @param {number} [screenWidth=800] - the width of the screen + * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false @@ -34,9 +34,9 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(width, height, options = {}) + constructor(screenWidth, screenHeight, options = {}) { - super('Canvas', width, height, options); + super('Canvas', screenWidth, screenHeight, options); this.type = RENDERER_TYPE.CANVAS; @@ -96,7 +96,7 @@ this.context = null; this.renderingToScreen = false; - this.resize(width, height); + this.resize(screenWidth, screenHeight); } /** @@ -283,12 +283,12 @@ * * @extends PIXI.SystemRenderer#resize * - * @param {number} width - The new width of the canvas view - * @param {number} height - The new height of the canvas view + * @param {number} screenWidth - the new width of the screen + * @param {number} screenHeight - the new height of the screen */ - resize(width, height) + resize(screenWidth, screenHeight) { - super.resize(width, height); + super.resize(screenWidth, screenHeight); // reset the scale mode.. oddly this seems to be reset when the canvas is resized. // surely a browser bug?? Let pixi fix that for you.. diff --git a/src/core/renderers/webgl/TextureManager.js b/src/core/renderers/webgl/TextureManager.js index b685c1a..8227289 100644 --- a/src/core/renderers/webgl/TextureManager.js +++ b/src/core/renderers/webgl/TextureManager.js @@ -61,7 +61,7 @@ * Updates and/or Creates a WebGL texture for the renderer's context. * * @param {PIXI.BaseTexture|PIXI.Texture} texture - the texture to update - * @param {Number} location - the location the texture will be bound to. + * @param {number} location - the location the texture will be bound to. * @return {GLTexture} The gl texture. */ updateTexture(texture, location) diff --git a/src/core/renderers/webgl/WebGLRenderer.js b/src/core/renderers/webgl/WebGLRenderer.js index e4cd8a0..830b382 100644 --- a/src/core/renderers/webgl/WebGLRenderer.js +++ b/src/core/renderers/webgl/WebGLRenderer.js @@ -31,8 +31,8 @@ { /** * - * @param {number} [width=0] - the width of the canvas view - * @param {number} [height=0] - the height of the canvas view + * @param {number} [screenWidth=800] - the width of the screen + * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false @@ -53,9 +53,9 @@ * @param {boolean} [options.legacy=false] - If true Pixi will aim to ensure compatibility * with older / less advanced devices. If you experiance unexplained flickering try setting this to true. */ - constructor(width, height, options = {}) + constructor(screenWidth, screenHeight, options = {}) { - super('WebGL', width, height, options); + super('WebGL', screenWidth, screenHeight, options); this.legacy = !!options.legacy; @@ -245,7 +245,7 @@ glCore._testingContext = gl; // setup the width/height properties and gl viewport - this.resize(this.width, this.height); + this.resize(this.screen.width, this.screen.height); } /** @@ -343,16 +343,16 @@ /** * Resizes the webGL view to the specified width and height. * - * @param {number} width - the new width of the webGL view - * @param {number} height - the new height of the webGL view + * @param {number} screenWidth - the new width of the screen + * @param {number} screenHeight - the new height of the screen */ - resize(width, height) + resize(screenWidth, screenHeight) { // if(width * this.resolution === this.width && height * this.resolution === this.height)return; - SystemRenderer.prototype.resize.call(this, width, height); + SystemRenderer.prototype.resize.call(this, screenWidth, screenHeight); - this.rootRenderTarget.resize(width, height); + this.rootRenderTarget.resize(screenWidth, screenHeight); if (this._activeRenderTarget === this.rootRenderTarget) { @@ -396,6 +396,26 @@ } /** + * Erases the render texture and fills the drawing area with a colour + * + * @param {PIXI.RenderTexture} renderTexture - The render texture to clear + * @param {number} [clearColor] - The colour + * @return {PIXI.WebGLRenderer} Returns itself. + */ + clearRenderTexture(renderTexture, clearColor) + { + const baseTexture = renderTexture.baseTexture; + const renderTarget = baseTexture._glRenderTargets[this.CONTEXT_UID]; + + if (renderTarget) + { + renderTarget.clear(clearColor); + } + + return this; + } + + /** * Binds a render texture for rendering * * @param {PIXI.RenderTexture} renderTexture - The render texture to render @@ -474,9 +494,10 @@ * Changes the current GLShader to the one given in parameter * * @param {PIXI.glCore.Shader} shader - the new glShader + * @param {boolean} [autoProject=true] - Whether automatically set the projection matrix * @return {PIXI.WebGLRenderer} Returns itself. */ - _bindGLShader(shader) + _bindGLShader(shader, autoProject) { // TODO cache if (this._activeShader !== shader) @@ -484,8 +505,14 @@ this._activeShader = shader; shader.bind(); - // automatically set the projection matrix - shader.uniforms.projectionMatrix = this._activeRenderTarget.projectionMatrix.toArray(true); + // `autoProject` normally would be a default parameter set to true + // but because of how Babel transpiles default parameters + // it hinders the performance of this method. + if (autoProject !== false) + { + // automatically set the projection matrix + shader.uniforms.projectionMatrix = this._activeRenderTarget.projectionMatrix.toArray(true); + } } return this; diff --git a/src/core/renderers/webgl/WebGLState.js b/src/core/renderers/webgl/WebGLState.js index ba6ecc5..6c846d5 100755 --- a/src/core/renderers/webgl/WebGLState.js +++ b/src/core/renderers/webgl/WebGLState.js @@ -82,18 +82,20 @@ push() { // next state.. - let state = this.stack[++this.stackIndex]; + let state = this.stack[this.stackIndex]; if (!state) { state = this.stack[this.stackIndex] = new Uint8Array(16); } + ++this.stackIndex; + // copy state.. // set active state so we can force overrides of gl state for (let i = 0; i < this.activeState.length; i++) { - this.activeState[i] = state[i]; + state[i] = this.activeState[i]; } } diff --git a/src/core/renderers/webgl/filters/Filter.js b/src/core/renderers/webgl/filters/Filter.js index 2c3eaad..4eb6128 100644 --- a/src/core/renderers/webgl/filters/Filter.js +++ b/src/core/renderers/webgl/filters/Filter.js @@ -9,7 +9,7 @@ * @memberof PIXI * @extends PIXI.Shader */ -class Filter extends Shader +export default class Filter extends Shader { /** * @param {string} [vertexSrc] - The source of the vertex shader. @@ -56,15 +56,16 @@ * @param {PIXI.RenderTarget} input - The input render target. * @param {PIXI.RenderTarget} output - The target to output to. * @param {boolean} clear - Should the output be cleared before rendering to it + * @param {object} [currentState] - It's current state of filter. + * There are some useful properties in the currentState : + * target, filters, sourceFrame, destinationFrame, renderTarget, resolution */ - apply(filterManager, input, output, clear) + apply(filterManager, input, output, clear, currentState) // eslint-disable-line no-unused-vars { // do as you please! - filterManager.applyFilter(this, input, output, clear); + filterManager.applyFilter(this, input, output, clear, currentState); // or just do a regular render.. } } - -export default Filter; diff --git a/src/core/renderers/webgl/filters/extractUniformsFromSrc.js b/src/core/renderers/webgl/filters/extractUniformsFromSrc.js index d78a0b1..4893214 100644 --- a/src/core/renderers/webgl/filters/extractUniformsFromSrc.js +++ b/src/core/renderers/webgl/filters/extractUniformsFromSrc.js @@ -12,7 +12,7 @@ function extractUniformsFromString(string) { - const maskRegex = new RegExp('^(projectionMatrix|uSampler|filterArea)$'); + const maskRegex = new RegExp('^(projectionMatrix|uSampler|filterArea|filterClamp)$'); const uniforms = {}; let nameSplit; diff --git a/src/core/renderers/webgl/managers/FilterManager.js b/src/core/renderers/webgl/managers/FilterManager.js index 43fc869..6c7422d 100644 --- a/src/core/renderers/webgl/managers/FilterManager.js +++ b/src/core/renderers/webgl/managers/FilterManager.js @@ -2,6 +2,7 @@ import RenderTarget from '../utils/RenderTarget'; import Quad from '../utils/Quad'; import { Rectangle } from '../../../math'; +import Shader from '../../../Shader'; import * as filterTransforms from '../filters/filterTransforms'; import bitTwiddle from 'bit-twiddle'; @@ -22,8 +23,6 @@ this.filters = []; this.target = null; this.resolution = 1; - - this.firstRun = true; } } @@ -155,7 +154,7 @@ if (filters.length === 1) { - filters[0].apply(this, currentState.renderTarget, lastState.renderTarget, false); + filters[0].apply(this, currentState.renderTarget, lastState.renderTarget, false, currentState); this.freePotRenderTarget(currentState.renderTarget); } else @@ -177,7 +176,7 @@ for (i = 0; i < filters.length - 1; ++i) { - filters[i].apply(this, flip, flop, true); + filters[i].apply(this, flip, flop, true, currentState); const t = flip; @@ -185,7 +184,7 @@ flop = t; } - filters[i].apply(this, flip, lastState.renderTarget, false); + filters[i].apply(this, flip, lastState.renderTarget, false, currentState); this.freePotRenderTarget(flip); this.freePotRenderTarget(flop); @@ -212,16 +211,28 @@ const renderer = this.renderer; const gl = renderer.gl; - renderer.bindShader(filter, true); + let shader = filter.glShaders[renderer.CONTEXT_UID]; - // TODO theres a better way! - // quad can be a mesh and we then should be able to pull this off without accessing the shader directly - const shader = renderer.shaderManager.getGLShader(); - - // TODO change quad to mesh.. - if (!this.firstRun) + // cacheing.. + if (!shader) { - this.firstRun = true; + if (filter.glShaderKey) + { + shader = this.shaderCache[filter.glShaderKey]; + + if (!shader) + { + shader = new Shader(this.gl, filter.vertexSrc, filter.fragmentSrc); + + filter.glShaders[renderer.CONTEXT_UID] = this.shaderCache[filter.glShaderKey] = shader; + } + } + else + { + shader = filter.glShaders[renderer.CONTEXT_UID] = new Shader(this.gl, filter.vertexSrc, filter.fragmentSrc); + } + + // TODO - this only needs to be done once? renderer.bindVao(null); this.quad.initVao(shader); @@ -244,58 +255,146 @@ renderer.maskManager.pushScissorMask(null, renderer.maskManager.scissorData); } - // renderer._bindGLShader(shader); + renderer.bindShader(shader); - let currentState; - const uniforms = filter.uniforms; + // free unit 0 for us, doesn't matter what was there + // don't try to restore it, because syncUniforms can upload it to another slot + // and it'll be a problem + const tex = this.renderer.emptyTextures[0]; - if (uniforms.filterArea) - { - currentState = this.filterData.stack[this.filterData.index]; - - const filterArea = uniforms.filterArea; - - filterArea[0] = currentState.renderTarget.size.width; - filterArea[1] = currentState.renderTarget.size.height; - filterArea[2] = currentState.sourceFrame.x; - filterArea[3] = currentState.sourceFrame.y; - - filter.uniforms.filterArea = filterArea; - } - - // use this to clamp displaced texture coords so they belong to filterArea - // see displacementFilter fragment shader for an example - if (uniforms.filterClamp) - { - currentState = this.filterData.stack[this.filterData.index]; - - const filterClamp = uniforms.filterClamp; - - filterClamp[0] = 0; - filterClamp[1] = 0; - filterClamp[2] = (currentState.sourceFrame.width - 1) / currentState.renderTarget.size.width; - filterClamp[3] = (currentState.sourceFrame.height - 1) / currentState.renderTarget.size.height; - - filter.uniforms.filterClamp = filterClamp; - } - - renderer.shaderManager.setUniforms(filter.uniforms); + this.renderer.boundTextures[0] = tex; + // this syncs the pixi filters uniforms with glsl uniforms + this.syncUniforms(shader, filter); renderer.state.setBlendMode(filter.blendMode); - // temporary bypass cache.. - const tex = this.renderer.boundTextures[0]; - gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, input.texture.texture); this.quad.vao.draw(this.renderer.gl.TRIANGLES, 6, 0); - // restore cache. gl.bindTexture(gl.TEXTURE_2D, tex._glTextures[this.renderer.CONTEXT_UID].texture); } /** + * Uploads the uniforms of the filter. + * + * @param {GLShader} shader - The underlying gl shader. + * @param {PIXI.Filter} filter - The filter we are synchronizing. + */ + syncUniforms(shader, filter) + { + const uniformData = filter.uniformData; + const uniforms = filter.uniforms; + + // 0 is reserved for the pixi texture so we start at 1! + let textureCount = 1; + let currentState; + + // filterArea and filterClamp that are handled by FilterManager directly + // they must not appear in uniformData + + if (shader.uniforms.filterArea) + { + currentState = this.filterData.stack[this.filterData.index]; + + const filterArea = shader.uniforms.filterArea; + + filterArea[0] = currentState.renderTarget.size.width; + filterArea[1] = currentState.renderTarget.size.height; + filterArea[2] = currentState.sourceFrame.x; + filterArea[3] = currentState.sourceFrame.y; + + shader.uniforms.filterArea = filterArea; + } + + // use this to clamp displaced texture coords so they belong to filterArea + // see displacementFilter fragment shader for an example + if (shader.uniforms.filterClamp) + { + currentState = currentState || this.filterData.stack[this.filterData.index]; + + const filterClamp = shader.uniforms.filterClamp; + + filterClamp[0] = 0; + filterClamp[1] = 0; + filterClamp[2] = (currentState.sourceFrame.width - 1) / currentState.renderTarget.size.width; + filterClamp[3] = (currentState.sourceFrame.height - 1) / currentState.renderTarget.size.height; + + shader.uniforms.filterClamp = filterClamp; + } + + // TODO Cacheing layer.. + for (const i in uniformData) + { + if (uniformData[i].type === 'sampler2D' && uniforms[i] !== 0) + { + if (uniforms[i].baseTexture) + { + shader.uniforms[i] = this.renderer.bindTexture(uniforms[i].baseTexture, textureCount); + } + else + { + shader.uniforms[i] = textureCount; + + // TODO + // this is helpful as renderTargets can also be set. + // Although thinking about it, we could probably + // make the filter texture cache return a RenderTexture + // rather than a renderTarget + const gl = this.renderer.gl; + + this.renderer.boundTextures[textureCount] = this.renderer.emptyTextures[textureCount]; + gl.activeTexture(gl.TEXTURE0 + textureCount); + + uniforms[i].texture.bind(); + } + + textureCount++; + } + else if (uniformData[i].type === 'mat3') + { + // check if its pixi matrix.. + if (uniforms[i].a !== undefined) + { + shader.uniforms[i] = uniforms[i].toArray(true); + } + else + { + shader.uniforms[i] = uniforms[i]; + } + } + else if (uniformData[i].type === 'vec2') + { + // check if its a point.. + if (uniforms[i].x !== undefined) + { + const val = shader.uniforms[i] || new Float32Array(2); + + val[0] = uniforms[i].x; + val[1] = uniforms[i].y; + shader.uniforms[i] = val; + } + else + { + shader.uniforms[i] = uniforms[i]; + } + } + else if (uniformData[i].type === 'float') + { + if (shader.uniforms.data[i].value !== uniformData[i]) + { + shader.uniforms[i] = uniforms[i]; + } + } + else + { + shader.uniforms[i] = uniforms[i]; + } + } + } + + /** * Gets a render target from the pool, or creates a new one. * * @param {boolean} clear - Should we clear the render texture when we get it? @@ -442,7 +541,6 @@ renderTarget.resolution = resolution; renderTarget.defaultFrame.width = renderTarget.size.width = minWidth / resolution; renderTarget.defaultFrame.height = renderTarget.size.height = minHeight / resolution; - renderTarget.attachStencilBuffer(); return renderTarget; } diff --git a/src/core/settings.js b/src/core/settings.js index bb08cb0..f4dd087 100644 --- a/src/core/settings.js +++ b/src/core/settings.js @@ -77,11 +77,11 @@ * * @static * @memberof PIXI.settings - * @type {RegExp|string} + * @type {RegExp} * @example `@2x` - * @default /@(.+)x/ + * @default /@([0-9\.]+)x/ */ - RETINA_PREFIX: /@(.+)x/, + RETINA_PREFIX: /@([0-9\.]+)x/, /** * The default render options if none are supplied to {@link PIXI.WebGLRenderer} @@ -175,14 +175,24 @@ SCALE_MODE: 0, /** - * Default specify float precision in shaders. + * Default specify float precision in vertex shader. + * + * @static + * @memberof PIXI.settings + * @type {PIXI.PRECISION} + * @default PIXI.PRECISION.HIGH + */ + PRECISION_VERTEX: 'highp', + + /** + * Default specify float precision in fragment shader. * * @static * @memberof PIXI.settings * @type {PIXI.PRECISION} * @default PIXI.PRECISION.MEDIUM */ - PRECISION: 'mediump', + PRECISION_FRAGMENT: 'mediump', /** * Can we upload the same buffer in a single frame? diff --git a/src/core/sprites/Sprite.js b/src/core/sprites/Sprite.js index 28b0b18..121bf58 100644 --- a/src/core/sprites/Sprite.js +++ b/src/core/sprites/Sprite.js @@ -212,11 +212,11 @@ } else { - w0 = orig.width * (1 - anchor._x); - w1 = orig.width * -anchor._x; + w1 = -anchor._x * orig.width; + w0 = w1 + orig.width; - h0 = orig.height * (1 - anchor._y); - h1 = orig.height * -anchor._y; + h1 = -anchor._y * orig.height; + h0 = h1 + orig.height; } // xy @@ -269,11 +269,11 @@ const tx = wt.tx; const ty = wt.ty; - const w0 = (orig.width) * (1 - anchor._x); - const w1 = (orig.width) * -anchor._x; + const w1 = -anchor._x * orig.width; + const w0 = w1 + orig.width; - const h0 = orig.height * (1 - anchor._y); - const h1 = orig.height * -anchor._y; + const h1 = -anchor._y * orig.height; + const h0 = h1 + orig.height; // xy vertexData[0] = (a * w1) + (c * h1) + tx; @@ -440,7 +440,7 @@ * * @static * @param {number|string|PIXI.BaseTexture|HTMLCanvasElement|HTMLVideoElement} source Source to create texture from - * @return {PIXI.Sprite} The newly created texture + * @return {PIXI.Sprite} The newly created sprite */ static from(source) { @@ -538,8 +538,8 @@ } /** - * The tint applied to the sprite. This is a hex value. A value of - * 0xFFFFFF will remove any tint effect. + * The tint applied to the sprite. This is a hex value. + * A value of 0xFFFFFF will remove any tint effect. * * @member {number} * @default 0xFFFFFF diff --git a/src/core/sprites/canvas/CanvasTinter.js b/src/core/sprites/canvas/CanvasTinter.js index 1cf316b..27e5e0c 100644 --- a/src/core/sprites/canvas/CanvasTinter.js +++ b/src/core/sprites/canvas/CanvasTinter.js @@ -75,8 +75,8 @@ crop.width *= resolution; crop.height *= resolution; - canvas.width = crop.width; - canvas.height = crop.height; + canvas.width = Math.ceil(crop.width); + canvas.height = Math.ceil(crop.height); context.fillStyle = `#${(`00000${(color | 0).toString(16)}`).substr(-6)}`; @@ -130,8 +130,8 @@ crop.width *= resolution; crop.height *= resolution; - canvas.width = crop.width; - canvas.height = crop.height; + canvas.width = Math.ceil(crop.width); + canvas.height = Math.ceil(crop.height); context.globalCompositeOperation = 'copy'; context.fillStyle = `#${(`00000${(color | 0).toString(16)}`).substr(-6)}`; @@ -172,8 +172,8 @@ crop.width *= resolution; crop.height *= resolution; - canvas.width = crop.width; - canvas.height = crop.height; + canvas.width = Math.ceil(crop.width); + canvas.height = Math.ceil(crop.height); context.globalCompositeOperation = 'copy'; context.drawImage( diff --git a/src/core/sprites/webgl/SpriteRenderer.js b/src/core/sprites/webgl/SpriteRenderer.js index 2affcd5..7c352e2 100644 --- a/src/core/sprites/webgl/SpriteRenderer.js +++ b/src/core/sprites/webgl/SpriteRenderer.js @@ -30,7 +30,7 @@ /** * Number of values sent in the vertex buffer. - * positionX, positionY, colorR, colorG, colorB = 5 + * aVertexPosition(2), aTextureCoord(1), aColor(1), aTextureId(1) = 5 * * @member {number} */ @@ -75,7 +75,6 @@ this.shader = null; this.currentIndex = 0; - TICK = 0; this.groups = []; for (let k = 0; k < this.size; k++) diff --git a/src/core/textures/BaseTexture.js b/src/core/textures/BaseTexture.js index 853de7e..aeccf39 100644 --- a/src/core/textures/BaseTexture.js +++ b/src/core/textures/BaseTexture.js @@ -232,16 +232,24 @@ this.realWidth = this.source.naturalWidth || this.source.videoWidth || this.source.width; this.realHeight = this.source.naturalHeight || this.source.videoHeight || this.source.height; - this.width = this.realWidth / this.resolution; - this.height = this.realHeight / this.resolution; - - this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); + this._updateDimensions(); } this.emit('update', this); } /** + * Update dimensions from real values + */ + _updateDimensions() + { + this.width = this.realWidth / this.resolution; + this.height = this.realHeight / this.resolution; + + this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); + } + + /** * Load a source. * * If the source is not-immediately-available, such as an image that needs to be @@ -525,11 +533,7 @@ this.realWidth = Math.round(svgWidth * this.sourceScale); this.realHeight = Math.round(svgHeight * this.sourceScale); - this.width = this.realWidth / this.resolution; - this.height = this.realHeight / this.resolution; - - // Check pow2 after scale - this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); + this._updateDimensions(); // Create a canvas element const canvas = document.createElement('canvas'); @@ -690,4 +694,52 @@ return baseTexture; } + + /** + * Helper function that creates a base texture based on the source you provide. + * The source can be - image url, image element, canvas element. + * + * @static + * @param {string|HTMLImageElement|HTMLCanvasElement} source - The source to create base texture from. + * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values + * @param {number} [sourceScale=(auto)] - Scale for the original image, used with Svg images. + * @return {PIXI.BaseTexture} The new base texture. + */ + static from(source, scaleMode, sourceScale) + { + if (typeof source === 'string') + { + return BaseTexture.fromImage(source, undefined, scaleMode, sourceScale); + } + else if (source instanceof HTMLImageElement) + { + const imageUrl = source.src; + let baseTexture = BaseTextureCache[imageUrl]; + + if (!baseTexture) + { + baseTexture = new BaseTexture(source, scaleMode); + baseTexture.imageUrl = imageUrl; + + if (sourceScale) + { + baseTexture.sourceScale = sourceScale; + } + + // if there is an @2x at the end of the url we are going to assume its a highres image + baseTexture.resolution = getResolutionOfUrl(imageUrl); + + BaseTextureCache[imageUrl] = baseTexture; + } + + return baseTexture; + } + else if (source instanceof HTMLCanvasElement) + { + return BaseTexture.fromCanvas(source, scaleMode); + } + + // lets assume its a base texture! + return source; + } } diff --git a/src/core/textures/Spritesheet.js b/src/core/textures/Spritesheet.js new file mode 100644 index 0000000..d469c62 --- /dev/null +++ b/src/core/textures/Spritesheet.js @@ -0,0 +1,274 @@ +import { Rectangle, Texture } from '../'; +import { getResolutionOfUrl, TextureCache } from '../utils'; + +/** + * Utility class for maintaining reference to a collection + * of Textures on a single Spritesheet. + * + * @class + * @memberof PIXI + */ +export default class Spritesheet +{ + /** + * The maximum number of Textures to build per process. + * + * @type {number} + * @default 1000 + */ + static get BATCH_SIZE() + { + return 1000; + } + + /** + * @param {PIXI.BaseTexture} baseTexture Reference to the source BaseTexture object. + * @param {Object} data - Spritesheet image data. + * @param {string} [resolutionFilename] - The filename to consider when determining + * the resolution of the spritesheet. If not provided, the imageUrl will + * be used on the BaseTexture. + */ + constructor(baseTexture, data, resolutionFilename = null) + { + /** + * Reference to ths source texture + * @type {PIXI.BaseTexture} + */ + this.baseTexture = baseTexture; + + /** + * Map of spritesheet textures. + * @type {Object} + */ + this.textures = {}; + + /** + * Reference to the original JSON data. + * @type {Object} + */ + this.data = data; + + /** + * The resolution of the spritesheet. + * @type {number} + */ + this.resolution = this._updateResolution( + resolutionFilename || this.baseTexture.imageUrl + ); + + /** + * Map of spritesheet frames. + * @type {Object} + * @private + */ + this._frames = this.data.frames; + + /** + * Collection of frame names. + * @type {string[]} + * @private + */ + this._frameKeys = Object.keys(this._frames); + + /** + * Current batch index being processed. + * @type {number} + * @private + */ + this._batchIndex = 0; + + /** + * Callback when parse is completed. + * @type {Function} + * @private + */ + this._callback = null; + } + + /** + * Generate the resolution from the filename or fallback + * to the meta.scale field of the JSON data. + * + * @private + * @param {string} resolutionFilename - The filename to use for resolving + * the default resolution. + * @return {number} Resolution to use for spritesheet. + */ + _updateResolution(resolutionFilename) + { + const scale = this.data.meta.scale; + + // Use a defaultValue of `null` to check if a url-based resolution is set + let resolution = getResolutionOfUrl(resolutionFilename, null); + + // No resolution found via URL + if (resolution === null) + { + // Use the scale value or default to 1 + resolution = scale !== undefined ? parseFloat(scale) : 1; + } + + // For non-1 resolutions, update baseTexture + if (resolution !== 1) + { + this.baseTexture.resolution = resolution; + this.baseTexture.update(); + } + + return resolution; + } + + /** + * Parser spritesheet from loaded data. This is done asynchronously + * to prevent creating too many Texture within a single process. + * + * @param {Function} callback - Callback when complete returns + * a map of the Textures for this spritesheet. + */ + parse(callback) + { + this._batchIndex = 0; + this._callback = callback; + + if (this._frameKeys.length <= Spritesheet.BATCH_SIZE) + { + this._processFrames(0); + this._parseComplete(); + } + else + { + this._nextBatch(); + } + } + + /** + * Process a batch of frames + * + * @private + * @param {number} initialFrameIndex - The index of frame to start. + */ + _processFrames(initialFrameIndex) + { + let frameIndex = initialFrameIndex; + const maxFrames = Spritesheet.BATCH_SIZE; + + while (frameIndex - initialFrameIndex < maxFrames && frameIndex < this._frameKeys.length) + { + const i = this._frameKeys[frameIndex]; + const rect = this._frames[i].frame; + + if (rect) + { + let frame = null; + let trim = null; + const orig = new Rectangle( + 0, + 0, + this._frames[i].sourceSize.w / this.resolution, + this._frames[i].sourceSize.h / this.resolution + ); + + if (this._frames[i].rotated) + { + frame = new Rectangle( + rect.x / this.resolution, + rect.y / this.resolution, + rect.h / this.resolution, + rect.w / this.resolution + ); + } + else + { + frame = new Rectangle( + rect.x / this.resolution, + rect.y / this.resolution, + rect.w / this.resolution, + rect.h / this.resolution + ); + } + + // Check to see if the sprite is trimmed + if (this._frames[i].trimmed) + { + trim = new Rectangle( + this._frames[i].spriteSourceSize.x / this.resolution, + this._frames[i].spriteSourceSize.y / this.resolution, + rect.w / this.resolution, + rect.h / this.resolution + ); + } + + this.textures[i] = new Texture( + this.baseTexture, + frame, + orig, + trim, + this._frames[i].rotated ? 2 : 0 + ); + + // lets also add the frame to pixi's global cache for fromFrame and fromImage functions + TextureCache[i] = this.textures[i]; + } + + frameIndex++; + } + } + + /** + * The parse has completed. + * + * @private + */ + _parseComplete() + { + const callback = this._callback; + + this._callback = null; + this._batchIndex = 0; + callback.call(this, this.textures); + } + + /** + * Begin the next batch of textures. + * + * @private + */ + _nextBatch() + { + this._processFrames(this._batchIndex * Spritesheet.BATCH_SIZE); + this._batchIndex++; + setTimeout(() => + { + if (this._batchIndex * Spritesheet.BATCH_SIZE < this._frameKeys.length) + { + this._nextBatch(); + } + else + { + this._parseComplete(); + } + }, 0); + } + + /** + * Destroy Spritesheet and don't use after this. + * + * @param {boolean} [destroyBase=false] Whether to destroy the base texture as well + */ + destroy(destroyBase = false) + { + for (const i in this.textures) + { + this.textures[i].destroy(); + } + this._frames = null; + this._frameKeys = null; + this.data = null; + this.textures = null; + if (destroyBase) + { + this.baseTexture.destroy(); + } + this.baseTexture = null; + } +} diff --git a/src/core/textures/Texture.js b/src/core/textures/Texture.js index 6403d4f..afe4254 100644 --- a/src/core/textures/Texture.js +++ b/src/core/textures/Texture.js @@ -3,7 +3,7 @@ import TextureUvs from './TextureUvs'; import EventEmitter from 'eventemitter3'; import { Rectangle } from '../math'; -import { TextureCache, BaseTextureCache } from '../utils'; +import { TextureCache, BaseTextureCache, getResolutionOfUrl } from '../utils'; /** * A texture stores the information that represents an image or part of an image. It cannot be added @@ -365,7 +365,8 @@ * The source can be - frame id, image url, video url, canvas element, video element, base texture * * @static - * @param {number|string|PIXI.BaseTexture|HTMLCanvasElement|HTMLVideoElement} source - Source to create texture from + * @param {number|string|HTMLImageElement|HTMLCanvasElement|HTMLVideoElement|PIXI.BaseTexture} + * source - Source to create texture from * @return {PIXI.Texture} The newly created texture */ static from(source) @@ -393,7 +394,7 @@ } else if (source instanceof HTMLImageElement) { - return new Texture(new BaseTexture(source)); + return new Texture(BaseTexture.from(source)); } else if (source instanceof HTMLCanvasElement) { @@ -413,6 +414,43 @@ } /** + * Create a texture from a source and add to the cache. + * + * @static + * @param {HTMLImageElement|HTMLCanvasElement} source - The input source. + * @param {String} imageUrl - File name of texture, for cache and resolving resolution. + * @param {String} [name] - Human readible name for the texture cache. If no name is + * specified, only `imageUrl` will be used as the cache ID. + * @return {PIXI.Texture} Output texture + */ + static fromLoader(source, imageUrl, name) + { + const baseTexture = new BaseTexture(source, null, getResolutionOfUrl(imageUrl)); + const texture = new Texture(baseTexture); + + baseTexture.imageUrl = imageUrl; + + // No name, use imageUrl instead + if (!name) + { + name = imageUrl; + } + + // lets also add the frame to pixi's global cache for fromFrame and fromImage fucntions + BaseTextureCache[name] = baseTexture; + TextureCache[name] = texture; + + // also add references by url if they are different. + if (name !== imageUrl) + { + BaseTextureCache[imageUrl] = baseTexture; + TextureCache[imageUrl] = texture; + } + + return texture; + } + + /** * Adds a texture to the global TextureCache. This cache is shared across the whole PIXI object. * * @static @@ -522,6 +560,29 @@ } } +function createWhiteTexture() +{ + const canvas = document.createElement('canvas'); + + canvas.width = 10; + canvas.height = 10; + + const context = canvas.getContext('2d'); + + context.fillStyle = 'white'; + context.fillRect(0, 0, 10, 10); + + return new Texture(new BaseTexture(canvas)); +} + +function removeAllHandlers(tex) +{ + tex.destroy = function _emptyDestroy() { /* empty */ }; + tex.on = function _emptyOn() { /* empty */ }; + tex.once = function _emptyOnce() { /* empty */ }; + tex.emit = function _emptyEmit() { /* empty */ }; +} + /** * An empty texture, used often to not have to create multiple empty textures. * Can not be destroyed. @@ -530,7 +591,14 @@ * @constant */ Texture.EMPTY = new Texture(new BaseTexture()); -Texture.EMPTY.destroy = function _emptyDestroy() { /* empty */ }; -Texture.EMPTY.on = function _emptyOn() { /* empty */ }; -Texture.EMPTY.once = function _emptyOnce() { /* empty */ }; -Texture.EMPTY.emit = function _emptyEmit() { /* empty */ }; +removeAllHandlers(Texture.EMPTY); + +/** + * A white texture of 10x10 size, used for graphics and other things + * Can not be destroyed. + * + * @static + * @constant + */ +Texture.WHITE = createWhiteTexture(); +removeAllHandlers(Texture.WHITE); diff --git a/src/core/ticker/Ticker.js b/src/core/ticker/Ticker.js index d98a1f0..17a1517 100644 --- a/src/core/ticker/Ticker.js +++ b/src/core/ticker/Ticker.js @@ -67,11 +67,12 @@ * is based, this value is neither capped nor scaled. * If the platform supports DOMHighResTimeStamp, * this value will have a precision of 1 µs. + * Defaults to target frame time * * @member {number} - * @default 1 / TARGET_FPMS + * @default 16.66 */ - this.elapsedMS = 1 / settings.TARGET_FPMS; // default to target frame time + this.elapsedMS = 1 / settings.TARGET_FPMS; /** * The last time {@link PIXI.ticker.Ticker#update} was invoked. diff --git a/src/core/utils/index.js b/src/core/utils/index.js index 6ff609f..be44750 100644 --- a/src/core/utils/index.js +++ b/src/core/utils/index.js @@ -2,6 +2,7 @@ import settings from '../settings'; import EventEmitter from 'eventemitter3'; import pluginTarget from './pluginTarget'; +import * as mixins from './mixin'; import * as isMobile from 'ismobilejs'; let nextUid = 0; @@ -33,6 +34,7 @@ * @type {mixin} */ pluginTarget, + mixins, }; /** @@ -93,7 +95,7 @@ */ export function rgb2hex(rgb) { - return (((rgb[0] * 255) << 16) + ((rgb[1] * 255) << 8) + (rgb[2] * 255)); + return (((rgb[0] * 255) << 16) + ((rgb[1] * 255) << 8) + (rgb[2] * 255 | 0)); } /** @@ -368,3 +370,43 @@ * @private */ export const ProgramCache = {}; + +/** + * Destroys all texture in the cache + * + * @memberof PIXI.utils + * @function destroyTextureCache + */ +export function destroyTextureCache() +{ + let key; + + for (key in TextureCache) + { + TextureCache[key].destroy(); + } + for (key in BaseTextureCache) + { + BaseTextureCache[key].destroy(); + } +} + +/** + * Removes all textures from cache, but does not destroy them + * + * @memberof PIXI.utils + * @function clearTextureCache + */ +export function clearTextureCache() +{ + let key; + + for (key in TextureCache) + { + delete TextureCache[key]; + } + for (key in BaseTextureCache) + { + delete BaseTextureCache[key]; + } +} diff --git a/src/core/utils/mixin.js b/src/core/utils/mixin.js new file mode 100644 index 0000000..114de2a --- /dev/null +++ b/src/core/utils/mixin.js @@ -0,0 +1,59 @@ +/** + * Mixes all enumerable properties and methods from a source object to a target object. + * + * @memberof PIXI.utils.mixins + * @function mixin + * @param {object} target The prototype or instance that properties and methods should be added to. + * @param {object} source The source of properties and methods to mix in. + */ +export function mixin(target, source) +{ + if (!target || !source) return; + // in ES8/ES2017, this would be really easy: + // Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); + + // get all the enumerable property keys + const keys = Object.keys(source); + + // loop through properties + for (let i = 0; i < keys.length; ++i) + { + const propertyName = keys[i]; + + // Set the property using the property descriptor - this works for accessors and normal value properties + Object.defineProperty(target, propertyName, Object.getOwnPropertyDescriptor(source, propertyName)); + } +} + +const mixins = []; + +/** + * Queues a mixin to be handled towards the end of the initialization of PIXI, so that deprecation + * can take effect. + * + * @memberof PIXI.utils.mixins + * @function delayMixin + * @private + * @param {object} target The prototype or instance that properties and methods should be added to. + * @param {object} source The source of properties and methods to mix in. + */ +export function delayMixin(target, source) +{ + mixins.push(target, source); +} + +/** + * Handles all mixins queued via delayMixin(). + * + * @memberof PIXI.utils.mixins + * @function performMixins + * @private + */ +export function performMixins() +{ + for (let i = 0; i < mixins.length; i += 2) + { + mixin(mixins[i], mixins[i + 1]); + } + mixins.length = 0; +} diff --git a/package.json b/package.json index 6a366ba..71ade15 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pixi.js", - "version": "4.3.4", + "version": "4.3.5", "description": "Pixi.js is a fast lightweight 2D library that works across all devices.", "author": "Mat Groves", "contributors": [ @@ -61,7 +61,7 @@ "ismobilejs": "^0.4.0", "object-assign": "^4.0.1", "pixi-gl-core": "^1.0.3", - "resource-loader": "^2.0.4" + "resource-loader": "^2.0.6" }, "devDependencies": { "babel-cli": "^6.18.0", @@ -70,9 +70,9 @@ "babel-preset-es2015": "^6.14.0", "babelify": "^7.3.0", "del": "^2.2.0", - "electron-prebuilt": "^1.3.2", + "electron": "^1.4.15", "eslint": "^3.5.0", - "floss": "^1.2.0", + "floss": "^2.0.1", "gh-pages": "^0.11.0", "jaguarjs-jsdoc": "^1.0.1", "js-md5": "^0.4.1", diff --git a/src/accessibility/AccessibilityManager.js b/src/accessibility/AccessibilityManager.js index df2716a..f16bcb1 100644 --- a/src/accessibility/AccessibilityManager.js +++ b/src/accessibility/AccessibilityManager.js @@ -3,7 +3,7 @@ import accessibleTarget from './accessibleTarget'; // add some extra variables to the container.. -Object.assign( +core.utils.mixins.delayMixin( core.DisplayObject.prototype, accessibleTarget ); diff --git a/src/core/Application.js b/src/core/Application.js index ea0f058..5340578 100644 --- a/src/core/Application.js +++ b/src/core/Application.js @@ -1,6 +1,6 @@ import { autoDetectRenderer } from './autoDetectRenderer'; import Container from './display/Container'; -import Ticker from './ticker/Ticker'; +import { shared, Ticker } from './ticker'; /** * Convenience class to create a new PIXI application. @@ -35,9 +35,9 @@ * @param {boolean} [noWebGL=false] - prevents selection of WebGL renderer, even if such is present * @param {boolean} [options.legacy=false] - If true Pixi will aim to ensure compatibility * with older / less advanced devices. If you experience unexplained flickering try setting this to true. - + * @param {boolean} [useSharedTicker=false] - `true` to use PIXI.ticker.shared, `false` to create new ticker. */ - constructor(width, height, options, noWebGL) + constructor(width, height, options, noWebGL, useSharedTicker = false) { /** * WebGL renderer if available, otherwise CanvasRenderer @@ -46,23 +46,46 @@ this.renderer = autoDetectRenderer(width, height, options, noWebGL); /** - * The root display container that's renderered. + * The root display container that's rendered. * @member {PIXI.Container} */ this.stage = new Container(); /** + * Internal reference to the ticker + * @member {PIXI.ticker.Ticker} + * @private + */ + this._ticker = null; + + /** * Ticker for doing render updates. * @member {PIXI.ticker.Ticker} + * @default PIXI.ticker.shared */ - this.ticker = new Ticker(); - - this.ticker.add(this.render, this); + this.ticker = useSharedTicker ? shared : new Ticker(); // Start the rendering this.start(); } + set ticker(ticker) // eslint-disable-line require-jsdoc + { + if (this._ticker) + { + this._ticker.remove(this.render, this); + } + this._ticker = ticker; + if (ticker) + { + ticker.add(this.render, this); + } + } + get ticker() // eslint-disable-line require-jsdoc + { + return this._ticker; + } + /** * Render the current stage. */ @@ -76,7 +99,7 @@ */ stop() { - this.ticker.stop(); + this._ticker.stop(); } /** @@ -84,7 +107,7 @@ */ start() { - this.ticker.start(); + this._ticker.start(); } /** @@ -98,13 +121,22 @@ } /** + * Reference to the renderer's screen rectangle. Its safe to use as filterArea or hitArea for whole screen + * @member {PIXI.Rectangle} + * @readonly + */ + get screen() + { + return this.renderer.screen; + } + + /** * Destroy and don't use after this. * @param {Boolean} [removeView=false] Automatically remove canvas from DOM. */ destroy(removeView) { this.stop(); - this.ticker.remove(this.render, this); this.ticker = null; this.stage.destroy(); diff --git a/src/core/Shader.js b/src/core/Shader.js new file mode 100644 index 0000000..78b17e6 --- /dev/null +++ b/src/core/Shader.js @@ -0,0 +1,46 @@ +import { GLShader } from 'pixi-gl-core'; +import settings from './settings'; + +function checkPrecision(src, def) +{ + if (src instanceof Array) + { + if (src[0].substring(0, 9) !== 'precision') + { + const copy = src.slice(0); + + copy.unshift(`precision ${def} float;`); + + return copy; + } + } + else if (src.substring(0, 9) !== 'precision') + { + return `precision ${def} float;\n${src}`; + } + + return src; +} + +/** + * Wrapper class, webGL Shader for Pixi. + * Adds precision string if vertexSrc or fragmentSrc have no mention of it. + * + * @class + * @extends GLShader + * @memberof PIXI + */ +export default class Shader extends GLShader +{ + /** + * + * @param {WebGLRenderingContext} gl - The current WebGL rendering context + * @param {string|string[]} vertexSrc - The vertex shader source as an array of strings. + * @param {string|string[]} fragmentSrc - The fragment shader source as an array of strings. + */ + constructor(gl, vertexSrc, fragmentSrc) + { + super(gl, checkPrecision(vertexSrc, settings.PRECISION_VERTEX), + checkPrecision(fragmentSrc, settings.PRECISION_FRAGMENT)); + } +} diff --git a/src/core/const.js b/src/core/const.js index ef6f662..3de0efe 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -188,7 +188,7 @@ * The gc modes that are supported by pixi. * * The {@link PIXI.settings.GC_MODE} Garbage Collection mode for pixi textures is AUTO - * If set to GC_MODE, the renderer will occasianally check textures usage. If they are not + * If set to GC_MODE, the renderer will occasionally check textures usage. If they are not * used for a specified period of time they will be removed from the GPU. They will of course * be uploaded again when they are required. This is a silent behind the scenes process that * should ensure that the GPU does not get filled up. diff --git a/src/core/display/Container.js b/src/core/display/Container.js index ed8cd5f..17fde8d 100644 --- a/src/core/display/Container.js +++ b/src/core/display/Container.js @@ -73,11 +73,12 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.push(child); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -108,11 +109,12 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.splice(index, 0, child); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -225,10 +227,11 @@ if (index === -1) return null; child.parent = null; + // ensure child transform will be recalculated + child.transform._parentID = -1; removeItems(this.children, index, 1); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -249,11 +252,12 @@ { const child = this.getChildAt(index); + // ensure child transform will be recalculated.. child.parent = null; + child.transform._parentID = -1; removeItems(this.children, index, 1); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -284,13 +288,12 @@ for (let i = 0; i < removed.length; ++i) { removed[i].parent = null; + if (removed[i].transform) + { + removed[i].transform._parentID = -1; + } } - // ensure a transform will be recalculated.. - if (this.transform) - { - this.transform._parentID = -1; - } this._boundsID++; this.onChildrenChange(beginIndex); diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index f37f2f5..be4c4fb 100644 --- a/src/core/graphics/Graphics.js +++ b/src/core/graphics/Graphics.js @@ -28,8 +28,9 @@ { /** * + * @param {boolean} [nativeLines=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP */ - constructor() + constructor(nativeLines = false) { super(); @@ -50,6 +51,13 @@ this.lineWidth = 0; /** + * If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * + * @member {boolean} + */ + this.nativeLines = nativeLines; + + /** * The color of any lines drawn. * * @member {string} @@ -757,28 +765,14 @@ if (!this._spriteRect) { - if (!Graphics._SPRITE_TEXTURE) - { - Graphics._SPRITE_TEXTURE = RenderTexture.create(10, 10); - - const canvas = document.createElement('canvas'); - - canvas.width = 10; - canvas.height = 10; - - const context = canvas.getContext('2d'); - - context.fillStyle = 'white'; - context.fillRect(0, 0, 10, 10); - - Graphics._SPRITE_TEXTURE = Texture.fromCanvas(canvas); - } - - this._spriteRect = new Sprite(Graphics._SPRITE_TEXTURE); + this._spriteRect = new Sprite(new Texture(Texture.WHITE)); } + + const sprite = this._spriteRect; + if (this.tint === 0xffffff) { - this._spriteRect.tint = this.graphicsData[0].fillColor; + sprite.tint = this.graphicsData[0].fillColor; } else { @@ -792,20 +786,21 @@ t1[1] *= t2[1]; t1[2] *= t2[2]; - this._spriteRect.tint = rgb2hex(t1); + sprite.tint = rgb2hex(t1); } - this._spriteRect.alpha = this.graphicsData[0].fillAlpha; - this._spriteRect.worldAlpha = this.worldAlpha * this._spriteRect.alpha; + sprite.alpha = this.graphicsData[0].fillAlpha; + sprite.worldAlpha = this.worldAlpha * sprite.alpha; + sprite.blendMode = this.blendMode; - Graphics._SPRITE_TEXTURE._frame.width = rect.width; - Graphics._SPRITE_TEXTURE._frame.height = rect.height; + sprite.texture._frame.width = rect.width; + sprite.texture._frame.height = rect.height; - this._spriteRect.transform.worldTransform = this.transform.worldTransform; + sprite.transform.worldTransform = this.transform.worldTransform; - this._spriteRect.anchor.set(-rect.x / rect.width, -rect.y / rect.height); - this._spriteRect._onAnchorUpdate(); + sprite.anchor.set(-rect.x / rect.width, -rect.y / rect.height); + sprite._onAnchorUpdate(); - this._spriteRect._renderWebGL(renderer); + sprite._renderWebGL(renderer); } /** @@ -1031,6 +1026,7 @@ this.fillColor, this.fillAlpha, this.filling, + this.nativeLines, shape ); diff --git a/src/core/graphics/GraphicsData.js b/src/core/graphics/GraphicsData.js index f5a87ae..b6f03c0 100644 --- a/src/core/graphics/GraphicsData.js +++ b/src/core/graphics/GraphicsData.js @@ -14,14 +14,19 @@ * @param {number} fillColor - the color of the fill * @param {number} fillAlpha - the alpha of the fill * @param {boolean} fill - whether or not the shape is filled with a colour + * @param {boolean} nativeLines - the method for drawing lines * @param {PIXI.Circle|PIXI.Rectangle|PIXI.Ellipse|PIXI.Polygon} shape - The shape object to draw. */ - constructor(lineWidth, lineColor, lineAlpha, fillColor, fillAlpha, fill, shape) + constructor(lineWidth, lineColor, lineAlpha, fillColor, fillAlpha, fill, nativeLines, shape) { /** * @member {number} the width of the line to draw */ this.lineWidth = lineWidth; + /** + * @member {boolean} if true the liens will be draw using LINES instead of TRIANGLE_STRIP + */ + this.nativeLines = nativeLines; /** * @member {number} the color of the line to draw @@ -85,6 +90,7 @@ this.fillColor, this.fillAlpha, this.fill, + this.nativeLines, this.shape ); } diff --git a/src/core/graphics/webgl/GraphicsRenderer.js b/src/core/graphics/webgl/GraphicsRenderer.js index 14083fe..67f5e92 100644 --- a/src/core/graphics/webgl/GraphicsRenderer.js +++ b/src/core/graphics/webgl/GraphicsRenderer.js @@ -102,7 +102,15 @@ shaderTemp.uniforms.alpha = graphics.worldAlpha; renderer.bindVao(webGLData.vao); - webGLData.vao.draw(gl.TRIANGLE_STRIP, webGLData.indices.length); + + if (graphics.nativeLines) + { + gl.drawArrays(gl.LINES, 0, webGLData.points.length / 6); + } + else + { + webGLData.vao.draw(gl.TRIANGLE_STRIP, webGLData.indices.length); + } } } diff --git a/src/core/graphics/webgl/utils/buildLine.js b/src/core/graphics/webgl/utils/buildLine.js index ecf43f7..4d6862e 100644 --- a/src/core/graphics/webgl/utils/buildLine.js +++ b/src/core/graphics/webgl/utils/buildLine.js @@ -11,7 +11,29 @@ * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties * @param {object} webGLData - an object containing all the webGL-specific information to create this shape */ -export default function buildLine(graphicsData, webGLData) +export default function (graphicsData, webGLData) +{ + if (graphicsData.nativeLines) + { + buildNativeLine(graphicsData, webGLData); + } + else + { + buildLine(graphicsData, webGLData); + } +} + +/** + * Builds a line to draw using the poligon method. + * + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @private + * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties + * @param {object} webGLData - an object containing all the webGL-specific information to create this shape + */ +function buildLine(graphicsData, webGLData) { // TODO OPTIMISE! let points = graphicsData.points; @@ -224,3 +246,46 @@ indices.push(indexStart - 1); } + +/** + * Builds a line to draw using the gl.drawArrays(gl.LINES) method + * + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @private + * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties + * @param {object} webGLData - an object containing all the webGL-specific information to create this shape + */ +function buildNativeLine(graphicsData, webGLData) +{ + let i = 0; + const points = graphicsData.points; + + if (points.length === 0) return; + + const verts = webGLData.points; + const length = points.length / 2; + + // sort color + const color = hex2rgb(graphicsData.lineColor); + const alpha = graphicsData.lineAlpha; + const r = color[0] * alpha; + const g = color[1] * alpha; + const b = color[2] * alpha; + + for (i = 1; i < length; i++) + { + const p1x = points[(i - 1) * 2]; + const p1y = points[((i - 1) * 2) + 1]; + + const p2x = points[i * 2]; + const p2y = points[(i * 2) + 1]; + + verts.push(p1x, p1y); + verts.push(r, g, b, alpha); + + verts.push(p2x, p2y); + verts.push(r, g, b, alpha); + } +} diff --git a/src/core/graphics/webgl/utils/buildRoundedRectangle.js b/src/core/graphics/webgl/utils/buildRoundedRectangle.js index 9abc807..a6c8461 100644 --- a/src/core/graphics/webgl/utils/buildRoundedRectangle.js +++ b/src/core/graphics/webgl/utils/buildRoundedRectangle.js @@ -77,6 +77,26 @@ } /** + * Calculate a single point for a quadratic bezier curve. + * Utility function used by quadraticBezierCurve. + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @private + * @param {number} n1 - first number + * @param {number} n2 - second number + * @param {number} perc - percentage + * @return {number} the result + * + */ +function getPt(n1, n2, perc) +{ + const diff = n2 - n1; + + return n1 + (diff * perc); +} + +/** * Calculate the points for a quadratic bezier curve. (helper function..) * Based on: https://stackoverflow.com/questions/785097/how-do-i-implement-a-bezier-curve-in-c * @@ -105,13 +125,6 @@ let x = 0; let y = 0; - function getPt(n1, n2, perc) - { - const diff = n2 - n1; - - return n1 + (diff * perc); - } - for (let i = 0, j = 0; i <= n; ++i) { j = i / n; diff --git a/src/core/index.js b/src/core/index.js index f7a817c..4f414e8 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -30,6 +30,7 @@ export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; export { default as CanvasGraphicsRenderer } from './graphics/canvas/CanvasGraphicsRenderer'; +export { default as Spritesheet } from './textures/Spritesheet'; export { default as Texture } from './textures/Texture'; export { default as BaseTexture } from './textures/BaseTexture'; export { default as RenderTexture } from './textures/RenderTexture'; diff --git a/src/core/math/Matrix.js b/src/core/math/Matrix.js index 0b77477..3679860 100644 --- a/src/core/math/Matrix.js +++ b/src/core/math/Matrix.js @@ -13,45 +13,50 @@ export default class Matrix { /** - * + * @param {number} [a=1] - x scale + * @param {number} [b=0] - y skew + * @param {number} [c=0] - x skew + * @param {number} [d=1] - y scale + * @param {number} [tx=0] - x translation + * @param {number} [ty=0] - y translation */ - constructor() + constructor(a = 1, b = 0, c = 0, d = 1, tx = 0, ty = 0) { /** * @member {number} * @default 1 */ - this.a = 1; + this.a = a; /** * @member {number} * @default 0 */ - this.b = 0; + this.b = b; /** * @member {number} * @default 0 */ - this.c = 0; + this.c = c; /** * @member {number} * @default 1 */ - this.d = 1; + this.d = d; /** * @member {number} * @default 0 */ - this.tx = 0; + this.tx = tx; /** * @member {number} * @default 0 */ - this.ty = 0; + this.ty = ty; this.array = null; } diff --git a/src/core/renderers/SystemRenderer.js b/src/core/renderers/SystemRenderer.js index fa116a6..a24c775 100644 --- a/src/core/renderers/SystemRenderer.js +++ b/src/core/renderers/SystemRenderer.js @@ -1,5 +1,5 @@ import { sayHello, hex2string, hex2rgb } from '../utils'; -import { Matrix } from '../math'; +import { Matrix, Rectangle } from '../math'; import { RENDERER_TYPE } from '../const'; import settings from '../settings'; import Container from '../display/Container'; @@ -21,8 +21,8 @@ { /** * @param {string} system - The name of the system this renderer is for. - * @param {number} [width=800] - the width of the canvas view - * @param {number} [height=600] - the height of the canvas view + * @param {number} [screenWidth=800] - the width of the screen + * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false @@ -37,7 +37,7 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(system, width, height, options) + constructor(system, screenWidth, screenHeight, options) { super(); @@ -69,20 +69,13 @@ this.type = RENDERER_TYPE.UNKNOWN; /** - * The width of the canvas view + * Measurements of the screen. (0, 0, screenWidth, screenHeight) * - * @member {number} - * @default 800 - */ - this.width = width || 800; - - /** - * The height of the canvas view + * Its safe to use as filterArea or hitArea for whole stage * - * @member {number} - * @default 600 + * @member {PIXI.Rectangle} */ - this.height = height || 600; + this.screen = new Rectangle(0, 0, screenWidth || 800, screenHeight || 600); /** * The canvas element that everything is drawn to @@ -107,7 +100,7 @@ this.transparent = options.transparent; /** - * Whether the render view should be resized automatically + * Whether css dimensions of canvas view should be resized to screen dimensions automatically * * @member {boolean} */ @@ -192,23 +185,48 @@ } /** - * Resizes the canvas view to the specified width and height + * Same as view.width, actual number of pixels in the canvas by horizontal * - * @param {number} width - the new width of the canvas view - * @param {number} height - the new height of the canvas view + * @member {number} + * @readonly + * @default 800 */ - resize(width, height) + get width() { - this.width = width * this.resolution; - this.height = height * this.resolution; + return this.view.width; + } - this.view.width = this.width; - this.view.height = this.height; + /** + * Same as view.height, actual number of pixels in the canvas by vertical + * + * @member {number} + * @readonly + * @default 600 + */ + get height() + { + return this.view.height; + } + + /** + * Resizes the screen and canvas to the specified width and height + * Canvas dimensions are multiplied by resolution + * + * @param {number} screenWidth - the new width of the screen + * @param {number} screenHeight - the new height of the screen + */ + resize(screenWidth, screenHeight) + { + this.screen.width = screenWidth; + this.screen.height = screenHeight; + + this.view.width = screenWidth * this.resolution; + this.view.height = screenHeight * this.resolution; if (this.autoResize) { - this.view.style.width = `${this.width / this.resolution}px`; - this.view.style.height = `${this.height / this.resolution}px`; + this.view.style.width = `${screenWidth}px`; + this.view.style.height = `${screenHeight}px`; } } @@ -249,11 +267,10 @@ this.type = RENDERER_TYPE.UNKNOWN; - this.width = 0; - this.height = 0; - this.view = null; + this.screen = null; + this.resolution = 0; this.transparent = false; diff --git a/src/core/renderers/canvas/CanvasRenderer.js b/src/core/renderers/canvas/CanvasRenderer.js index 1bd2d5e..e84e49c 100644 --- a/src/core/renderers/canvas/CanvasRenderer.js +++ b/src/core/renderers/canvas/CanvasRenderer.js @@ -18,8 +18,8 @@ export default class CanvasRenderer extends SystemRenderer { /** - * @param {number} [width=800] - the width of the canvas view - * @param {number} [height=600] - the height of the canvas view + * @param {number} [screenWidth=800] - the width of the screen + * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false @@ -34,9 +34,9 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(width, height, options = {}) + constructor(screenWidth, screenHeight, options = {}) { - super('Canvas', width, height, options); + super('Canvas', screenWidth, screenHeight, options); this.type = RENDERER_TYPE.CANVAS; @@ -96,7 +96,7 @@ this.context = null; this.renderingToScreen = false; - this.resize(width, height); + this.resize(screenWidth, screenHeight); } /** @@ -283,12 +283,12 @@ * * @extends PIXI.SystemRenderer#resize * - * @param {number} width - The new width of the canvas view - * @param {number} height - The new height of the canvas view + * @param {number} screenWidth - the new width of the screen + * @param {number} screenHeight - the new height of the screen */ - resize(width, height) + resize(screenWidth, screenHeight) { - super.resize(width, height); + super.resize(screenWidth, screenHeight); // reset the scale mode.. oddly this seems to be reset when the canvas is resized. // surely a browser bug?? Let pixi fix that for you.. diff --git a/src/core/renderers/webgl/TextureManager.js b/src/core/renderers/webgl/TextureManager.js index b685c1a..8227289 100644 --- a/src/core/renderers/webgl/TextureManager.js +++ b/src/core/renderers/webgl/TextureManager.js @@ -61,7 +61,7 @@ * Updates and/or Creates a WebGL texture for the renderer's context. * * @param {PIXI.BaseTexture|PIXI.Texture} texture - the texture to update - * @param {Number} location - the location the texture will be bound to. + * @param {number} location - the location the texture will be bound to. * @return {GLTexture} The gl texture. */ updateTexture(texture, location) diff --git a/src/core/renderers/webgl/WebGLRenderer.js b/src/core/renderers/webgl/WebGLRenderer.js index e4cd8a0..830b382 100644 --- a/src/core/renderers/webgl/WebGLRenderer.js +++ b/src/core/renderers/webgl/WebGLRenderer.js @@ -31,8 +31,8 @@ { /** * - * @param {number} [width=0] - the width of the canvas view - * @param {number} [height=0] - the height of the canvas view + * @param {number} [screenWidth=800] - the width of the screen + * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false @@ -53,9 +53,9 @@ * @param {boolean} [options.legacy=false] - If true Pixi will aim to ensure compatibility * with older / less advanced devices. If you experiance unexplained flickering try setting this to true. */ - constructor(width, height, options = {}) + constructor(screenWidth, screenHeight, options = {}) { - super('WebGL', width, height, options); + super('WebGL', screenWidth, screenHeight, options); this.legacy = !!options.legacy; @@ -245,7 +245,7 @@ glCore._testingContext = gl; // setup the width/height properties and gl viewport - this.resize(this.width, this.height); + this.resize(this.screen.width, this.screen.height); } /** @@ -343,16 +343,16 @@ /** * Resizes the webGL view to the specified width and height. * - * @param {number} width - the new width of the webGL view - * @param {number} height - the new height of the webGL view + * @param {number} screenWidth - the new width of the screen + * @param {number} screenHeight - the new height of the screen */ - resize(width, height) + resize(screenWidth, screenHeight) { // if(width * this.resolution === this.width && height * this.resolution === this.height)return; - SystemRenderer.prototype.resize.call(this, width, height); + SystemRenderer.prototype.resize.call(this, screenWidth, screenHeight); - this.rootRenderTarget.resize(width, height); + this.rootRenderTarget.resize(screenWidth, screenHeight); if (this._activeRenderTarget === this.rootRenderTarget) { @@ -396,6 +396,26 @@ } /** + * Erases the render texture and fills the drawing area with a colour + * + * @param {PIXI.RenderTexture} renderTexture - The render texture to clear + * @param {number} [clearColor] - The colour + * @return {PIXI.WebGLRenderer} Returns itself. + */ + clearRenderTexture(renderTexture, clearColor) + { + const baseTexture = renderTexture.baseTexture; + const renderTarget = baseTexture._glRenderTargets[this.CONTEXT_UID]; + + if (renderTarget) + { + renderTarget.clear(clearColor); + } + + return this; + } + + /** * Binds a render texture for rendering * * @param {PIXI.RenderTexture} renderTexture - The render texture to render @@ -474,9 +494,10 @@ * Changes the current GLShader to the one given in parameter * * @param {PIXI.glCore.Shader} shader - the new glShader + * @param {boolean} [autoProject=true] - Whether automatically set the projection matrix * @return {PIXI.WebGLRenderer} Returns itself. */ - _bindGLShader(shader) + _bindGLShader(shader, autoProject) { // TODO cache if (this._activeShader !== shader) @@ -484,8 +505,14 @@ this._activeShader = shader; shader.bind(); - // automatically set the projection matrix - shader.uniforms.projectionMatrix = this._activeRenderTarget.projectionMatrix.toArray(true); + // `autoProject` normally would be a default parameter set to true + // but because of how Babel transpiles default parameters + // it hinders the performance of this method. + if (autoProject !== false) + { + // automatically set the projection matrix + shader.uniforms.projectionMatrix = this._activeRenderTarget.projectionMatrix.toArray(true); + } } return this; diff --git a/src/core/renderers/webgl/WebGLState.js b/src/core/renderers/webgl/WebGLState.js index ba6ecc5..6c846d5 100755 --- a/src/core/renderers/webgl/WebGLState.js +++ b/src/core/renderers/webgl/WebGLState.js @@ -82,18 +82,20 @@ push() { // next state.. - let state = this.stack[++this.stackIndex]; + let state = this.stack[this.stackIndex]; if (!state) { state = this.stack[this.stackIndex] = new Uint8Array(16); } + ++this.stackIndex; + // copy state.. // set active state so we can force overrides of gl state for (let i = 0; i < this.activeState.length; i++) { - this.activeState[i] = state[i]; + state[i] = this.activeState[i]; } } diff --git a/src/core/renderers/webgl/filters/Filter.js b/src/core/renderers/webgl/filters/Filter.js index 2c3eaad..4eb6128 100644 --- a/src/core/renderers/webgl/filters/Filter.js +++ b/src/core/renderers/webgl/filters/Filter.js @@ -9,7 +9,7 @@ * @memberof PIXI * @extends PIXI.Shader */ -class Filter extends Shader +export default class Filter extends Shader { /** * @param {string} [vertexSrc] - The source of the vertex shader. @@ -56,15 +56,16 @@ * @param {PIXI.RenderTarget} input - The input render target. * @param {PIXI.RenderTarget} output - The target to output to. * @param {boolean} clear - Should the output be cleared before rendering to it + * @param {object} [currentState] - It's current state of filter. + * There are some useful properties in the currentState : + * target, filters, sourceFrame, destinationFrame, renderTarget, resolution */ - apply(filterManager, input, output, clear) + apply(filterManager, input, output, clear, currentState) // eslint-disable-line no-unused-vars { // do as you please! - filterManager.applyFilter(this, input, output, clear); + filterManager.applyFilter(this, input, output, clear, currentState); // or just do a regular render.. } } - -export default Filter; diff --git a/src/core/renderers/webgl/filters/extractUniformsFromSrc.js b/src/core/renderers/webgl/filters/extractUniformsFromSrc.js index d78a0b1..4893214 100644 --- a/src/core/renderers/webgl/filters/extractUniformsFromSrc.js +++ b/src/core/renderers/webgl/filters/extractUniformsFromSrc.js @@ -12,7 +12,7 @@ function extractUniformsFromString(string) { - const maskRegex = new RegExp('^(projectionMatrix|uSampler|filterArea)$'); + const maskRegex = new RegExp('^(projectionMatrix|uSampler|filterArea|filterClamp)$'); const uniforms = {}; let nameSplit; diff --git a/src/core/renderers/webgl/managers/FilterManager.js b/src/core/renderers/webgl/managers/FilterManager.js index 43fc869..6c7422d 100644 --- a/src/core/renderers/webgl/managers/FilterManager.js +++ b/src/core/renderers/webgl/managers/FilterManager.js @@ -2,6 +2,7 @@ import RenderTarget from '../utils/RenderTarget'; import Quad from '../utils/Quad'; import { Rectangle } from '../../../math'; +import Shader from '../../../Shader'; import * as filterTransforms from '../filters/filterTransforms'; import bitTwiddle from 'bit-twiddle'; @@ -22,8 +23,6 @@ this.filters = []; this.target = null; this.resolution = 1; - - this.firstRun = true; } } @@ -155,7 +154,7 @@ if (filters.length === 1) { - filters[0].apply(this, currentState.renderTarget, lastState.renderTarget, false); + filters[0].apply(this, currentState.renderTarget, lastState.renderTarget, false, currentState); this.freePotRenderTarget(currentState.renderTarget); } else @@ -177,7 +176,7 @@ for (i = 0; i < filters.length - 1; ++i) { - filters[i].apply(this, flip, flop, true); + filters[i].apply(this, flip, flop, true, currentState); const t = flip; @@ -185,7 +184,7 @@ flop = t; } - filters[i].apply(this, flip, lastState.renderTarget, false); + filters[i].apply(this, flip, lastState.renderTarget, false, currentState); this.freePotRenderTarget(flip); this.freePotRenderTarget(flop); @@ -212,16 +211,28 @@ const renderer = this.renderer; const gl = renderer.gl; - renderer.bindShader(filter, true); + let shader = filter.glShaders[renderer.CONTEXT_UID]; - // TODO theres a better way! - // quad can be a mesh and we then should be able to pull this off without accessing the shader directly - const shader = renderer.shaderManager.getGLShader(); - - // TODO change quad to mesh.. - if (!this.firstRun) + // cacheing.. + if (!shader) { - this.firstRun = true; + if (filter.glShaderKey) + { + shader = this.shaderCache[filter.glShaderKey]; + + if (!shader) + { + shader = new Shader(this.gl, filter.vertexSrc, filter.fragmentSrc); + + filter.glShaders[renderer.CONTEXT_UID] = this.shaderCache[filter.glShaderKey] = shader; + } + } + else + { + shader = filter.glShaders[renderer.CONTEXT_UID] = new Shader(this.gl, filter.vertexSrc, filter.fragmentSrc); + } + + // TODO - this only needs to be done once? renderer.bindVao(null); this.quad.initVao(shader); @@ -244,58 +255,146 @@ renderer.maskManager.pushScissorMask(null, renderer.maskManager.scissorData); } - // renderer._bindGLShader(shader); + renderer.bindShader(shader); - let currentState; - const uniforms = filter.uniforms; + // free unit 0 for us, doesn't matter what was there + // don't try to restore it, because syncUniforms can upload it to another slot + // and it'll be a problem + const tex = this.renderer.emptyTextures[0]; - if (uniforms.filterArea) - { - currentState = this.filterData.stack[this.filterData.index]; - - const filterArea = uniforms.filterArea; - - filterArea[0] = currentState.renderTarget.size.width; - filterArea[1] = currentState.renderTarget.size.height; - filterArea[2] = currentState.sourceFrame.x; - filterArea[3] = currentState.sourceFrame.y; - - filter.uniforms.filterArea = filterArea; - } - - // use this to clamp displaced texture coords so they belong to filterArea - // see displacementFilter fragment shader for an example - if (uniforms.filterClamp) - { - currentState = this.filterData.stack[this.filterData.index]; - - const filterClamp = uniforms.filterClamp; - - filterClamp[0] = 0; - filterClamp[1] = 0; - filterClamp[2] = (currentState.sourceFrame.width - 1) / currentState.renderTarget.size.width; - filterClamp[3] = (currentState.sourceFrame.height - 1) / currentState.renderTarget.size.height; - - filter.uniforms.filterClamp = filterClamp; - } - - renderer.shaderManager.setUniforms(filter.uniforms); + this.renderer.boundTextures[0] = tex; + // this syncs the pixi filters uniforms with glsl uniforms + this.syncUniforms(shader, filter); renderer.state.setBlendMode(filter.blendMode); - // temporary bypass cache.. - const tex = this.renderer.boundTextures[0]; - gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, input.texture.texture); this.quad.vao.draw(this.renderer.gl.TRIANGLES, 6, 0); - // restore cache. gl.bindTexture(gl.TEXTURE_2D, tex._glTextures[this.renderer.CONTEXT_UID].texture); } /** + * Uploads the uniforms of the filter. + * + * @param {GLShader} shader - The underlying gl shader. + * @param {PIXI.Filter} filter - The filter we are synchronizing. + */ + syncUniforms(shader, filter) + { + const uniformData = filter.uniformData; + const uniforms = filter.uniforms; + + // 0 is reserved for the pixi texture so we start at 1! + let textureCount = 1; + let currentState; + + // filterArea and filterClamp that are handled by FilterManager directly + // they must not appear in uniformData + + if (shader.uniforms.filterArea) + { + currentState = this.filterData.stack[this.filterData.index]; + + const filterArea = shader.uniforms.filterArea; + + filterArea[0] = currentState.renderTarget.size.width; + filterArea[1] = currentState.renderTarget.size.height; + filterArea[2] = currentState.sourceFrame.x; + filterArea[3] = currentState.sourceFrame.y; + + shader.uniforms.filterArea = filterArea; + } + + // use this to clamp displaced texture coords so they belong to filterArea + // see displacementFilter fragment shader for an example + if (shader.uniforms.filterClamp) + { + currentState = currentState || this.filterData.stack[this.filterData.index]; + + const filterClamp = shader.uniforms.filterClamp; + + filterClamp[0] = 0; + filterClamp[1] = 0; + filterClamp[2] = (currentState.sourceFrame.width - 1) / currentState.renderTarget.size.width; + filterClamp[3] = (currentState.sourceFrame.height - 1) / currentState.renderTarget.size.height; + + shader.uniforms.filterClamp = filterClamp; + } + + // TODO Cacheing layer.. + for (const i in uniformData) + { + if (uniformData[i].type === 'sampler2D' && uniforms[i] !== 0) + { + if (uniforms[i].baseTexture) + { + shader.uniforms[i] = this.renderer.bindTexture(uniforms[i].baseTexture, textureCount); + } + else + { + shader.uniforms[i] = textureCount; + + // TODO + // this is helpful as renderTargets can also be set. + // Although thinking about it, we could probably + // make the filter texture cache return a RenderTexture + // rather than a renderTarget + const gl = this.renderer.gl; + + this.renderer.boundTextures[textureCount] = this.renderer.emptyTextures[textureCount]; + gl.activeTexture(gl.TEXTURE0 + textureCount); + + uniforms[i].texture.bind(); + } + + textureCount++; + } + else if (uniformData[i].type === 'mat3') + { + // check if its pixi matrix.. + if (uniforms[i].a !== undefined) + { + shader.uniforms[i] = uniforms[i].toArray(true); + } + else + { + shader.uniforms[i] = uniforms[i]; + } + } + else if (uniformData[i].type === 'vec2') + { + // check if its a point.. + if (uniforms[i].x !== undefined) + { + const val = shader.uniforms[i] || new Float32Array(2); + + val[0] = uniforms[i].x; + val[1] = uniforms[i].y; + shader.uniforms[i] = val; + } + else + { + shader.uniforms[i] = uniforms[i]; + } + } + else if (uniformData[i].type === 'float') + { + if (shader.uniforms.data[i].value !== uniformData[i]) + { + shader.uniforms[i] = uniforms[i]; + } + } + else + { + shader.uniforms[i] = uniforms[i]; + } + } + } + + /** * Gets a render target from the pool, or creates a new one. * * @param {boolean} clear - Should we clear the render texture when we get it? @@ -442,7 +541,6 @@ renderTarget.resolution = resolution; renderTarget.defaultFrame.width = renderTarget.size.width = minWidth / resolution; renderTarget.defaultFrame.height = renderTarget.size.height = minHeight / resolution; - renderTarget.attachStencilBuffer(); return renderTarget; } diff --git a/src/core/settings.js b/src/core/settings.js index bb08cb0..f4dd087 100644 --- a/src/core/settings.js +++ b/src/core/settings.js @@ -77,11 +77,11 @@ * * @static * @memberof PIXI.settings - * @type {RegExp|string} + * @type {RegExp} * @example `@2x` - * @default /@(.+)x/ + * @default /@([0-9\.]+)x/ */ - RETINA_PREFIX: /@(.+)x/, + RETINA_PREFIX: /@([0-9\.]+)x/, /** * The default render options if none are supplied to {@link PIXI.WebGLRenderer} @@ -175,14 +175,24 @@ SCALE_MODE: 0, /** - * Default specify float precision in shaders. + * Default specify float precision in vertex shader. + * + * @static + * @memberof PIXI.settings + * @type {PIXI.PRECISION} + * @default PIXI.PRECISION.HIGH + */ + PRECISION_VERTEX: 'highp', + + /** + * Default specify float precision in fragment shader. * * @static * @memberof PIXI.settings * @type {PIXI.PRECISION} * @default PIXI.PRECISION.MEDIUM */ - PRECISION: 'mediump', + PRECISION_FRAGMENT: 'mediump', /** * Can we upload the same buffer in a single frame? diff --git a/src/core/sprites/Sprite.js b/src/core/sprites/Sprite.js index 28b0b18..121bf58 100644 --- a/src/core/sprites/Sprite.js +++ b/src/core/sprites/Sprite.js @@ -212,11 +212,11 @@ } else { - w0 = orig.width * (1 - anchor._x); - w1 = orig.width * -anchor._x; + w1 = -anchor._x * orig.width; + w0 = w1 + orig.width; - h0 = orig.height * (1 - anchor._y); - h1 = orig.height * -anchor._y; + h1 = -anchor._y * orig.height; + h0 = h1 + orig.height; } // xy @@ -269,11 +269,11 @@ const tx = wt.tx; const ty = wt.ty; - const w0 = (orig.width) * (1 - anchor._x); - const w1 = (orig.width) * -anchor._x; + const w1 = -anchor._x * orig.width; + const w0 = w1 + orig.width; - const h0 = orig.height * (1 - anchor._y); - const h1 = orig.height * -anchor._y; + const h1 = -anchor._y * orig.height; + const h0 = h1 + orig.height; // xy vertexData[0] = (a * w1) + (c * h1) + tx; @@ -440,7 +440,7 @@ * * @static * @param {number|string|PIXI.BaseTexture|HTMLCanvasElement|HTMLVideoElement} source Source to create texture from - * @return {PIXI.Sprite} The newly created texture + * @return {PIXI.Sprite} The newly created sprite */ static from(source) { @@ -538,8 +538,8 @@ } /** - * The tint applied to the sprite. This is a hex value. A value of - * 0xFFFFFF will remove any tint effect. + * The tint applied to the sprite. This is a hex value. + * A value of 0xFFFFFF will remove any tint effect. * * @member {number} * @default 0xFFFFFF diff --git a/src/core/sprites/canvas/CanvasTinter.js b/src/core/sprites/canvas/CanvasTinter.js index 1cf316b..27e5e0c 100644 --- a/src/core/sprites/canvas/CanvasTinter.js +++ b/src/core/sprites/canvas/CanvasTinter.js @@ -75,8 +75,8 @@ crop.width *= resolution; crop.height *= resolution; - canvas.width = crop.width; - canvas.height = crop.height; + canvas.width = Math.ceil(crop.width); + canvas.height = Math.ceil(crop.height); context.fillStyle = `#${(`00000${(color | 0).toString(16)}`).substr(-6)}`; @@ -130,8 +130,8 @@ crop.width *= resolution; crop.height *= resolution; - canvas.width = crop.width; - canvas.height = crop.height; + canvas.width = Math.ceil(crop.width); + canvas.height = Math.ceil(crop.height); context.globalCompositeOperation = 'copy'; context.fillStyle = `#${(`00000${(color | 0).toString(16)}`).substr(-6)}`; @@ -172,8 +172,8 @@ crop.width *= resolution; crop.height *= resolution; - canvas.width = crop.width; - canvas.height = crop.height; + canvas.width = Math.ceil(crop.width); + canvas.height = Math.ceil(crop.height); context.globalCompositeOperation = 'copy'; context.drawImage( diff --git a/src/core/sprites/webgl/SpriteRenderer.js b/src/core/sprites/webgl/SpriteRenderer.js index 2affcd5..7c352e2 100644 --- a/src/core/sprites/webgl/SpriteRenderer.js +++ b/src/core/sprites/webgl/SpriteRenderer.js @@ -30,7 +30,7 @@ /** * Number of values sent in the vertex buffer. - * positionX, positionY, colorR, colorG, colorB = 5 + * aVertexPosition(2), aTextureCoord(1), aColor(1), aTextureId(1) = 5 * * @member {number} */ @@ -75,7 +75,6 @@ this.shader = null; this.currentIndex = 0; - TICK = 0; this.groups = []; for (let k = 0; k < this.size; k++) diff --git a/src/core/textures/BaseTexture.js b/src/core/textures/BaseTexture.js index 853de7e..aeccf39 100644 --- a/src/core/textures/BaseTexture.js +++ b/src/core/textures/BaseTexture.js @@ -232,16 +232,24 @@ this.realWidth = this.source.naturalWidth || this.source.videoWidth || this.source.width; this.realHeight = this.source.naturalHeight || this.source.videoHeight || this.source.height; - this.width = this.realWidth / this.resolution; - this.height = this.realHeight / this.resolution; - - this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); + this._updateDimensions(); } this.emit('update', this); } /** + * Update dimensions from real values + */ + _updateDimensions() + { + this.width = this.realWidth / this.resolution; + this.height = this.realHeight / this.resolution; + + this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); + } + + /** * Load a source. * * If the source is not-immediately-available, such as an image that needs to be @@ -525,11 +533,7 @@ this.realWidth = Math.round(svgWidth * this.sourceScale); this.realHeight = Math.round(svgHeight * this.sourceScale); - this.width = this.realWidth / this.resolution; - this.height = this.realHeight / this.resolution; - - // Check pow2 after scale - this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); + this._updateDimensions(); // Create a canvas element const canvas = document.createElement('canvas'); @@ -690,4 +694,52 @@ return baseTexture; } + + /** + * Helper function that creates a base texture based on the source you provide. + * The source can be - image url, image element, canvas element. + * + * @static + * @param {string|HTMLImageElement|HTMLCanvasElement} source - The source to create base texture from. + * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values + * @param {number} [sourceScale=(auto)] - Scale for the original image, used with Svg images. + * @return {PIXI.BaseTexture} The new base texture. + */ + static from(source, scaleMode, sourceScale) + { + if (typeof source === 'string') + { + return BaseTexture.fromImage(source, undefined, scaleMode, sourceScale); + } + else if (source instanceof HTMLImageElement) + { + const imageUrl = source.src; + let baseTexture = BaseTextureCache[imageUrl]; + + if (!baseTexture) + { + baseTexture = new BaseTexture(source, scaleMode); + baseTexture.imageUrl = imageUrl; + + if (sourceScale) + { + baseTexture.sourceScale = sourceScale; + } + + // if there is an @2x at the end of the url we are going to assume its a highres image + baseTexture.resolution = getResolutionOfUrl(imageUrl); + + BaseTextureCache[imageUrl] = baseTexture; + } + + return baseTexture; + } + else if (source instanceof HTMLCanvasElement) + { + return BaseTexture.fromCanvas(source, scaleMode); + } + + // lets assume its a base texture! + return source; + } } diff --git a/src/core/textures/Spritesheet.js b/src/core/textures/Spritesheet.js new file mode 100644 index 0000000..d469c62 --- /dev/null +++ b/src/core/textures/Spritesheet.js @@ -0,0 +1,274 @@ +import { Rectangle, Texture } from '../'; +import { getResolutionOfUrl, TextureCache } from '../utils'; + +/** + * Utility class for maintaining reference to a collection + * of Textures on a single Spritesheet. + * + * @class + * @memberof PIXI + */ +export default class Spritesheet +{ + /** + * The maximum number of Textures to build per process. + * + * @type {number} + * @default 1000 + */ + static get BATCH_SIZE() + { + return 1000; + } + + /** + * @param {PIXI.BaseTexture} baseTexture Reference to the source BaseTexture object. + * @param {Object} data - Spritesheet image data. + * @param {string} [resolutionFilename] - The filename to consider when determining + * the resolution of the spritesheet. If not provided, the imageUrl will + * be used on the BaseTexture. + */ + constructor(baseTexture, data, resolutionFilename = null) + { + /** + * Reference to ths source texture + * @type {PIXI.BaseTexture} + */ + this.baseTexture = baseTexture; + + /** + * Map of spritesheet textures. + * @type {Object} + */ + this.textures = {}; + + /** + * Reference to the original JSON data. + * @type {Object} + */ + this.data = data; + + /** + * The resolution of the spritesheet. + * @type {number} + */ + this.resolution = this._updateResolution( + resolutionFilename || this.baseTexture.imageUrl + ); + + /** + * Map of spritesheet frames. + * @type {Object} + * @private + */ + this._frames = this.data.frames; + + /** + * Collection of frame names. + * @type {string[]} + * @private + */ + this._frameKeys = Object.keys(this._frames); + + /** + * Current batch index being processed. + * @type {number} + * @private + */ + this._batchIndex = 0; + + /** + * Callback when parse is completed. + * @type {Function} + * @private + */ + this._callback = null; + } + + /** + * Generate the resolution from the filename or fallback + * to the meta.scale field of the JSON data. + * + * @private + * @param {string} resolutionFilename - The filename to use for resolving + * the default resolution. + * @return {number} Resolution to use for spritesheet. + */ + _updateResolution(resolutionFilename) + { + const scale = this.data.meta.scale; + + // Use a defaultValue of `null` to check if a url-based resolution is set + let resolution = getResolutionOfUrl(resolutionFilename, null); + + // No resolution found via URL + if (resolution === null) + { + // Use the scale value or default to 1 + resolution = scale !== undefined ? parseFloat(scale) : 1; + } + + // For non-1 resolutions, update baseTexture + if (resolution !== 1) + { + this.baseTexture.resolution = resolution; + this.baseTexture.update(); + } + + return resolution; + } + + /** + * Parser spritesheet from loaded data. This is done asynchronously + * to prevent creating too many Texture within a single process. + * + * @param {Function} callback - Callback when complete returns + * a map of the Textures for this spritesheet. + */ + parse(callback) + { + this._batchIndex = 0; + this._callback = callback; + + if (this._frameKeys.length <= Spritesheet.BATCH_SIZE) + { + this._processFrames(0); + this._parseComplete(); + } + else + { + this._nextBatch(); + } + } + + /** + * Process a batch of frames + * + * @private + * @param {number} initialFrameIndex - The index of frame to start. + */ + _processFrames(initialFrameIndex) + { + let frameIndex = initialFrameIndex; + const maxFrames = Spritesheet.BATCH_SIZE; + + while (frameIndex - initialFrameIndex < maxFrames && frameIndex < this._frameKeys.length) + { + const i = this._frameKeys[frameIndex]; + const rect = this._frames[i].frame; + + if (rect) + { + let frame = null; + let trim = null; + const orig = new Rectangle( + 0, + 0, + this._frames[i].sourceSize.w / this.resolution, + this._frames[i].sourceSize.h / this.resolution + ); + + if (this._frames[i].rotated) + { + frame = new Rectangle( + rect.x / this.resolution, + rect.y / this.resolution, + rect.h / this.resolution, + rect.w / this.resolution + ); + } + else + { + frame = new Rectangle( + rect.x / this.resolution, + rect.y / this.resolution, + rect.w / this.resolution, + rect.h / this.resolution + ); + } + + // Check to see if the sprite is trimmed + if (this._frames[i].trimmed) + { + trim = new Rectangle( + this._frames[i].spriteSourceSize.x / this.resolution, + this._frames[i].spriteSourceSize.y / this.resolution, + rect.w / this.resolution, + rect.h / this.resolution + ); + } + + this.textures[i] = new Texture( + this.baseTexture, + frame, + orig, + trim, + this._frames[i].rotated ? 2 : 0 + ); + + // lets also add the frame to pixi's global cache for fromFrame and fromImage functions + TextureCache[i] = this.textures[i]; + } + + frameIndex++; + } + } + + /** + * The parse has completed. + * + * @private + */ + _parseComplete() + { + const callback = this._callback; + + this._callback = null; + this._batchIndex = 0; + callback.call(this, this.textures); + } + + /** + * Begin the next batch of textures. + * + * @private + */ + _nextBatch() + { + this._processFrames(this._batchIndex * Spritesheet.BATCH_SIZE); + this._batchIndex++; + setTimeout(() => + { + if (this._batchIndex * Spritesheet.BATCH_SIZE < this._frameKeys.length) + { + this._nextBatch(); + } + else + { + this._parseComplete(); + } + }, 0); + } + + /** + * Destroy Spritesheet and don't use after this. + * + * @param {boolean} [destroyBase=false] Whether to destroy the base texture as well + */ + destroy(destroyBase = false) + { + for (const i in this.textures) + { + this.textures[i].destroy(); + } + this._frames = null; + this._frameKeys = null; + this.data = null; + this.textures = null; + if (destroyBase) + { + this.baseTexture.destroy(); + } + this.baseTexture = null; + } +} diff --git a/src/core/textures/Texture.js b/src/core/textures/Texture.js index 6403d4f..afe4254 100644 --- a/src/core/textures/Texture.js +++ b/src/core/textures/Texture.js @@ -3,7 +3,7 @@ import TextureUvs from './TextureUvs'; import EventEmitter from 'eventemitter3'; import { Rectangle } from '../math'; -import { TextureCache, BaseTextureCache } from '../utils'; +import { TextureCache, BaseTextureCache, getResolutionOfUrl } from '../utils'; /** * A texture stores the information that represents an image or part of an image. It cannot be added @@ -365,7 +365,8 @@ * The source can be - frame id, image url, video url, canvas element, video element, base texture * * @static - * @param {number|string|PIXI.BaseTexture|HTMLCanvasElement|HTMLVideoElement} source - Source to create texture from + * @param {number|string|HTMLImageElement|HTMLCanvasElement|HTMLVideoElement|PIXI.BaseTexture} + * source - Source to create texture from * @return {PIXI.Texture} The newly created texture */ static from(source) @@ -393,7 +394,7 @@ } else if (source instanceof HTMLImageElement) { - return new Texture(new BaseTexture(source)); + return new Texture(BaseTexture.from(source)); } else if (source instanceof HTMLCanvasElement) { @@ -413,6 +414,43 @@ } /** + * Create a texture from a source and add to the cache. + * + * @static + * @param {HTMLImageElement|HTMLCanvasElement} source - The input source. + * @param {String} imageUrl - File name of texture, for cache and resolving resolution. + * @param {String} [name] - Human readible name for the texture cache. If no name is + * specified, only `imageUrl` will be used as the cache ID. + * @return {PIXI.Texture} Output texture + */ + static fromLoader(source, imageUrl, name) + { + const baseTexture = new BaseTexture(source, null, getResolutionOfUrl(imageUrl)); + const texture = new Texture(baseTexture); + + baseTexture.imageUrl = imageUrl; + + // No name, use imageUrl instead + if (!name) + { + name = imageUrl; + } + + // lets also add the frame to pixi's global cache for fromFrame and fromImage fucntions + BaseTextureCache[name] = baseTexture; + TextureCache[name] = texture; + + // also add references by url if they are different. + if (name !== imageUrl) + { + BaseTextureCache[imageUrl] = baseTexture; + TextureCache[imageUrl] = texture; + } + + return texture; + } + + /** * Adds a texture to the global TextureCache. This cache is shared across the whole PIXI object. * * @static @@ -522,6 +560,29 @@ } } +function createWhiteTexture() +{ + const canvas = document.createElement('canvas'); + + canvas.width = 10; + canvas.height = 10; + + const context = canvas.getContext('2d'); + + context.fillStyle = 'white'; + context.fillRect(0, 0, 10, 10); + + return new Texture(new BaseTexture(canvas)); +} + +function removeAllHandlers(tex) +{ + tex.destroy = function _emptyDestroy() { /* empty */ }; + tex.on = function _emptyOn() { /* empty */ }; + tex.once = function _emptyOnce() { /* empty */ }; + tex.emit = function _emptyEmit() { /* empty */ }; +} + /** * An empty texture, used often to not have to create multiple empty textures. * Can not be destroyed. @@ -530,7 +591,14 @@ * @constant */ Texture.EMPTY = new Texture(new BaseTexture()); -Texture.EMPTY.destroy = function _emptyDestroy() { /* empty */ }; -Texture.EMPTY.on = function _emptyOn() { /* empty */ }; -Texture.EMPTY.once = function _emptyOnce() { /* empty */ }; -Texture.EMPTY.emit = function _emptyEmit() { /* empty */ }; +removeAllHandlers(Texture.EMPTY); + +/** + * A white texture of 10x10 size, used for graphics and other things + * Can not be destroyed. + * + * @static + * @constant + */ +Texture.WHITE = createWhiteTexture(); +removeAllHandlers(Texture.WHITE); diff --git a/src/core/ticker/Ticker.js b/src/core/ticker/Ticker.js index d98a1f0..17a1517 100644 --- a/src/core/ticker/Ticker.js +++ b/src/core/ticker/Ticker.js @@ -67,11 +67,12 @@ * is based, this value is neither capped nor scaled. * If the platform supports DOMHighResTimeStamp, * this value will have a precision of 1 µs. + * Defaults to target frame time * * @member {number} - * @default 1 / TARGET_FPMS + * @default 16.66 */ - this.elapsedMS = 1 / settings.TARGET_FPMS; // default to target frame time + this.elapsedMS = 1 / settings.TARGET_FPMS; /** * The last time {@link PIXI.ticker.Ticker#update} was invoked. diff --git a/src/core/utils/index.js b/src/core/utils/index.js index 6ff609f..be44750 100644 --- a/src/core/utils/index.js +++ b/src/core/utils/index.js @@ -2,6 +2,7 @@ import settings from '../settings'; import EventEmitter from 'eventemitter3'; import pluginTarget from './pluginTarget'; +import * as mixins from './mixin'; import * as isMobile from 'ismobilejs'; let nextUid = 0; @@ -33,6 +34,7 @@ * @type {mixin} */ pluginTarget, + mixins, }; /** @@ -93,7 +95,7 @@ */ export function rgb2hex(rgb) { - return (((rgb[0] * 255) << 16) + ((rgb[1] * 255) << 8) + (rgb[2] * 255)); + return (((rgb[0] * 255) << 16) + ((rgb[1] * 255) << 8) + (rgb[2] * 255 | 0)); } /** @@ -368,3 +370,43 @@ * @private */ export const ProgramCache = {}; + +/** + * Destroys all texture in the cache + * + * @memberof PIXI.utils + * @function destroyTextureCache + */ +export function destroyTextureCache() +{ + let key; + + for (key in TextureCache) + { + TextureCache[key].destroy(); + } + for (key in BaseTextureCache) + { + BaseTextureCache[key].destroy(); + } +} + +/** + * Removes all textures from cache, but does not destroy them + * + * @memberof PIXI.utils + * @function clearTextureCache + */ +export function clearTextureCache() +{ + let key; + + for (key in TextureCache) + { + delete TextureCache[key]; + } + for (key in BaseTextureCache) + { + delete BaseTextureCache[key]; + } +} diff --git a/src/core/utils/mixin.js b/src/core/utils/mixin.js new file mode 100644 index 0000000..114de2a --- /dev/null +++ b/src/core/utils/mixin.js @@ -0,0 +1,59 @@ +/** + * Mixes all enumerable properties and methods from a source object to a target object. + * + * @memberof PIXI.utils.mixins + * @function mixin + * @param {object} target The prototype or instance that properties and methods should be added to. + * @param {object} source The source of properties and methods to mix in. + */ +export function mixin(target, source) +{ + if (!target || !source) return; + // in ES8/ES2017, this would be really easy: + // Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); + + // get all the enumerable property keys + const keys = Object.keys(source); + + // loop through properties + for (let i = 0; i < keys.length; ++i) + { + const propertyName = keys[i]; + + // Set the property using the property descriptor - this works for accessors and normal value properties + Object.defineProperty(target, propertyName, Object.getOwnPropertyDescriptor(source, propertyName)); + } +} + +const mixins = []; + +/** + * Queues a mixin to be handled towards the end of the initialization of PIXI, so that deprecation + * can take effect. + * + * @memberof PIXI.utils.mixins + * @function delayMixin + * @private + * @param {object} target The prototype or instance that properties and methods should be added to. + * @param {object} source The source of properties and methods to mix in. + */ +export function delayMixin(target, source) +{ + mixins.push(target, source); +} + +/** + * Handles all mixins queued via delayMixin(). + * + * @memberof PIXI.utils.mixins + * @function performMixins + * @private + */ +export function performMixins() +{ + for (let i = 0; i < mixins.length; i += 2) + { + mixin(mixins[i], mixins[i + 1]); + } + mixins.length = 0; +} diff --git a/src/deprecation.js b/src/deprecation.js index 045a19f..b9c73b3 100644 --- a/src/deprecation.js +++ b/src/deprecation.js @@ -5,6 +5,7 @@ import * as filters from './filters'; import * as prepare from './prepare'; import * as loaders from './loaders'; +import * as interaction from './interaction'; // provide method to give a stack track for warnings // useful for tracking-down where deprecated methods/properties/classes @@ -534,7 +535,7 @@ { parent: 'GC_MODES', target: 'GC_MODE' }, { parent: 'WRAP_MODES', target: 'WRAP_MODE' }, { parent: 'SCALE_MODES', target: 'SCALE_MODE' }, - { parent: 'PRECISION', target: 'PRECISION' }, + { parent: 'PRECISION', target: 'PRECISION_FRAGMENT' }, ]; for (let i = 0; i < defaults.length; i++) @@ -558,6 +559,32 @@ }); } +Object.defineProperties(core.settings, { + + /** + * @static + * @name PRECISION + * @memberof PIXI.settings + * @see PIXI.PRECISION + * @deprecated since version 4.4.0 + */ + PRECISION: { + enumerable: true, + get() + { + warn('PIXI.settings.PRECISION has been deprecated, please use PIXI.settings.PRECISION_FRAGMENT'); + + return core.settings.PRECISION_FRAGMENT; + }, + set(value) + { + warn('PIXI.settings.PRECISION has been deprecated, please use PIXI.settings.PRECISION_FRAGMENT'); + + core.settings.PRECISION_FRAGMENT = value; + }, + }, +}); + Object.defineProperties(extras, { /** @@ -694,7 +721,7 @@ warn('determineFontProperties is now deprecated, please use the static calculateFontProperties method, ' + 'e.g : Text.calculateFontProperties(fontStyle);'); - return Text.calculateFontProperties(fontStyle); + return core.Text.calculateFontProperties(fontStyle); }; Object.defineProperties(core.TextStyle.prototype, { @@ -1003,3 +1030,69 @@ }, }, }); + +/** + * @name PIXI.interaction.interactiveTarget#defaultCursor + * @static + * @type {number} + * @see PIXI.interaction.interactiveTarget#cursor + * @deprecated since 4.3.0 + */ +Object.defineProperty(interaction.interactiveTarget, 'defaultCursor', { + set(value) + { + warn('Property defaultCursor has been replaced with \'cursor\'. '); + this.cursor = value; + }, + get() + { + warn('Property defaultCursor has been replaced with \'cursor\'. '); + + return this.cursor; + }, + enumerable: true, +}); + +/** + * @name PIXI.interaction.InteractionManager#defaultCursorStyle + * @static + * @type {string} + * @see PIXI.interaction.InteractionManager#cursorStyles + * @deprecated since 4.3.0 + */ +Object.defineProperty(interaction.InteractionManager, 'defaultCursorStyle', { + set(value) + { + warn('Property defaultCursorStyle has been replaced with \'cursorStyles.default\'. '); + this.cursorStyles.default = value; + }, + get() + { + warn('Property defaultCursorStyle has been replaced with \'cursorStyles.default\'. '); + + return this.cursorStyles.default; + }, +}); + +/** + * @name PIXI.interaction.InteractionManager#currentCursorStyle + * @static + * @type {string} + * @see PIXI.interaction.InteractionManager#cursorStyles + * @deprecated since 4.3.0 + */ +Object.defineProperty(interaction.InteractionManager, 'currentCursorStyle', { + set(value) + { + warn('Property currentCursorStyle has been removed.' + + 'See the currentCursorMode property, which works differently.'); + this.currentCursorMode = value; + }, + get() + { + warn('Property currentCursorStyle has been removed.' + + 'See the currentCursorMode property, which works differently.'); + + return this.currentCursorMode; + }, +}); diff --git a/package.json b/package.json index 6a366ba..71ade15 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pixi.js", - "version": "4.3.4", + "version": "4.3.5", "description": "Pixi.js is a fast lightweight 2D library that works across all devices.", "author": "Mat Groves", "contributors": [ @@ -61,7 +61,7 @@ "ismobilejs": "^0.4.0", "object-assign": "^4.0.1", "pixi-gl-core": "^1.0.3", - "resource-loader": "^2.0.4" + "resource-loader": "^2.0.6" }, "devDependencies": { "babel-cli": "^6.18.0", @@ -70,9 +70,9 @@ "babel-preset-es2015": "^6.14.0", "babelify": "^7.3.0", "del": "^2.2.0", - "electron-prebuilt": "^1.3.2", + "electron": "^1.4.15", "eslint": "^3.5.0", - "floss": "^1.2.0", + "floss": "^2.0.1", "gh-pages": "^0.11.0", "jaguarjs-jsdoc": "^1.0.1", "js-md5": "^0.4.1", diff --git a/src/accessibility/AccessibilityManager.js b/src/accessibility/AccessibilityManager.js index df2716a..f16bcb1 100644 --- a/src/accessibility/AccessibilityManager.js +++ b/src/accessibility/AccessibilityManager.js @@ -3,7 +3,7 @@ import accessibleTarget from './accessibleTarget'; // add some extra variables to the container.. -Object.assign( +core.utils.mixins.delayMixin( core.DisplayObject.prototype, accessibleTarget ); diff --git a/src/core/Application.js b/src/core/Application.js index ea0f058..5340578 100644 --- a/src/core/Application.js +++ b/src/core/Application.js @@ -1,6 +1,6 @@ import { autoDetectRenderer } from './autoDetectRenderer'; import Container from './display/Container'; -import Ticker from './ticker/Ticker'; +import { shared, Ticker } from './ticker'; /** * Convenience class to create a new PIXI application. @@ -35,9 +35,9 @@ * @param {boolean} [noWebGL=false] - prevents selection of WebGL renderer, even if such is present * @param {boolean} [options.legacy=false] - If true Pixi will aim to ensure compatibility * with older / less advanced devices. If you experience unexplained flickering try setting this to true. - + * @param {boolean} [useSharedTicker=false] - `true` to use PIXI.ticker.shared, `false` to create new ticker. */ - constructor(width, height, options, noWebGL) + constructor(width, height, options, noWebGL, useSharedTicker = false) { /** * WebGL renderer if available, otherwise CanvasRenderer @@ -46,23 +46,46 @@ this.renderer = autoDetectRenderer(width, height, options, noWebGL); /** - * The root display container that's renderered. + * The root display container that's rendered. * @member {PIXI.Container} */ this.stage = new Container(); /** + * Internal reference to the ticker + * @member {PIXI.ticker.Ticker} + * @private + */ + this._ticker = null; + + /** * Ticker for doing render updates. * @member {PIXI.ticker.Ticker} + * @default PIXI.ticker.shared */ - this.ticker = new Ticker(); - - this.ticker.add(this.render, this); + this.ticker = useSharedTicker ? shared : new Ticker(); // Start the rendering this.start(); } + set ticker(ticker) // eslint-disable-line require-jsdoc + { + if (this._ticker) + { + this._ticker.remove(this.render, this); + } + this._ticker = ticker; + if (ticker) + { + ticker.add(this.render, this); + } + } + get ticker() // eslint-disable-line require-jsdoc + { + return this._ticker; + } + /** * Render the current stage. */ @@ -76,7 +99,7 @@ */ stop() { - this.ticker.stop(); + this._ticker.stop(); } /** @@ -84,7 +107,7 @@ */ start() { - this.ticker.start(); + this._ticker.start(); } /** @@ -98,13 +121,22 @@ } /** + * Reference to the renderer's screen rectangle. Its safe to use as filterArea or hitArea for whole screen + * @member {PIXI.Rectangle} + * @readonly + */ + get screen() + { + return this.renderer.screen; + } + + /** * Destroy and don't use after this. * @param {Boolean} [removeView=false] Automatically remove canvas from DOM. */ destroy(removeView) { this.stop(); - this.ticker.remove(this.render, this); this.ticker = null; this.stage.destroy(); diff --git a/src/core/Shader.js b/src/core/Shader.js new file mode 100644 index 0000000..78b17e6 --- /dev/null +++ b/src/core/Shader.js @@ -0,0 +1,46 @@ +import { GLShader } from 'pixi-gl-core'; +import settings from './settings'; + +function checkPrecision(src, def) +{ + if (src instanceof Array) + { + if (src[0].substring(0, 9) !== 'precision') + { + const copy = src.slice(0); + + copy.unshift(`precision ${def} float;`); + + return copy; + } + } + else if (src.substring(0, 9) !== 'precision') + { + return `precision ${def} float;\n${src}`; + } + + return src; +} + +/** + * Wrapper class, webGL Shader for Pixi. + * Adds precision string if vertexSrc or fragmentSrc have no mention of it. + * + * @class + * @extends GLShader + * @memberof PIXI + */ +export default class Shader extends GLShader +{ + /** + * + * @param {WebGLRenderingContext} gl - The current WebGL rendering context + * @param {string|string[]} vertexSrc - The vertex shader source as an array of strings. + * @param {string|string[]} fragmentSrc - The fragment shader source as an array of strings. + */ + constructor(gl, vertexSrc, fragmentSrc) + { + super(gl, checkPrecision(vertexSrc, settings.PRECISION_VERTEX), + checkPrecision(fragmentSrc, settings.PRECISION_FRAGMENT)); + } +} diff --git a/src/core/const.js b/src/core/const.js index ef6f662..3de0efe 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -188,7 +188,7 @@ * The gc modes that are supported by pixi. * * The {@link PIXI.settings.GC_MODE} Garbage Collection mode for pixi textures is AUTO - * If set to GC_MODE, the renderer will occasianally check textures usage. If they are not + * If set to GC_MODE, the renderer will occasionally check textures usage. If they are not * used for a specified period of time they will be removed from the GPU. They will of course * be uploaded again when they are required. This is a silent behind the scenes process that * should ensure that the GPU does not get filled up. diff --git a/src/core/display/Container.js b/src/core/display/Container.js index ed8cd5f..17fde8d 100644 --- a/src/core/display/Container.js +++ b/src/core/display/Container.js @@ -73,11 +73,12 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.push(child); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -108,11 +109,12 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.splice(index, 0, child); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -225,10 +227,11 @@ if (index === -1) return null; child.parent = null; + // ensure child transform will be recalculated + child.transform._parentID = -1; removeItems(this.children, index, 1); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -249,11 +252,12 @@ { const child = this.getChildAt(index); + // ensure child transform will be recalculated.. child.parent = null; + child.transform._parentID = -1; removeItems(this.children, index, 1); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -284,13 +288,12 @@ for (let i = 0; i < removed.length; ++i) { removed[i].parent = null; + if (removed[i].transform) + { + removed[i].transform._parentID = -1; + } } - // ensure a transform will be recalculated.. - if (this.transform) - { - this.transform._parentID = -1; - } this._boundsID++; this.onChildrenChange(beginIndex); diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index f37f2f5..be4c4fb 100644 --- a/src/core/graphics/Graphics.js +++ b/src/core/graphics/Graphics.js @@ -28,8 +28,9 @@ { /** * + * @param {boolean} [nativeLines=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP */ - constructor() + constructor(nativeLines = false) { super(); @@ -50,6 +51,13 @@ this.lineWidth = 0; /** + * If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * + * @member {boolean} + */ + this.nativeLines = nativeLines; + + /** * The color of any lines drawn. * * @member {string} @@ -757,28 +765,14 @@ if (!this._spriteRect) { - if (!Graphics._SPRITE_TEXTURE) - { - Graphics._SPRITE_TEXTURE = RenderTexture.create(10, 10); - - const canvas = document.createElement('canvas'); - - canvas.width = 10; - canvas.height = 10; - - const context = canvas.getContext('2d'); - - context.fillStyle = 'white'; - context.fillRect(0, 0, 10, 10); - - Graphics._SPRITE_TEXTURE = Texture.fromCanvas(canvas); - } - - this._spriteRect = new Sprite(Graphics._SPRITE_TEXTURE); + this._spriteRect = new Sprite(new Texture(Texture.WHITE)); } + + const sprite = this._spriteRect; + if (this.tint === 0xffffff) { - this._spriteRect.tint = this.graphicsData[0].fillColor; + sprite.tint = this.graphicsData[0].fillColor; } else { @@ -792,20 +786,21 @@ t1[1] *= t2[1]; t1[2] *= t2[2]; - this._spriteRect.tint = rgb2hex(t1); + sprite.tint = rgb2hex(t1); } - this._spriteRect.alpha = this.graphicsData[0].fillAlpha; - this._spriteRect.worldAlpha = this.worldAlpha * this._spriteRect.alpha; + sprite.alpha = this.graphicsData[0].fillAlpha; + sprite.worldAlpha = this.worldAlpha * sprite.alpha; + sprite.blendMode = this.blendMode; - Graphics._SPRITE_TEXTURE._frame.width = rect.width; - Graphics._SPRITE_TEXTURE._frame.height = rect.height; + sprite.texture._frame.width = rect.width; + sprite.texture._frame.height = rect.height; - this._spriteRect.transform.worldTransform = this.transform.worldTransform; + sprite.transform.worldTransform = this.transform.worldTransform; - this._spriteRect.anchor.set(-rect.x / rect.width, -rect.y / rect.height); - this._spriteRect._onAnchorUpdate(); + sprite.anchor.set(-rect.x / rect.width, -rect.y / rect.height); + sprite._onAnchorUpdate(); - this._spriteRect._renderWebGL(renderer); + sprite._renderWebGL(renderer); } /** @@ -1031,6 +1026,7 @@ this.fillColor, this.fillAlpha, this.filling, + this.nativeLines, shape ); diff --git a/src/core/graphics/GraphicsData.js b/src/core/graphics/GraphicsData.js index f5a87ae..b6f03c0 100644 --- a/src/core/graphics/GraphicsData.js +++ b/src/core/graphics/GraphicsData.js @@ -14,14 +14,19 @@ * @param {number} fillColor - the color of the fill * @param {number} fillAlpha - the alpha of the fill * @param {boolean} fill - whether or not the shape is filled with a colour + * @param {boolean} nativeLines - the method for drawing lines * @param {PIXI.Circle|PIXI.Rectangle|PIXI.Ellipse|PIXI.Polygon} shape - The shape object to draw. */ - constructor(lineWidth, lineColor, lineAlpha, fillColor, fillAlpha, fill, shape) + constructor(lineWidth, lineColor, lineAlpha, fillColor, fillAlpha, fill, nativeLines, shape) { /** * @member {number} the width of the line to draw */ this.lineWidth = lineWidth; + /** + * @member {boolean} if true the liens will be draw using LINES instead of TRIANGLE_STRIP + */ + this.nativeLines = nativeLines; /** * @member {number} the color of the line to draw @@ -85,6 +90,7 @@ this.fillColor, this.fillAlpha, this.fill, + this.nativeLines, this.shape ); } diff --git a/src/core/graphics/webgl/GraphicsRenderer.js b/src/core/graphics/webgl/GraphicsRenderer.js index 14083fe..67f5e92 100644 --- a/src/core/graphics/webgl/GraphicsRenderer.js +++ b/src/core/graphics/webgl/GraphicsRenderer.js @@ -102,7 +102,15 @@ shaderTemp.uniforms.alpha = graphics.worldAlpha; renderer.bindVao(webGLData.vao); - webGLData.vao.draw(gl.TRIANGLE_STRIP, webGLData.indices.length); + + if (graphics.nativeLines) + { + gl.drawArrays(gl.LINES, 0, webGLData.points.length / 6); + } + else + { + webGLData.vao.draw(gl.TRIANGLE_STRIP, webGLData.indices.length); + } } } diff --git a/src/core/graphics/webgl/utils/buildLine.js b/src/core/graphics/webgl/utils/buildLine.js index ecf43f7..4d6862e 100644 --- a/src/core/graphics/webgl/utils/buildLine.js +++ b/src/core/graphics/webgl/utils/buildLine.js @@ -11,7 +11,29 @@ * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties * @param {object} webGLData - an object containing all the webGL-specific information to create this shape */ -export default function buildLine(graphicsData, webGLData) +export default function (graphicsData, webGLData) +{ + if (graphicsData.nativeLines) + { + buildNativeLine(graphicsData, webGLData); + } + else + { + buildLine(graphicsData, webGLData); + } +} + +/** + * Builds a line to draw using the poligon method. + * + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @private + * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties + * @param {object} webGLData - an object containing all the webGL-specific information to create this shape + */ +function buildLine(graphicsData, webGLData) { // TODO OPTIMISE! let points = graphicsData.points; @@ -224,3 +246,46 @@ indices.push(indexStart - 1); } + +/** + * Builds a line to draw using the gl.drawArrays(gl.LINES) method + * + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @private + * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties + * @param {object} webGLData - an object containing all the webGL-specific information to create this shape + */ +function buildNativeLine(graphicsData, webGLData) +{ + let i = 0; + const points = graphicsData.points; + + if (points.length === 0) return; + + const verts = webGLData.points; + const length = points.length / 2; + + // sort color + const color = hex2rgb(graphicsData.lineColor); + const alpha = graphicsData.lineAlpha; + const r = color[0] * alpha; + const g = color[1] * alpha; + const b = color[2] * alpha; + + for (i = 1; i < length; i++) + { + const p1x = points[(i - 1) * 2]; + const p1y = points[((i - 1) * 2) + 1]; + + const p2x = points[i * 2]; + const p2y = points[(i * 2) + 1]; + + verts.push(p1x, p1y); + verts.push(r, g, b, alpha); + + verts.push(p2x, p2y); + verts.push(r, g, b, alpha); + } +} diff --git a/src/core/graphics/webgl/utils/buildRoundedRectangle.js b/src/core/graphics/webgl/utils/buildRoundedRectangle.js index 9abc807..a6c8461 100644 --- a/src/core/graphics/webgl/utils/buildRoundedRectangle.js +++ b/src/core/graphics/webgl/utils/buildRoundedRectangle.js @@ -77,6 +77,26 @@ } /** + * Calculate a single point for a quadratic bezier curve. + * Utility function used by quadraticBezierCurve. + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @private + * @param {number} n1 - first number + * @param {number} n2 - second number + * @param {number} perc - percentage + * @return {number} the result + * + */ +function getPt(n1, n2, perc) +{ + const diff = n2 - n1; + + return n1 + (diff * perc); +} + +/** * Calculate the points for a quadratic bezier curve. (helper function..) * Based on: https://stackoverflow.com/questions/785097/how-do-i-implement-a-bezier-curve-in-c * @@ -105,13 +125,6 @@ let x = 0; let y = 0; - function getPt(n1, n2, perc) - { - const diff = n2 - n1; - - return n1 + (diff * perc); - } - for (let i = 0, j = 0; i <= n; ++i) { j = i / n; diff --git a/src/core/index.js b/src/core/index.js index f7a817c..4f414e8 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -30,6 +30,7 @@ export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; export { default as CanvasGraphicsRenderer } from './graphics/canvas/CanvasGraphicsRenderer'; +export { default as Spritesheet } from './textures/Spritesheet'; export { default as Texture } from './textures/Texture'; export { default as BaseTexture } from './textures/BaseTexture'; export { default as RenderTexture } from './textures/RenderTexture'; diff --git a/src/core/math/Matrix.js b/src/core/math/Matrix.js index 0b77477..3679860 100644 --- a/src/core/math/Matrix.js +++ b/src/core/math/Matrix.js @@ -13,45 +13,50 @@ export default class Matrix { /** - * + * @param {number} [a=1] - x scale + * @param {number} [b=0] - y skew + * @param {number} [c=0] - x skew + * @param {number} [d=1] - y scale + * @param {number} [tx=0] - x translation + * @param {number} [ty=0] - y translation */ - constructor() + constructor(a = 1, b = 0, c = 0, d = 1, tx = 0, ty = 0) { /** * @member {number} * @default 1 */ - this.a = 1; + this.a = a; /** * @member {number} * @default 0 */ - this.b = 0; + this.b = b; /** * @member {number} * @default 0 */ - this.c = 0; + this.c = c; /** * @member {number} * @default 1 */ - this.d = 1; + this.d = d; /** * @member {number} * @default 0 */ - this.tx = 0; + this.tx = tx; /** * @member {number} * @default 0 */ - this.ty = 0; + this.ty = ty; this.array = null; } diff --git a/src/core/renderers/SystemRenderer.js b/src/core/renderers/SystemRenderer.js index fa116a6..a24c775 100644 --- a/src/core/renderers/SystemRenderer.js +++ b/src/core/renderers/SystemRenderer.js @@ -1,5 +1,5 @@ import { sayHello, hex2string, hex2rgb } from '../utils'; -import { Matrix } from '../math'; +import { Matrix, Rectangle } from '../math'; import { RENDERER_TYPE } from '../const'; import settings from '../settings'; import Container from '../display/Container'; @@ -21,8 +21,8 @@ { /** * @param {string} system - The name of the system this renderer is for. - * @param {number} [width=800] - the width of the canvas view - * @param {number} [height=600] - the height of the canvas view + * @param {number} [screenWidth=800] - the width of the screen + * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false @@ -37,7 +37,7 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(system, width, height, options) + constructor(system, screenWidth, screenHeight, options) { super(); @@ -69,20 +69,13 @@ this.type = RENDERER_TYPE.UNKNOWN; /** - * The width of the canvas view + * Measurements of the screen. (0, 0, screenWidth, screenHeight) * - * @member {number} - * @default 800 - */ - this.width = width || 800; - - /** - * The height of the canvas view + * Its safe to use as filterArea or hitArea for whole stage * - * @member {number} - * @default 600 + * @member {PIXI.Rectangle} */ - this.height = height || 600; + this.screen = new Rectangle(0, 0, screenWidth || 800, screenHeight || 600); /** * The canvas element that everything is drawn to @@ -107,7 +100,7 @@ this.transparent = options.transparent; /** - * Whether the render view should be resized automatically + * Whether css dimensions of canvas view should be resized to screen dimensions automatically * * @member {boolean} */ @@ -192,23 +185,48 @@ } /** - * Resizes the canvas view to the specified width and height + * Same as view.width, actual number of pixels in the canvas by horizontal * - * @param {number} width - the new width of the canvas view - * @param {number} height - the new height of the canvas view + * @member {number} + * @readonly + * @default 800 */ - resize(width, height) + get width() { - this.width = width * this.resolution; - this.height = height * this.resolution; + return this.view.width; + } - this.view.width = this.width; - this.view.height = this.height; + /** + * Same as view.height, actual number of pixels in the canvas by vertical + * + * @member {number} + * @readonly + * @default 600 + */ + get height() + { + return this.view.height; + } + + /** + * Resizes the screen and canvas to the specified width and height + * Canvas dimensions are multiplied by resolution + * + * @param {number} screenWidth - the new width of the screen + * @param {number} screenHeight - the new height of the screen + */ + resize(screenWidth, screenHeight) + { + this.screen.width = screenWidth; + this.screen.height = screenHeight; + + this.view.width = screenWidth * this.resolution; + this.view.height = screenHeight * this.resolution; if (this.autoResize) { - this.view.style.width = `${this.width / this.resolution}px`; - this.view.style.height = `${this.height / this.resolution}px`; + this.view.style.width = `${screenWidth}px`; + this.view.style.height = `${screenHeight}px`; } } @@ -249,11 +267,10 @@ this.type = RENDERER_TYPE.UNKNOWN; - this.width = 0; - this.height = 0; - this.view = null; + this.screen = null; + this.resolution = 0; this.transparent = false; diff --git a/src/core/renderers/canvas/CanvasRenderer.js b/src/core/renderers/canvas/CanvasRenderer.js index 1bd2d5e..e84e49c 100644 --- a/src/core/renderers/canvas/CanvasRenderer.js +++ b/src/core/renderers/canvas/CanvasRenderer.js @@ -18,8 +18,8 @@ export default class CanvasRenderer extends SystemRenderer { /** - * @param {number} [width=800] - the width of the canvas view - * @param {number} [height=600] - the height of the canvas view + * @param {number} [screenWidth=800] - the width of the screen + * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false @@ -34,9 +34,9 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(width, height, options = {}) + constructor(screenWidth, screenHeight, options = {}) { - super('Canvas', width, height, options); + super('Canvas', screenWidth, screenHeight, options); this.type = RENDERER_TYPE.CANVAS; @@ -96,7 +96,7 @@ this.context = null; this.renderingToScreen = false; - this.resize(width, height); + this.resize(screenWidth, screenHeight); } /** @@ -283,12 +283,12 @@ * * @extends PIXI.SystemRenderer#resize * - * @param {number} width - The new width of the canvas view - * @param {number} height - The new height of the canvas view + * @param {number} screenWidth - the new width of the screen + * @param {number} screenHeight - the new height of the screen */ - resize(width, height) + resize(screenWidth, screenHeight) { - super.resize(width, height); + super.resize(screenWidth, screenHeight); // reset the scale mode.. oddly this seems to be reset when the canvas is resized. // surely a browser bug?? Let pixi fix that for you.. diff --git a/src/core/renderers/webgl/TextureManager.js b/src/core/renderers/webgl/TextureManager.js index b685c1a..8227289 100644 --- a/src/core/renderers/webgl/TextureManager.js +++ b/src/core/renderers/webgl/TextureManager.js @@ -61,7 +61,7 @@ * Updates and/or Creates a WebGL texture for the renderer's context. * * @param {PIXI.BaseTexture|PIXI.Texture} texture - the texture to update - * @param {Number} location - the location the texture will be bound to. + * @param {number} location - the location the texture will be bound to. * @return {GLTexture} The gl texture. */ updateTexture(texture, location) diff --git a/src/core/renderers/webgl/WebGLRenderer.js b/src/core/renderers/webgl/WebGLRenderer.js index e4cd8a0..830b382 100644 --- a/src/core/renderers/webgl/WebGLRenderer.js +++ b/src/core/renderers/webgl/WebGLRenderer.js @@ -31,8 +31,8 @@ { /** * - * @param {number} [width=0] - the width of the canvas view - * @param {number} [height=0] - the height of the canvas view + * @param {number} [screenWidth=800] - the width of the screen + * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false @@ -53,9 +53,9 @@ * @param {boolean} [options.legacy=false] - If true Pixi will aim to ensure compatibility * with older / less advanced devices. If you experiance unexplained flickering try setting this to true. */ - constructor(width, height, options = {}) + constructor(screenWidth, screenHeight, options = {}) { - super('WebGL', width, height, options); + super('WebGL', screenWidth, screenHeight, options); this.legacy = !!options.legacy; @@ -245,7 +245,7 @@ glCore._testingContext = gl; // setup the width/height properties and gl viewport - this.resize(this.width, this.height); + this.resize(this.screen.width, this.screen.height); } /** @@ -343,16 +343,16 @@ /** * Resizes the webGL view to the specified width and height. * - * @param {number} width - the new width of the webGL view - * @param {number} height - the new height of the webGL view + * @param {number} screenWidth - the new width of the screen + * @param {number} screenHeight - the new height of the screen */ - resize(width, height) + resize(screenWidth, screenHeight) { // if(width * this.resolution === this.width && height * this.resolution === this.height)return; - SystemRenderer.prototype.resize.call(this, width, height); + SystemRenderer.prototype.resize.call(this, screenWidth, screenHeight); - this.rootRenderTarget.resize(width, height); + this.rootRenderTarget.resize(screenWidth, screenHeight); if (this._activeRenderTarget === this.rootRenderTarget) { @@ -396,6 +396,26 @@ } /** + * Erases the render texture and fills the drawing area with a colour + * + * @param {PIXI.RenderTexture} renderTexture - The render texture to clear + * @param {number} [clearColor] - The colour + * @return {PIXI.WebGLRenderer} Returns itself. + */ + clearRenderTexture(renderTexture, clearColor) + { + const baseTexture = renderTexture.baseTexture; + const renderTarget = baseTexture._glRenderTargets[this.CONTEXT_UID]; + + if (renderTarget) + { + renderTarget.clear(clearColor); + } + + return this; + } + + /** * Binds a render texture for rendering * * @param {PIXI.RenderTexture} renderTexture - The render texture to render @@ -474,9 +494,10 @@ * Changes the current GLShader to the one given in parameter * * @param {PIXI.glCore.Shader} shader - the new glShader + * @param {boolean} [autoProject=true] - Whether automatically set the projection matrix * @return {PIXI.WebGLRenderer} Returns itself. */ - _bindGLShader(shader) + _bindGLShader(shader, autoProject) { // TODO cache if (this._activeShader !== shader) @@ -484,8 +505,14 @@ this._activeShader = shader; shader.bind(); - // automatically set the projection matrix - shader.uniforms.projectionMatrix = this._activeRenderTarget.projectionMatrix.toArray(true); + // `autoProject` normally would be a default parameter set to true + // but because of how Babel transpiles default parameters + // it hinders the performance of this method. + if (autoProject !== false) + { + // automatically set the projection matrix + shader.uniforms.projectionMatrix = this._activeRenderTarget.projectionMatrix.toArray(true); + } } return this; diff --git a/src/core/renderers/webgl/WebGLState.js b/src/core/renderers/webgl/WebGLState.js index ba6ecc5..6c846d5 100755 --- a/src/core/renderers/webgl/WebGLState.js +++ b/src/core/renderers/webgl/WebGLState.js @@ -82,18 +82,20 @@ push() { // next state.. - let state = this.stack[++this.stackIndex]; + let state = this.stack[this.stackIndex]; if (!state) { state = this.stack[this.stackIndex] = new Uint8Array(16); } + ++this.stackIndex; + // copy state.. // set active state so we can force overrides of gl state for (let i = 0; i < this.activeState.length; i++) { - this.activeState[i] = state[i]; + state[i] = this.activeState[i]; } } diff --git a/src/core/renderers/webgl/filters/Filter.js b/src/core/renderers/webgl/filters/Filter.js index 2c3eaad..4eb6128 100644 --- a/src/core/renderers/webgl/filters/Filter.js +++ b/src/core/renderers/webgl/filters/Filter.js @@ -9,7 +9,7 @@ * @memberof PIXI * @extends PIXI.Shader */ -class Filter extends Shader +export default class Filter extends Shader { /** * @param {string} [vertexSrc] - The source of the vertex shader. @@ -56,15 +56,16 @@ * @param {PIXI.RenderTarget} input - The input render target. * @param {PIXI.RenderTarget} output - The target to output to. * @param {boolean} clear - Should the output be cleared before rendering to it + * @param {object} [currentState] - It's current state of filter. + * There are some useful properties in the currentState : + * target, filters, sourceFrame, destinationFrame, renderTarget, resolution */ - apply(filterManager, input, output, clear) + apply(filterManager, input, output, clear, currentState) // eslint-disable-line no-unused-vars { // do as you please! - filterManager.applyFilter(this, input, output, clear); + filterManager.applyFilter(this, input, output, clear, currentState); // or just do a regular render.. } } - -export default Filter; diff --git a/src/core/renderers/webgl/filters/extractUniformsFromSrc.js b/src/core/renderers/webgl/filters/extractUniformsFromSrc.js index d78a0b1..4893214 100644 --- a/src/core/renderers/webgl/filters/extractUniformsFromSrc.js +++ b/src/core/renderers/webgl/filters/extractUniformsFromSrc.js @@ -12,7 +12,7 @@ function extractUniformsFromString(string) { - const maskRegex = new RegExp('^(projectionMatrix|uSampler|filterArea)$'); + const maskRegex = new RegExp('^(projectionMatrix|uSampler|filterArea|filterClamp)$'); const uniforms = {}; let nameSplit; diff --git a/src/core/renderers/webgl/managers/FilterManager.js b/src/core/renderers/webgl/managers/FilterManager.js index 43fc869..6c7422d 100644 --- a/src/core/renderers/webgl/managers/FilterManager.js +++ b/src/core/renderers/webgl/managers/FilterManager.js @@ -2,6 +2,7 @@ import RenderTarget from '../utils/RenderTarget'; import Quad from '../utils/Quad'; import { Rectangle } from '../../../math'; +import Shader from '../../../Shader'; import * as filterTransforms from '../filters/filterTransforms'; import bitTwiddle from 'bit-twiddle'; @@ -22,8 +23,6 @@ this.filters = []; this.target = null; this.resolution = 1; - - this.firstRun = true; } } @@ -155,7 +154,7 @@ if (filters.length === 1) { - filters[0].apply(this, currentState.renderTarget, lastState.renderTarget, false); + filters[0].apply(this, currentState.renderTarget, lastState.renderTarget, false, currentState); this.freePotRenderTarget(currentState.renderTarget); } else @@ -177,7 +176,7 @@ for (i = 0; i < filters.length - 1; ++i) { - filters[i].apply(this, flip, flop, true); + filters[i].apply(this, flip, flop, true, currentState); const t = flip; @@ -185,7 +184,7 @@ flop = t; } - filters[i].apply(this, flip, lastState.renderTarget, false); + filters[i].apply(this, flip, lastState.renderTarget, false, currentState); this.freePotRenderTarget(flip); this.freePotRenderTarget(flop); @@ -212,16 +211,28 @@ const renderer = this.renderer; const gl = renderer.gl; - renderer.bindShader(filter, true); + let shader = filter.glShaders[renderer.CONTEXT_UID]; - // TODO theres a better way! - // quad can be a mesh and we then should be able to pull this off without accessing the shader directly - const shader = renderer.shaderManager.getGLShader(); - - // TODO change quad to mesh.. - if (!this.firstRun) + // cacheing.. + if (!shader) { - this.firstRun = true; + if (filter.glShaderKey) + { + shader = this.shaderCache[filter.glShaderKey]; + + if (!shader) + { + shader = new Shader(this.gl, filter.vertexSrc, filter.fragmentSrc); + + filter.glShaders[renderer.CONTEXT_UID] = this.shaderCache[filter.glShaderKey] = shader; + } + } + else + { + shader = filter.glShaders[renderer.CONTEXT_UID] = new Shader(this.gl, filter.vertexSrc, filter.fragmentSrc); + } + + // TODO - this only needs to be done once? renderer.bindVao(null); this.quad.initVao(shader); @@ -244,58 +255,146 @@ renderer.maskManager.pushScissorMask(null, renderer.maskManager.scissorData); } - // renderer._bindGLShader(shader); + renderer.bindShader(shader); - let currentState; - const uniforms = filter.uniforms; + // free unit 0 for us, doesn't matter what was there + // don't try to restore it, because syncUniforms can upload it to another slot + // and it'll be a problem + const tex = this.renderer.emptyTextures[0]; - if (uniforms.filterArea) - { - currentState = this.filterData.stack[this.filterData.index]; - - const filterArea = uniforms.filterArea; - - filterArea[0] = currentState.renderTarget.size.width; - filterArea[1] = currentState.renderTarget.size.height; - filterArea[2] = currentState.sourceFrame.x; - filterArea[3] = currentState.sourceFrame.y; - - filter.uniforms.filterArea = filterArea; - } - - // use this to clamp displaced texture coords so they belong to filterArea - // see displacementFilter fragment shader for an example - if (uniforms.filterClamp) - { - currentState = this.filterData.stack[this.filterData.index]; - - const filterClamp = uniforms.filterClamp; - - filterClamp[0] = 0; - filterClamp[1] = 0; - filterClamp[2] = (currentState.sourceFrame.width - 1) / currentState.renderTarget.size.width; - filterClamp[3] = (currentState.sourceFrame.height - 1) / currentState.renderTarget.size.height; - - filter.uniforms.filterClamp = filterClamp; - } - - renderer.shaderManager.setUniforms(filter.uniforms); + this.renderer.boundTextures[0] = tex; + // this syncs the pixi filters uniforms with glsl uniforms + this.syncUniforms(shader, filter); renderer.state.setBlendMode(filter.blendMode); - // temporary bypass cache.. - const tex = this.renderer.boundTextures[0]; - gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, input.texture.texture); this.quad.vao.draw(this.renderer.gl.TRIANGLES, 6, 0); - // restore cache. gl.bindTexture(gl.TEXTURE_2D, tex._glTextures[this.renderer.CONTEXT_UID].texture); } /** + * Uploads the uniforms of the filter. + * + * @param {GLShader} shader - The underlying gl shader. + * @param {PIXI.Filter} filter - The filter we are synchronizing. + */ + syncUniforms(shader, filter) + { + const uniformData = filter.uniformData; + const uniforms = filter.uniforms; + + // 0 is reserved for the pixi texture so we start at 1! + let textureCount = 1; + let currentState; + + // filterArea and filterClamp that are handled by FilterManager directly + // they must not appear in uniformData + + if (shader.uniforms.filterArea) + { + currentState = this.filterData.stack[this.filterData.index]; + + const filterArea = shader.uniforms.filterArea; + + filterArea[0] = currentState.renderTarget.size.width; + filterArea[1] = currentState.renderTarget.size.height; + filterArea[2] = currentState.sourceFrame.x; + filterArea[3] = currentState.sourceFrame.y; + + shader.uniforms.filterArea = filterArea; + } + + // use this to clamp displaced texture coords so they belong to filterArea + // see displacementFilter fragment shader for an example + if (shader.uniforms.filterClamp) + { + currentState = currentState || this.filterData.stack[this.filterData.index]; + + const filterClamp = shader.uniforms.filterClamp; + + filterClamp[0] = 0; + filterClamp[1] = 0; + filterClamp[2] = (currentState.sourceFrame.width - 1) / currentState.renderTarget.size.width; + filterClamp[3] = (currentState.sourceFrame.height - 1) / currentState.renderTarget.size.height; + + shader.uniforms.filterClamp = filterClamp; + } + + // TODO Cacheing layer.. + for (const i in uniformData) + { + if (uniformData[i].type === 'sampler2D' && uniforms[i] !== 0) + { + if (uniforms[i].baseTexture) + { + shader.uniforms[i] = this.renderer.bindTexture(uniforms[i].baseTexture, textureCount); + } + else + { + shader.uniforms[i] = textureCount; + + // TODO + // this is helpful as renderTargets can also be set. + // Although thinking about it, we could probably + // make the filter texture cache return a RenderTexture + // rather than a renderTarget + const gl = this.renderer.gl; + + this.renderer.boundTextures[textureCount] = this.renderer.emptyTextures[textureCount]; + gl.activeTexture(gl.TEXTURE0 + textureCount); + + uniforms[i].texture.bind(); + } + + textureCount++; + } + else if (uniformData[i].type === 'mat3') + { + // check if its pixi matrix.. + if (uniforms[i].a !== undefined) + { + shader.uniforms[i] = uniforms[i].toArray(true); + } + else + { + shader.uniforms[i] = uniforms[i]; + } + } + else if (uniformData[i].type === 'vec2') + { + // check if its a point.. + if (uniforms[i].x !== undefined) + { + const val = shader.uniforms[i] || new Float32Array(2); + + val[0] = uniforms[i].x; + val[1] = uniforms[i].y; + shader.uniforms[i] = val; + } + else + { + shader.uniforms[i] = uniforms[i]; + } + } + else if (uniformData[i].type === 'float') + { + if (shader.uniforms.data[i].value !== uniformData[i]) + { + shader.uniforms[i] = uniforms[i]; + } + } + else + { + shader.uniforms[i] = uniforms[i]; + } + } + } + + /** * Gets a render target from the pool, or creates a new one. * * @param {boolean} clear - Should we clear the render texture when we get it? @@ -442,7 +541,6 @@ renderTarget.resolution = resolution; renderTarget.defaultFrame.width = renderTarget.size.width = minWidth / resolution; renderTarget.defaultFrame.height = renderTarget.size.height = minHeight / resolution; - renderTarget.attachStencilBuffer(); return renderTarget; } diff --git a/src/core/settings.js b/src/core/settings.js index bb08cb0..f4dd087 100644 --- a/src/core/settings.js +++ b/src/core/settings.js @@ -77,11 +77,11 @@ * * @static * @memberof PIXI.settings - * @type {RegExp|string} + * @type {RegExp} * @example `@2x` - * @default /@(.+)x/ + * @default /@([0-9\.]+)x/ */ - RETINA_PREFIX: /@(.+)x/, + RETINA_PREFIX: /@([0-9\.]+)x/, /** * The default render options if none are supplied to {@link PIXI.WebGLRenderer} @@ -175,14 +175,24 @@ SCALE_MODE: 0, /** - * Default specify float precision in shaders. + * Default specify float precision in vertex shader. + * + * @static + * @memberof PIXI.settings + * @type {PIXI.PRECISION} + * @default PIXI.PRECISION.HIGH + */ + PRECISION_VERTEX: 'highp', + + /** + * Default specify float precision in fragment shader. * * @static * @memberof PIXI.settings * @type {PIXI.PRECISION} * @default PIXI.PRECISION.MEDIUM */ - PRECISION: 'mediump', + PRECISION_FRAGMENT: 'mediump', /** * Can we upload the same buffer in a single frame? diff --git a/src/core/sprites/Sprite.js b/src/core/sprites/Sprite.js index 28b0b18..121bf58 100644 --- a/src/core/sprites/Sprite.js +++ b/src/core/sprites/Sprite.js @@ -212,11 +212,11 @@ } else { - w0 = orig.width * (1 - anchor._x); - w1 = orig.width * -anchor._x; + w1 = -anchor._x * orig.width; + w0 = w1 + orig.width; - h0 = orig.height * (1 - anchor._y); - h1 = orig.height * -anchor._y; + h1 = -anchor._y * orig.height; + h0 = h1 + orig.height; } // xy @@ -269,11 +269,11 @@ const tx = wt.tx; const ty = wt.ty; - const w0 = (orig.width) * (1 - anchor._x); - const w1 = (orig.width) * -anchor._x; + const w1 = -anchor._x * orig.width; + const w0 = w1 + orig.width; - const h0 = orig.height * (1 - anchor._y); - const h1 = orig.height * -anchor._y; + const h1 = -anchor._y * orig.height; + const h0 = h1 + orig.height; // xy vertexData[0] = (a * w1) + (c * h1) + tx; @@ -440,7 +440,7 @@ * * @static * @param {number|string|PIXI.BaseTexture|HTMLCanvasElement|HTMLVideoElement} source Source to create texture from - * @return {PIXI.Sprite} The newly created texture + * @return {PIXI.Sprite} The newly created sprite */ static from(source) { @@ -538,8 +538,8 @@ } /** - * The tint applied to the sprite. This is a hex value. A value of - * 0xFFFFFF will remove any tint effect. + * The tint applied to the sprite. This is a hex value. + * A value of 0xFFFFFF will remove any tint effect. * * @member {number} * @default 0xFFFFFF diff --git a/src/core/sprites/canvas/CanvasTinter.js b/src/core/sprites/canvas/CanvasTinter.js index 1cf316b..27e5e0c 100644 --- a/src/core/sprites/canvas/CanvasTinter.js +++ b/src/core/sprites/canvas/CanvasTinter.js @@ -75,8 +75,8 @@ crop.width *= resolution; crop.height *= resolution; - canvas.width = crop.width; - canvas.height = crop.height; + canvas.width = Math.ceil(crop.width); + canvas.height = Math.ceil(crop.height); context.fillStyle = `#${(`00000${(color | 0).toString(16)}`).substr(-6)}`; @@ -130,8 +130,8 @@ crop.width *= resolution; crop.height *= resolution; - canvas.width = crop.width; - canvas.height = crop.height; + canvas.width = Math.ceil(crop.width); + canvas.height = Math.ceil(crop.height); context.globalCompositeOperation = 'copy'; context.fillStyle = `#${(`00000${(color | 0).toString(16)}`).substr(-6)}`; @@ -172,8 +172,8 @@ crop.width *= resolution; crop.height *= resolution; - canvas.width = crop.width; - canvas.height = crop.height; + canvas.width = Math.ceil(crop.width); + canvas.height = Math.ceil(crop.height); context.globalCompositeOperation = 'copy'; context.drawImage( diff --git a/src/core/sprites/webgl/SpriteRenderer.js b/src/core/sprites/webgl/SpriteRenderer.js index 2affcd5..7c352e2 100644 --- a/src/core/sprites/webgl/SpriteRenderer.js +++ b/src/core/sprites/webgl/SpriteRenderer.js @@ -30,7 +30,7 @@ /** * Number of values sent in the vertex buffer. - * positionX, positionY, colorR, colorG, colorB = 5 + * aVertexPosition(2), aTextureCoord(1), aColor(1), aTextureId(1) = 5 * * @member {number} */ @@ -75,7 +75,6 @@ this.shader = null; this.currentIndex = 0; - TICK = 0; this.groups = []; for (let k = 0; k < this.size; k++) diff --git a/src/core/textures/BaseTexture.js b/src/core/textures/BaseTexture.js index 853de7e..aeccf39 100644 --- a/src/core/textures/BaseTexture.js +++ b/src/core/textures/BaseTexture.js @@ -232,16 +232,24 @@ this.realWidth = this.source.naturalWidth || this.source.videoWidth || this.source.width; this.realHeight = this.source.naturalHeight || this.source.videoHeight || this.source.height; - this.width = this.realWidth / this.resolution; - this.height = this.realHeight / this.resolution; - - this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); + this._updateDimensions(); } this.emit('update', this); } /** + * Update dimensions from real values + */ + _updateDimensions() + { + this.width = this.realWidth / this.resolution; + this.height = this.realHeight / this.resolution; + + this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); + } + + /** * Load a source. * * If the source is not-immediately-available, such as an image that needs to be @@ -525,11 +533,7 @@ this.realWidth = Math.round(svgWidth * this.sourceScale); this.realHeight = Math.round(svgHeight * this.sourceScale); - this.width = this.realWidth / this.resolution; - this.height = this.realHeight / this.resolution; - - // Check pow2 after scale - this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); + this._updateDimensions(); // Create a canvas element const canvas = document.createElement('canvas'); @@ -690,4 +694,52 @@ return baseTexture; } + + /** + * Helper function that creates a base texture based on the source you provide. + * The source can be - image url, image element, canvas element. + * + * @static + * @param {string|HTMLImageElement|HTMLCanvasElement} source - The source to create base texture from. + * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values + * @param {number} [sourceScale=(auto)] - Scale for the original image, used with Svg images. + * @return {PIXI.BaseTexture} The new base texture. + */ + static from(source, scaleMode, sourceScale) + { + if (typeof source === 'string') + { + return BaseTexture.fromImage(source, undefined, scaleMode, sourceScale); + } + else if (source instanceof HTMLImageElement) + { + const imageUrl = source.src; + let baseTexture = BaseTextureCache[imageUrl]; + + if (!baseTexture) + { + baseTexture = new BaseTexture(source, scaleMode); + baseTexture.imageUrl = imageUrl; + + if (sourceScale) + { + baseTexture.sourceScale = sourceScale; + } + + // if there is an @2x at the end of the url we are going to assume its a highres image + baseTexture.resolution = getResolutionOfUrl(imageUrl); + + BaseTextureCache[imageUrl] = baseTexture; + } + + return baseTexture; + } + else if (source instanceof HTMLCanvasElement) + { + return BaseTexture.fromCanvas(source, scaleMode); + } + + // lets assume its a base texture! + return source; + } } diff --git a/src/core/textures/Spritesheet.js b/src/core/textures/Spritesheet.js new file mode 100644 index 0000000..d469c62 --- /dev/null +++ b/src/core/textures/Spritesheet.js @@ -0,0 +1,274 @@ +import { Rectangle, Texture } from '../'; +import { getResolutionOfUrl, TextureCache } from '../utils'; + +/** + * Utility class for maintaining reference to a collection + * of Textures on a single Spritesheet. + * + * @class + * @memberof PIXI + */ +export default class Spritesheet +{ + /** + * The maximum number of Textures to build per process. + * + * @type {number} + * @default 1000 + */ + static get BATCH_SIZE() + { + return 1000; + } + + /** + * @param {PIXI.BaseTexture} baseTexture Reference to the source BaseTexture object. + * @param {Object} data - Spritesheet image data. + * @param {string} [resolutionFilename] - The filename to consider when determining + * the resolution of the spritesheet. If not provided, the imageUrl will + * be used on the BaseTexture. + */ + constructor(baseTexture, data, resolutionFilename = null) + { + /** + * Reference to ths source texture + * @type {PIXI.BaseTexture} + */ + this.baseTexture = baseTexture; + + /** + * Map of spritesheet textures. + * @type {Object} + */ + this.textures = {}; + + /** + * Reference to the original JSON data. + * @type {Object} + */ + this.data = data; + + /** + * The resolution of the spritesheet. + * @type {number} + */ + this.resolution = this._updateResolution( + resolutionFilename || this.baseTexture.imageUrl + ); + + /** + * Map of spritesheet frames. + * @type {Object} + * @private + */ + this._frames = this.data.frames; + + /** + * Collection of frame names. + * @type {string[]} + * @private + */ + this._frameKeys = Object.keys(this._frames); + + /** + * Current batch index being processed. + * @type {number} + * @private + */ + this._batchIndex = 0; + + /** + * Callback when parse is completed. + * @type {Function} + * @private + */ + this._callback = null; + } + + /** + * Generate the resolution from the filename or fallback + * to the meta.scale field of the JSON data. + * + * @private + * @param {string} resolutionFilename - The filename to use for resolving + * the default resolution. + * @return {number} Resolution to use for spritesheet. + */ + _updateResolution(resolutionFilename) + { + const scale = this.data.meta.scale; + + // Use a defaultValue of `null` to check if a url-based resolution is set + let resolution = getResolutionOfUrl(resolutionFilename, null); + + // No resolution found via URL + if (resolution === null) + { + // Use the scale value or default to 1 + resolution = scale !== undefined ? parseFloat(scale) : 1; + } + + // For non-1 resolutions, update baseTexture + if (resolution !== 1) + { + this.baseTexture.resolution = resolution; + this.baseTexture.update(); + } + + return resolution; + } + + /** + * Parser spritesheet from loaded data. This is done asynchronously + * to prevent creating too many Texture within a single process. + * + * @param {Function} callback - Callback when complete returns + * a map of the Textures for this spritesheet. + */ + parse(callback) + { + this._batchIndex = 0; + this._callback = callback; + + if (this._frameKeys.length <= Spritesheet.BATCH_SIZE) + { + this._processFrames(0); + this._parseComplete(); + } + else + { + this._nextBatch(); + } + } + + /** + * Process a batch of frames + * + * @private + * @param {number} initialFrameIndex - The index of frame to start. + */ + _processFrames(initialFrameIndex) + { + let frameIndex = initialFrameIndex; + const maxFrames = Spritesheet.BATCH_SIZE; + + while (frameIndex - initialFrameIndex < maxFrames && frameIndex < this._frameKeys.length) + { + const i = this._frameKeys[frameIndex]; + const rect = this._frames[i].frame; + + if (rect) + { + let frame = null; + let trim = null; + const orig = new Rectangle( + 0, + 0, + this._frames[i].sourceSize.w / this.resolution, + this._frames[i].sourceSize.h / this.resolution + ); + + if (this._frames[i].rotated) + { + frame = new Rectangle( + rect.x / this.resolution, + rect.y / this.resolution, + rect.h / this.resolution, + rect.w / this.resolution + ); + } + else + { + frame = new Rectangle( + rect.x / this.resolution, + rect.y / this.resolution, + rect.w / this.resolution, + rect.h / this.resolution + ); + } + + // Check to see if the sprite is trimmed + if (this._frames[i].trimmed) + { + trim = new Rectangle( + this._frames[i].spriteSourceSize.x / this.resolution, + this._frames[i].spriteSourceSize.y / this.resolution, + rect.w / this.resolution, + rect.h / this.resolution + ); + } + + this.textures[i] = new Texture( + this.baseTexture, + frame, + orig, + trim, + this._frames[i].rotated ? 2 : 0 + ); + + // lets also add the frame to pixi's global cache for fromFrame and fromImage functions + TextureCache[i] = this.textures[i]; + } + + frameIndex++; + } + } + + /** + * The parse has completed. + * + * @private + */ + _parseComplete() + { + const callback = this._callback; + + this._callback = null; + this._batchIndex = 0; + callback.call(this, this.textures); + } + + /** + * Begin the next batch of textures. + * + * @private + */ + _nextBatch() + { + this._processFrames(this._batchIndex * Spritesheet.BATCH_SIZE); + this._batchIndex++; + setTimeout(() => + { + if (this._batchIndex * Spritesheet.BATCH_SIZE < this._frameKeys.length) + { + this._nextBatch(); + } + else + { + this._parseComplete(); + } + }, 0); + } + + /** + * Destroy Spritesheet and don't use after this. + * + * @param {boolean} [destroyBase=false] Whether to destroy the base texture as well + */ + destroy(destroyBase = false) + { + for (const i in this.textures) + { + this.textures[i].destroy(); + } + this._frames = null; + this._frameKeys = null; + this.data = null; + this.textures = null; + if (destroyBase) + { + this.baseTexture.destroy(); + } + this.baseTexture = null; + } +} diff --git a/src/core/textures/Texture.js b/src/core/textures/Texture.js index 6403d4f..afe4254 100644 --- a/src/core/textures/Texture.js +++ b/src/core/textures/Texture.js @@ -3,7 +3,7 @@ import TextureUvs from './TextureUvs'; import EventEmitter from 'eventemitter3'; import { Rectangle } from '../math'; -import { TextureCache, BaseTextureCache } from '../utils'; +import { TextureCache, BaseTextureCache, getResolutionOfUrl } from '../utils'; /** * A texture stores the information that represents an image or part of an image. It cannot be added @@ -365,7 +365,8 @@ * The source can be - frame id, image url, video url, canvas element, video element, base texture * * @static - * @param {number|string|PIXI.BaseTexture|HTMLCanvasElement|HTMLVideoElement} source - Source to create texture from + * @param {number|string|HTMLImageElement|HTMLCanvasElement|HTMLVideoElement|PIXI.BaseTexture} + * source - Source to create texture from * @return {PIXI.Texture} The newly created texture */ static from(source) @@ -393,7 +394,7 @@ } else if (source instanceof HTMLImageElement) { - return new Texture(new BaseTexture(source)); + return new Texture(BaseTexture.from(source)); } else if (source instanceof HTMLCanvasElement) { @@ -413,6 +414,43 @@ } /** + * Create a texture from a source and add to the cache. + * + * @static + * @param {HTMLImageElement|HTMLCanvasElement} source - The input source. + * @param {String} imageUrl - File name of texture, for cache and resolving resolution. + * @param {String} [name] - Human readible name for the texture cache. If no name is + * specified, only `imageUrl` will be used as the cache ID. + * @return {PIXI.Texture} Output texture + */ + static fromLoader(source, imageUrl, name) + { + const baseTexture = new BaseTexture(source, null, getResolutionOfUrl(imageUrl)); + const texture = new Texture(baseTexture); + + baseTexture.imageUrl = imageUrl; + + // No name, use imageUrl instead + if (!name) + { + name = imageUrl; + } + + // lets also add the frame to pixi's global cache for fromFrame and fromImage fucntions + BaseTextureCache[name] = baseTexture; + TextureCache[name] = texture; + + // also add references by url if they are different. + if (name !== imageUrl) + { + BaseTextureCache[imageUrl] = baseTexture; + TextureCache[imageUrl] = texture; + } + + return texture; + } + + /** * Adds a texture to the global TextureCache. This cache is shared across the whole PIXI object. * * @static @@ -522,6 +560,29 @@ } } +function createWhiteTexture() +{ + const canvas = document.createElement('canvas'); + + canvas.width = 10; + canvas.height = 10; + + const context = canvas.getContext('2d'); + + context.fillStyle = 'white'; + context.fillRect(0, 0, 10, 10); + + return new Texture(new BaseTexture(canvas)); +} + +function removeAllHandlers(tex) +{ + tex.destroy = function _emptyDestroy() { /* empty */ }; + tex.on = function _emptyOn() { /* empty */ }; + tex.once = function _emptyOnce() { /* empty */ }; + tex.emit = function _emptyEmit() { /* empty */ }; +} + /** * An empty texture, used often to not have to create multiple empty textures. * Can not be destroyed. @@ -530,7 +591,14 @@ * @constant */ Texture.EMPTY = new Texture(new BaseTexture()); -Texture.EMPTY.destroy = function _emptyDestroy() { /* empty */ }; -Texture.EMPTY.on = function _emptyOn() { /* empty */ }; -Texture.EMPTY.once = function _emptyOnce() { /* empty */ }; -Texture.EMPTY.emit = function _emptyEmit() { /* empty */ }; +removeAllHandlers(Texture.EMPTY); + +/** + * A white texture of 10x10 size, used for graphics and other things + * Can not be destroyed. + * + * @static + * @constant + */ +Texture.WHITE = createWhiteTexture(); +removeAllHandlers(Texture.WHITE); diff --git a/src/core/ticker/Ticker.js b/src/core/ticker/Ticker.js index d98a1f0..17a1517 100644 --- a/src/core/ticker/Ticker.js +++ b/src/core/ticker/Ticker.js @@ -67,11 +67,12 @@ * is based, this value is neither capped nor scaled. * If the platform supports DOMHighResTimeStamp, * this value will have a precision of 1 µs. + * Defaults to target frame time * * @member {number} - * @default 1 / TARGET_FPMS + * @default 16.66 */ - this.elapsedMS = 1 / settings.TARGET_FPMS; // default to target frame time + this.elapsedMS = 1 / settings.TARGET_FPMS; /** * The last time {@link PIXI.ticker.Ticker#update} was invoked. diff --git a/src/core/utils/index.js b/src/core/utils/index.js index 6ff609f..be44750 100644 --- a/src/core/utils/index.js +++ b/src/core/utils/index.js @@ -2,6 +2,7 @@ import settings from '../settings'; import EventEmitter from 'eventemitter3'; import pluginTarget from './pluginTarget'; +import * as mixins from './mixin'; import * as isMobile from 'ismobilejs'; let nextUid = 0; @@ -33,6 +34,7 @@ * @type {mixin} */ pluginTarget, + mixins, }; /** @@ -93,7 +95,7 @@ */ export function rgb2hex(rgb) { - return (((rgb[0] * 255) << 16) + ((rgb[1] * 255) << 8) + (rgb[2] * 255)); + return (((rgb[0] * 255) << 16) + ((rgb[1] * 255) << 8) + (rgb[2] * 255 | 0)); } /** @@ -368,3 +370,43 @@ * @private */ export const ProgramCache = {}; + +/** + * Destroys all texture in the cache + * + * @memberof PIXI.utils + * @function destroyTextureCache + */ +export function destroyTextureCache() +{ + let key; + + for (key in TextureCache) + { + TextureCache[key].destroy(); + } + for (key in BaseTextureCache) + { + BaseTextureCache[key].destroy(); + } +} + +/** + * Removes all textures from cache, but does not destroy them + * + * @memberof PIXI.utils + * @function clearTextureCache + */ +export function clearTextureCache() +{ + let key; + + for (key in TextureCache) + { + delete TextureCache[key]; + } + for (key in BaseTextureCache) + { + delete BaseTextureCache[key]; + } +} diff --git a/src/core/utils/mixin.js b/src/core/utils/mixin.js new file mode 100644 index 0000000..114de2a --- /dev/null +++ b/src/core/utils/mixin.js @@ -0,0 +1,59 @@ +/** + * Mixes all enumerable properties and methods from a source object to a target object. + * + * @memberof PIXI.utils.mixins + * @function mixin + * @param {object} target The prototype or instance that properties and methods should be added to. + * @param {object} source The source of properties and methods to mix in. + */ +export function mixin(target, source) +{ + if (!target || !source) return; + // in ES8/ES2017, this would be really easy: + // Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); + + // get all the enumerable property keys + const keys = Object.keys(source); + + // loop through properties + for (let i = 0; i < keys.length; ++i) + { + const propertyName = keys[i]; + + // Set the property using the property descriptor - this works for accessors and normal value properties + Object.defineProperty(target, propertyName, Object.getOwnPropertyDescriptor(source, propertyName)); + } +} + +const mixins = []; + +/** + * Queues a mixin to be handled towards the end of the initialization of PIXI, so that deprecation + * can take effect. + * + * @memberof PIXI.utils.mixins + * @function delayMixin + * @private + * @param {object} target The prototype or instance that properties and methods should be added to. + * @param {object} source The source of properties and methods to mix in. + */ +export function delayMixin(target, source) +{ + mixins.push(target, source); +} + +/** + * Handles all mixins queued via delayMixin(). + * + * @memberof PIXI.utils.mixins + * @function performMixins + * @private + */ +export function performMixins() +{ + for (let i = 0; i < mixins.length; i += 2) + { + mixin(mixins[i], mixins[i + 1]); + } + mixins.length = 0; +} diff --git a/src/deprecation.js b/src/deprecation.js index 045a19f..b9c73b3 100644 --- a/src/deprecation.js +++ b/src/deprecation.js @@ -5,6 +5,7 @@ import * as filters from './filters'; import * as prepare from './prepare'; import * as loaders from './loaders'; +import * as interaction from './interaction'; // provide method to give a stack track for warnings // useful for tracking-down where deprecated methods/properties/classes @@ -534,7 +535,7 @@ { parent: 'GC_MODES', target: 'GC_MODE' }, { parent: 'WRAP_MODES', target: 'WRAP_MODE' }, { parent: 'SCALE_MODES', target: 'SCALE_MODE' }, - { parent: 'PRECISION', target: 'PRECISION' }, + { parent: 'PRECISION', target: 'PRECISION_FRAGMENT' }, ]; for (let i = 0; i < defaults.length; i++) @@ -558,6 +559,32 @@ }); } +Object.defineProperties(core.settings, { + + /** + * @static + * @name PRECISION + * @memberof PIXI.settings + * @see PIXI.PRECISION + * @deprecated since version 4.4.0 + */ + PRECISION: { + enumerable: true, + get() + { + warn('PIXI.settings.PRECISION has been deprecated, please use PIXI.settings.PRECISION_FRAGMENT'); + + return core.settings.PRECISION_FRAGMENT; + }, + set(value) + { + warn('PIXI.settings.PRECISION has been deprecated, please use PIXI.settings.PRECISION_FRAGMENT'); + + core.settings.PRECISION_FRAGMENT = value; + }, + }, +}); + Object.defineProperties(extras, { /** @@ -694,7 +721,7 @@ warn('determineFontProperties is now deprecated, please use the static calculateFontProperties method, ' + 'e.g : Text.calculateFontProperties(fontStyle);'); - return Text.calculateFontProperties(fontStyle); + return core.Text.calculateFontProperties(fontStyle); }; Object.defineProperties(core.TextStyle.prototype, { @@ -1003,3 +1030,69 @@ }, }, }); + +/** + * @name PIXI.interaction.interactiveTarget#defaultCursor + * @static + * @type {number} + * @see PIXI.interaction.interactiveTarget#cursor + * @deprecated since 4.3.0 + */ +Object.defineProperty(interaction.interactiveTarget, 'defaultCursor', { + set(value) + { + warn('Property defaultCursor has been replaced with \'cursor\'. '); + this.cursor = value; + }, + get() + { + warn('Property defaultCursor has been replaced with \'cursor\'. '); + + return this.cursor; + }, + enumerable: true, +}); + +/** + * @name PIXI.interaction.InteractionManager#defaultCursorStyle + * @static + * @type {string} + * @see PIXI.interaction.InteractionManager#cursorStyles + * @deprecated since 4.3.0 + */ +Object.defineProperty(interaction.InteractionManager, 'defaultCursorStyle', { + set(value) + { + warn('Property defaultCursorStyle has been replaced with \'cursorStyles.default\'. '); + this.cursorStyles.default = value; + }, + get() + { + warn('Property defaultCursorStyle has been replaced with \'cursorStyles.default\'. '); + + return this.cursorStyles.default; + }, +}); + +/** + * @name PIXI.interaction.InteractionManager#currentCursorStyle + * @static + * @type {string} + * @see PIXI.interaction.InteractionManager#cursorStyles + * @deprecated since 4.3.0 + */ +Object.defineProperty(interaction.InteractionManager, 'currentCursorStyle', { + set(value) + { + warn('Property currentCursorStyle has been removed.' + + 'See the currentCursorMode property, which works differently.'); + this.currentCursorMode = value; + }, + get() + { + warn('Property currentCursorStyle has been removed.' + + 'See the currentCursorMode property, which works differently.'); + + return this.currentCursorMode; + }, +}); diff --git a/src/extras/BitmapText.js b/src/extras/BitmapText.js index fb6c343..0a0e150 100644 --- a/src/extras/BitmapText.js +++ b/src/extras/BitmapText.js @@ -455,6 +455,73 @@ return this._textHeight; } + + /** + * Register a bitmap font with data and a texture. + * + * @static + * @param {XMLDocument} xml - The XML document data. + * @param {PIXI.Texture} texture - Texture with all symbols. + * @return {Object} Result font object with font, size, lineHeight and char fields. + */ + static registerFont(xml, texture) + { + const data = {}; + const info = xml.getElementsByTagName('info')[0]; + const common = xml.getElementsByTagName('common')[0]; + + data.font = info.getAttribute('face'); + data.size = parseInt(info.getAttribute('size'), 10); + data.lineHeight = parseInt(common.getAttribute('lineHeight'), 10); + data.chars = {}; + + // parse letters + const letters = xml.getElementsByTagName('char'); + + for (let i = 0; i < letters.length; i++) + { + const letter = letters[i]; + const charCode = parseInt(letter.getAttribute('id'), 10); + + const textureRect = new core.Rectangle( + parseInt(letter.getAttribute('x'), 10) + texture.frame.x, + parseInt(letter.getAttribute('y'), 10) + texture.frame.y, + parseInt(letter.getAttribute('width'), 10), + parseInt(letter.getAttribute('height'), 10) + ); + + data.chars[charCode] = { + xOffset: parseInt(letter.getAttribute('xoffset'), 10), + yOffset: parseInt(letter.getAttribute('yoffset'), 10), + xAdvance: parseInt(letter.getAttribute('xadvance'), 10), + kerning: {}, + texture: new core.Texture(texture.baseTexture, textureRect), + + }; + } + + // parse kernings + const kernings = xml.getElementsByTagName('kerning'); + + for (let i = 0; i < kernings.length; i++) + { + const kerning = kernings[i]; + const first = parseInt(kerning.getAttribute('first'), 10); + const second = parseInt(kerning.getAttribute('second'), 10); + const amount = parseInt(kerning.getAttribute('amount'), 10); + + if (data.chars[second]) + { + data.chars[second].kerning[first] = amount; + } + } + + // I'm leaving this as a temporary fix so we can test the bitmap fonts in v3 + // but it's very likely to change + BitmapText.fonts[data.font] = data; + + return data; + } } BitmapText.fonts = {}; diff --git a/package.json b/package.json index 6a366ba..71ade15 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pixi.js", - "version": "4.3.4", + "version": "4.3.5", "description": "Pixi.js is a fast lightweight 2D library that works across all devices.", "author": "Mat Groves", "contributors": [ @@ -61,7 +61,7 @@ "ismobilejs": "^0.4.0", "object-assign": "^4.0.1", "pixi-gl-core": "^1.0.3", - "resource-loader": "^2.0.4" + "resource-loader": "^2.0.6" }, "devDependencies": { "babel-cli": "^6.18.0", @@ -70,9 +70,9 @@ "babel-preset-es2015": "^6.14.0", "babelify": "^7.3.0", "del": "^2.2.0", - "electron-prebuilt": "^1.3.2", + "electron": "^1.4.15", "eslint": "^3.5.0", - "floss": "^1.2.0", + "floss": "^2.0.1", "gh-pages": "^0.11.0", "jaguarjs-jsdoc": "^1.0.1", "js-md5": "^0.4.1", diff --git a/src/accessibility/AccessibilityManager.js b/src/accessibility/AccessibilityManager.js index df2716a..f16bcb1 100644 --- a/src/accessibility/AccessibilityManager.js +++ b/src/accessibility/AccessibilityManager.js @@ -3,7 +3,7 @@ import accessibleTarget from './accessibleTarget'; // add some extra variables to the container.. -Object.assign( +core.utils.mixins.delayMixin( core.DisplayObject.prototype, accessibleTarget ); diff --git a/src/core/Application.js b/src/core/Application.js index ea0f058..5340578 100644 --- a/src/core/Application.js +++ b/src/core/Application.js @@ -1,6 +1,6 @@ import { autoDetectRenderer } from './autoDetectRenderer'; import Container from './display/Container'; -import Ticker from './ticker/Ticker'; +import { shared, Ticker } from './ticker'; /** * Convenience class to create a new PIXI application. @@ -35,9 +35,9 @@ * @param {boolean} [noWebGL=false] - prevents selection of WebGL renderer, even if such is present * @param {boolean} [options.legacy=false] - If true Pixi will aim to ensure compatibility * with older / less advanced devices. If you experience unexplained flickering try setting this to true. - + * @param {boolean} [useSharedTicker=false] - `true` to use PIXI.ticker.shared, `false` to create new ticker. */ - constructor(width, height, options, noWebGL) + constructor(width, height, options, noWebGL, useSharedTicker = false) { /** * WebGL renderer if available, otherwise CanvasRenderer @@ -46,23 +46,46 @@ this.renderer = autoDetectRenderer(width, height, options, noWebGL); /** - * The root display container that's renderered. + * The root display container that's rendered. * @member {PIXI.Container} */ this.stage = new Container(); /** + * Internal reference to the ticker + * @member {PIXI.ticker.Ticker} + * @private + */ + this._ticker = null; + + /** * Ticker for doing render updates. * @member {PIXI.ticker.Ticker} + * @default PIXI.ticker.shared */ - this.ticker = new Ticker(); - - this.ticker.add(this.render, this); + this.ticker = useSharedTicker ? shared : new Ticker(); // Start the rendering this.start(); } + set ticker(ticker) // eslint-disable-line require-jsdoc + { + if (this._ticker) + { + this._ticker.remove(this.render, this); + } + this._ticker = ticker; + if (ticker) + { + ticker.add(this.render, this); + } + } + get ticker() // eslint-disable-line require-jsdoc + { + return this._ticker; + } + /** * Render the current stage. */ @@ -76,7 +99,7 @@ */ stop() { - this.ticker.stop(); + this._ticker.stop(); } /** @@ -84,7 +107,7 @@ */ start() { - this.ticker.start(); + this._ticker.start(); } /** @@ -98,13 +121,22 @@ } /** + * Reference to the renderer's screen rectangle. Its safe to use as filterArea or hitArea for whole screen + * @member {PIXI.Rectangle} + * @readonly + */ + get screen() + { + return this.renderer.screen; + } + + /** * Destroy and don't use after this. * @param {Boolean} [removeView=false] Automatically remove canvas from DOM. */ destroy(removeView) { this.stop(); - this.ticker.remove(this.render, this); this.ticker = null; this.stage.destroy(); diff --git a/src/core/Shader.js b/src/core/Shader.js new file mode 100644 index 0000000..78b17e6 --- /dev/null +++ b/src/core/Shader.js @@ -0,0 +1,46 @@ +import { GLShader } from 'pixi-gl-core'; +import settings from './settings'; + +function checkPrecision(src, def) +{ + if (src instanceof Array) + { + if (src[0].substring(0, 9) !== 'precision') + { + const copy = src.slice(0); + + copy.unshift(`precision ${def} float;`); + + return copy; + } + } + else if (src.substring(0, 9) !== 'precision') + { + return `precision ${def} float;\n${src}`; + } + + return src; +} + +/** + * Wrapper class, webGL Shader for Pixi. + * Adds precision string if vertexSrc or fragmentSrc have no mention of it. + * + * @class + * @extends GLShader + * @memberof PIXI + */ +export default class Shader extends GLShader +{ + /** + * + * @param {WebGLRenderingContext} gl - The current WebGL rendering context + * @param {string|string[]} vertexSrc - The vertex shader source as an array of strings. + * @param {string|string[]} fragmentSrc - The fragment shader source as an array of strings. + */ + constructor(gl, vertexSrc, fragmentSrc) + { + super(gl, checkPrecision(vertexSrc, settings.PRECISION_VERTEX), + checkPrecision(fragmentSrc, settings.PRECISION_FRAGMENT)); + } +} diff --git a/src/core/const.js b/src/core/const.js index ef6f662..3de0efe 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -188,7 +188,7 @@ * The gc modes that are supported by pixi. * * The {@link PIXI.settings.GC_MODE} Garbage Collection mode for pixi textures is AUTO - * If set to GC_MODE, the renderer will occasianally check textures usage. If they are not + * If set to GC_MODE, the renderer will occasionally check textures usage. If they are not * used for a specified period of time they will be removed from the GPU. They will of course * be uploaded again when they are required. This is a silent behind the scenes process that * should ensure that the GPU does not get filled up. diff --git a/src/core/display/Container.js b/src/core/display/Container.js index ed8cd5f..17fde8d 100644 --- a/src/core/display/Container.js +++ b/src/core/display/Container.js @@ -73,11 +73,12 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.push(child); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -108,11 +109,12 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.splice(index, 0, child); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -225,10 +227,11 @@ if (index === -1) return null; child.parent = null; + // ensure child transform will be recalculated + child.transform._parentID = -1; removeItems(this.children, index, 1); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -249,11 +252,12 @@ { const child = this.getChildAt(index); + // ensure child transform will be recalculated.. child.parent = null; + child.transform._parentID = -1; removeItems(this.children, index, 1); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -284,13 +288,12 @@ for (let i = 0; i < removed.length; ++i) { removed[i].parent = null; + if (removed[i].transform) + { + removed[i].transform._parentID = -1; + } } - // ensure a transform will be recalculated.. - if (this.transform) - { - this.transform._parentID = -1; - } this._boundsID++; this.onChildrenChange(beginIndex); diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index f37f2f5..be4c4fb 100644 --- a/src/core/graphics/Graphics.js +++ b/src/core/graphics/Graphics.js @@ -28,8 +28,9 @@ { /** * + * @param {boolean} [nativeLines=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP */ - constructor() + constructor(nativeLines = false) { super(); @@ -50,6 +51,13 @@ this.lineWidth = 0; /** + * If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * + * @member {boolean} + */ + this.nativeLines = nativeLines; + + /** * The color of any lines drawn. * * @member {string} @@ -757,28 +765,14 @@ if (!this._spriteRect) { - if (!Graphics._SPRITE_TEXTURE) - { - Graphics._SPRITE_TEXTURE = RenderTexture.create(10, 10); - - const canvas = document.createElement('canvas'); - - canvas.width = 10; - canvas.height = 10; - - const context = canvas.getContext('2d'); - - context.fillStyle = 'white'; - context.fillRect(0, 0, 10, 10); - - Graphics._SPRITE_TEXTURE = Texture.fromCanvas(canvas); - } - - this._spriteRect = new Sprite(Graphics._SPRITE_TEXTURE); + this._spriteRect = new Sprite(new Texture(Texture.WHITE)); } + + const sprite = this._spriteRect; + if (this.tint === 0xffffff) { - this._spriteRect.tint = this.graphicsData[0].fillColor; + sprite.tint = this.graphicsData[0].fillColor; } else { @@ -792,20 +786,21 @@ t1[1] *= t2[1]; t1[2] *= t2[2]; - this._spriteRect.tint = rgb2hex(t1); + sprite.tint = rgb2hex(t1); } - this._spriteRect.alpha = this.graphicsData[0].fillAlpha; - this._spriteRect.worldAlpha = this.worldAlpha * this._spriteRect.alpha; + sprite.alpha = this.graphicsData[0].fillAlpha; + sprite.worldAlpha = this.worldAlpha * sprite.alpha; + sprite.blendMode = this.blendMode; - Graphics._SPRITE_TEXTURE._frame.width = rect.width; - Graphics._SPRITE_TEXTURE._frame.height = rect.height; + sprite.texture._frame.width = rect.width; + sprite.texture._frame.height = rect.height; - this._spriteRect.transform.worldTransform = this.transform.worldTransform; + sprite.transform.worldTransform = this.transform.worldTransform; - this._spriteRect.anchor.set(-rect.x / rect.width, -rect.y / rect.height); - this._spriteRect._onAnchorUpdate(); + sprite.anchor.set(-rect.x / rect.width, -rect.y / rect.height); + sprite._onAnchorUpdate(); - this._spriteRect._renderWebGL(renderer); + sprite._renderWebGL(renderer); } /** @@ -1031,6 +1026,7 @@ this.fillColor, this.fillAlpha, this.filling, + this.nativeLines, shape ); diff --git a/src/core/graphics/GraphicsData.js b/src/core/graphics/GraphicsData.js index f5a87ae..b6f03c0 100644 --- a/src/core/graphics/GraphicsData.js +++ b/src/core/graphics/GraphicsData.js @@ -14,14 +14,19 @@ * @param {number} fillColor - the color of the fill * @param {number} fillAlpha - the alpha of the fill * @param {boolean} fill - whether or not the shape is filled with a colour + * @param {boolean} nativeLines - the method for drawing lines * @param {PIXI.Circle|PIXI.Rectangle|PIXI.Ellipse|PIXI.Polygon} shape - The shape object to draw. */ - constructor(lineWidth, lineColor, lineAlpha, fillColor, fillAlpha, fill, shape) + constructor(lineWidth, lineColor, lineAlpha, fillColor, fillAlpha, fill, nativeLines, shape) { /** * @member {number} the width of the line to draw */ this.lineWidth = lineWidth; + /** + * @member {boolean} if true the liens will be draw using LINES instead of TRIANGLE_STRIP + */ + this.nativeLines = nativeLines; /** * @member {number} the color of the line to draw @@ -85,6 +90,7 @@ this.fillColor, this.fillAlpha, this.fill, + this.nativeLines, this.shape ); } diff --git a/src/core/graphics/webgl/GraphicsRenderer.js b/src/core/graphics/webgl/GraphicsRenderer.js index 14083fe..67f5e92 100644 --- a/src/core/graphics/webgl/GraphicsRenderer.js +++ b/src/core/graphics/webgl/GraphicsRenderer.js @@ -102,7 +102,15 @@ shaderTemp.uniforms.alpha = graphics.worldAlpha; renderer.bindVao(webGLData.vao); - webGLData.vao.draw(gl.TRIANGLE_STRIP, webGLData.indices.length); + + if (graphics.nativeLines) + { + gl.drawArrays(gl.LINES, 0, webGLData.points.length / 6); + } + else + { + webGLData.vao.draw(gl.TRIANGLE_STRIP, webGLData.indices.length); + } } } diff --git a/src/core/graphics/webgl/utils/buildLine.js b/src/core/graphics/webgl/utils/buildLine.js index ecf43f7..4d6862e 100644 --- a/src/core/graphics/webgl/utils/buildLine.js +++ b/src/core/graphics/webgl/utils/buildLine.js @@ -11,7 +11,29 @@ * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties * @param {object} webGLData - an object containing all the webGL-specific information to create this shape */ -export default function buildLine(graphicsData, webGLData) +export default function (graphicsData, webGLData) +{ + if (graphicsData.nativeLines) + { + buildNativeLine(graphicsData, webGLData); + } + else + { + buildLine(graphicsData, webGLData); + } +} + +/** + * Builds a line to draw using the poligon method. + * + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @private + * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties + * @param {object} webGLData - an object containing all the webGL-specific information to create this shape + */ +function buildLine(graphicsData, webGLData) { // TODO OPTIMISE! let points = graphicsData.points; @@ -224,3 +246,46 @@ indices.push(indexStart - 1); } + +/** + * Builds a line to draw using the gl.drawArrays(gl.LINES) method + * + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @private + * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties + * @param {object} webGLData - an object containing all the webGL-specific information to create this shape + */ +function buildNativeLine(graphicsData, webGLData) +{ + let i = 0; + const points = graphicsData.points; + + if (points.length === 0) return; + + const verts = webGLData.points; + const length = points.length / 2; + + // sort color + const color = hex2rgb(graphicsData.lineColor); + const alpha = graphicsData.lineAlpha; + const r = color[0] * alpha; + const g = color[1] * alpha; + const b = color[2] * alpha; + + for (i = 1; i < length; i++) + { + const p1x = points[(i - 1) * 2]; + const p1y = points[((i - 1) * 2) + 1]; + + const p2x = points[i * 2]; + const p2y = points[(i * 2) + 1]; + + verts.push(p1x, p1y); + verts.push(r, g, b, alpha); + + verts.push(p2x, p2y); + verts.push(r, g, b, alpha); + } +} diff --git a/src/core/graphics/webgl/utils/buildRoundedRectangle.js b/src/core/graphics/webgl/utils/buildRoundedRectangle.js index 9abc807..a6c8461 100644 --- a/src/core/graphics/webgl/utils/buildRoundedRectangle.js +++ b/src/core/graphics/webgl/utils/buildRoundedRectangle.js @@ -77,6 +77,26 @@ } /** + * Calculate a single point for a quadratic bezier curve. + * Utility function used by quadraticBezierCurve. + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @private + * @param {number} n1 - first number + * @param {number} n2 - second number + * @param {number} perc - percentage + * @return {number} the result + * + */ +function getPt(n1, n2, perc) +{ + const diff = n2 - n1; + + return n1 + (diff * perc); +} + +/** * Calculate the points for a quadratic bezier curve. (helper function..) * Based on: https://stackoverflow.com/questions/785097/how-do-i-implement-a-bezier-curve-in-c * @@ -105,13 +125,6 @@ let x = 0; let y = 0; - function getPt(n1, n2, perc) - { - const diff = n2 - n1; - - return n1 + (diff * perc); - } - for (let i = 0, j = 0; i <= n; ++i) { j = i / n; diff --git a/src/core/index.js b/src/core/index.js index f7a817c..4f414e8 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -30,6 +30,7 @@ export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; export { default as CanvasGraphicsRenderer } from './graphics/canvas/CanvasGraphicsRenderer'; +export { default as Spritesheet } from './textures/Spritesheet'; export { default as Texture } from './textures/Texture'; export { default as BaseTexture } from './textures/BaseTexture'; export { default as RenderTexture } from './textures/RenderTexture'; diff --git a/src/core/math/Matrix.js b/src/core/math/Matrix.js index 0b77477..3679860 100644 --- a/src/core/math/Matrix.js +++ b/src/core/math/Matrix.js @@ -13,45 +13,50 @@ export default class Matrix { /** - * + * @param {number} [a=1] - x scale + * @param {number} [b=0] - y skew + * @param {number} [c=0] - x skew + * @param {number} [d=1] - y scale + * @param {number} [tx=0] - x translation + * @param {number} [ty=0] - y translation */ - constructor() + constructor(a = 1, b = 0, c = 0, d = 1, tx = 0, ty = 0) { /** * @member {number} * @default 1 */ - this.a = 1; + this.a = a; /** * @member {number} * @default 0 */ - this.b = 0; + this.b = b; /** * @member {number} * @default 0 */ - this.c = 0; + this.c = c; /** * @member {number} * @default 1 */ - this.d = 1; + this.d = d; /** * @member {number} * @default 0 */ - this.tx = 0; + this.tx = tx; /** * @member {number} * @default 0 */ - this.ty = 0; + this.ty = ty; this.array = null; } diff --git a/src/core/renderers/SystemRenderer.js b/src/core/renderers/SystemRenderer.js index fa116a6..a24c775 100644 --- a/src/core/renderers/SystemRenderer.js +++ b/src/core/renderers/SystemRenderer.js @@ -1,5 +1,5 @@ import { sayHello, hex2string, hex2rgb } from '../utils'; -import { Matrix } from '../math'; +import { Matrix, Rectangle } from '../math'; import { RENDERER_TYPE } from '../const'; import settings from '../settings'; import Container from '../display/Container'; @@ -21,8 +21,8 @@ { /** * @param {string} system - The name of the system this renderer is for. - * @param {number} [width=800] - the width of the canvas view - * @param {number} [height=600] - the height of the canvas view + * @param {number} [screenWidth=800] - the width of the screen + * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false @@ -37,7 +37,7 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(system, width, height, options) + constructor(system, screenWidth, screenHeight, options) { super(); @@ -69,20 +69,13 @@ this.type = RENDERER_TYPE.UNKNOWN; /** - * The width of the canvas view + * Measurements of the screen. (0, 0, screenWidth, screenHeight) * - * @member {number} - * @default 800 - */ - this.width = width || 800; - - /** - * The height of the canvas view + * Its safe to use as filterArea or hitArea for whole stage * - * @member {number} - * @default 600 + * @member {PIXI.Rectangle} */ - this.height = height || 600; + this.screen = new Rectangle(0, 0, screenWidth || 800, screenHeight || 600); /** * The canvas element that everything is drawn to @@ -107,7 +100,7 @@ this.transparent = options.transparent; /** - * Whether the render view should be resized automatically + * Whether css dimensions of canvas view should be resized to screen dimensions automatically * * @member {boolean} */ @@ -192,23 +185,48 @@ } /** - * Resizes the canvas view to the specified width and height + * Same as view.width, actual number of pixels in the canvas by horizontal * - * @param {number} width - the new width of the canvas view - * @param {number} height - the new height of the canvas view + * @member {number} + * @readonly + * @default 800 */ - resize(width, height) + get width() { - this.width = width * this.resolution; - this.height = height * this.resolution; + return this.view.width; + } - this.view.width = this.width; - this.view.height = this.height; + /** + * Same as view.height, actual number of pixels in the canvas by vertical + * + * @member {number} + * @readonly + * @default 600 + */ + get height() + { + return this.view.height; + } + + /** + * Resizes the screen and canvas to the specified width and height + * Canvas dimensions are multiplied by resolution + * + * @param {number} screenWidth - the new width of the screen + * @param {number} screenHeight - the new height of the screen + */ + resize(screenWidth, screenHeight) + { + this.screen.width = screenWidth; + this.screen.height = screenHeight; + + this.view.width = screenWidth * this.resolution; + this.view.height = screenHeight * this.resolution; if (this.autoResize) { - this.view.style.width = `${this.width / this.resolution}px`; - this.view.style.height = `${this.height / this.resolution}px`; + this.view.style.width = `${screenWidth}px`; + this.view.style.height = `${screenHeight}px`; } } @@ -249,11 +267,10 @@ this.type = RENDERER_TYPE.UNKNOWN; - this.width = 0; - this.height = 0; - this.view = null; + this.screen = null; + this.resolution = 0; this.transparent = false; diff --git a/src/core/renderers/canvas/CanvasRenderer.js b/src/core/renderers/canvas/CanvasRenderer.js index 1bd2d5e..e84e49c 100644 --- a/src/core/renderers/canvas/CanvasRenderer.js +++ b/src/core/renderers/canvas/CanvasRenderer.js @@ -18,8 +18,8 @@ export default class CanvasRenderer extends SystemRenderer { /** - * @param {number} [width=800] - the width of the canvas view - * @param {number} [height=600] - the height of the canvas view + * @param {number} [screenWidth=800] - the width of the screen + * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false @@ -34,9 +34,9 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(width, height, options = {}) + constructor(screenWidth, screenHeight, options = {}) { - super('Canvas', width, height, options); + super('Canvas', screenWidth, screenHeight, options); this.type = RENDERER_TYPE.CANVAS; @@ -96,7 +96,7 @@ this.context = null; this.renderingToScreen = false; - this.resize(width, height); + this.resize(screenWidth, screenHeight); } /** @@ -283,12 +283,12 @@ * * @extends PIXI.SystemRenderer#resize * - * @param {number} width - The new width of the canvas view - * @param {number} height - The new height of the canvas view + * @param {number} screenWidth - the new width of the screen + * @param {number} screenHeight - the new height of the screen */ - resize(width, height) + resize(screenWidth, screenHeight) { - super.resize(width, height); + super.resize(screenWidth, screenHeight); // reset the scale mode.. oddly this seems to be reset when the canvas is resized. // surely a browser bug?? Let pixi fix that for you.. diff --git a/src/core/renderers/webgl/TextureManager.js b/src/core/renderers/webgl/TextureManager.js index b685c1a..8227289 100644 --- a/src/core/renderers/webgl/TextureManager.js +++ b/src/core/renderers/webgl/TextureManager.js @@ -61,7 +61,7 @@ * Updates and/or Creates a WebGL texture for the renderer's context. * * @param {PIXI.BaseTexture|PIXI.Texture} texture - the texture to update - * @param {Number} location - the location the texture will be bound to. + * @param {number} location - the location the texture will be bound to. * @return {GLTexture} The gl texture. */ updateTexture(texture, location) diff --git a/src/core/renderers/webgl/WebGLRenderer.js b/src/core/renderers/webgl/WebGLRenderer.js index e4cd8a0..830b382 100644 --- a/src/core/renderers/webgl/WebGLRenderer.js +++ b/src/core/renderers/webgl/WebGLRenderer.js @@ -31,8 +31,8 @@ { /** * - * @param {number} [width=0] - the width of the canvas view - * @param {number} [height=0] - the height of the canvas view + * @param {number} [screenWidth=800] - the width of the screen + * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false @@ -53,9 +53,9 @@ * @param {boolean} [options.legacy=false] - If true Pixi will aim to ensure compatibility * with older / less advanced devices. If you experiance unexplained flickering try setting this to true. */ - constructor(width, height, options = {}) + constructor(screenWidth, screenHeight, options = {}) { - super('WebGL', width, height, options); + super('WebGL', screenWidth, screenHeight, options); this.legacy = !!options.legacy; @@ -245,7 +245,7 @@ glCore._testingContext = gl; // setup the width/height properties and gl viewport - this.resize(this.width, this.height); + this.resize(this.screen.width, this.screen.height); } /** @@ -343,16 +343,16 @@ /** * Resizes the webGL view to the specified width and height. * - * @param {number} width - the new width of the webGL view - * @param {number} height - the new height of the webGL view + * @param {number} screenWidth - the new width of the screen + * @param {number} screenHeight - the new height of the screen */ - resize(width, height) + resize(screenWidth, screenHeight) { // if(width * this.resolution === this.width && height * this.resolution === this.height)return; - SystemRenderer.prototype.resize.call(this, width, height); + SystemRenderer.prototype.resize.call(this, screenWidth, screenHeight); - this.rootRenderTarget.resize(width, height); + this.rootRenderTarget.resize(screenWidth, screenHeight); if (this._activeRenderTarget === this.rootRenderTarget) { @@ -396,6 +396,26 @@ } /** + * Erases the render texture and fills the drawing area with a colour + * + * @param {PIXI.RenderTexture} renderTexture - The render texture to clear + * @param {number} [clearColor] - The colour + * @return {PIXI.WebGLRenderer} Returns itself. + */ + clearRenderTexture(renderTexture, clearColor) + { + const baseTexture = renderTexture.baseTexture; + const renderTarget = baseTexture._glRenderTargets[this.CONTEXT_UID]; + + if (renderTarget) + { + renderTarget.clear(clearColor); + } + + return this; + } + + /** * Binds a render texture for rendering * * @param {PIXI.RenderTexture} renderTexture - The render texture to render @@ -474,9 +494,10 @@ * Changes the current GLShader to the one given in parameter * * @param {PIXI.glCore.Shader} shader - the new glShader + * @param {boolean} [autoProject=true] - Whether automatically set the projection matrix * @return {PIXI.WebGLRenderer} Returns itself. */ - _bindGLShader(shader) + _bindGLShader(shader, autoProject) { // TODO cache if (this._activeShader !== shader) @@ -484,8 +505,14 @@ this._activeShader = shader; shader.bind(); - // automatically set the projection matrix - shader.uniforms.projectionMatrix = this._activeRenderTarget.projectionMatrix.toArray(true); + // `autoProject` normally would be a default parameter set to true + // but because of how Babel transpiles default parameters + // it hinders the performance of this method. + if (autoProject !== false) + { + // automatically set the projection matrix + shader.uniforms.projectionMatrix = this._activeRenderTarget.projectionMatrix.toArray(true); + } } return this; diff --git a/src/core/renderers/webgl/WebGLState.js b/src/core/renderers/webgl/WebGLState.js index ba6ecc5..6c846d5 100755 --- a/src/core/renderers/webgl/WebGLState.js +++ b/src/core/renderers/webgl/WebGLState.js @@ -82,18 +82,20 @@ push() { // next state.. - let state = this.stack[++this.stackIndex]; + let state = this.stack[this.stackIndex]; if (!state) { state = this.stack[this.stackIndex] = new Uint8Array(16); } + ++this.stackIndex; + // copy state.. // set active state so we can force overrides of gl state for (let i = 0; i < this.activeState.length; i++) { - this.activeState[i] = state[i]; + state[i] = this.activeState[i]; } } diff --git a/src/core/renderers/webgl/filters/Filter.js b/src/core/renderers/webgl/filters/Filter.js index 2c3eaad..4eb6128 100644 --- a/src/core/renderers/webgl/filters/Filter.js +++ b/src/core/renderers/webgl/filters/Filter.js @@ -9,7 +9,7 @@ * @memberof PIXI * @extends PIXI.Shader */ -class Filter extends Shader +export default class Filter extends Shader { /** * @param {string} [vertexSrc] - The source of the vertex shader. @@ -56,15 +56,16 @@ * @param {PIXI.RenderTarget} input - The input render target. * @param {PIXI.RenderTarget} output - The target to output to. * @param {boolean} clear - Should the output be cleared before rendering to it + * @param {object} [currentState] - It's current state of filter. + * There are some useful properties in the currentState : + * target, filters, sourceFrame, destinationFrame, renderTarget, resolution */ - apply(filterManager, input, output, clear) + apply(filterManager, input, output, clear, currentState) // eslint-disable-line no-unused-vars { // do as you please! - filterManager.applyFilter(this, input, output, clear); + filterManager.applyFilter(this, input, output, clear, currentState); // or just do a regular render.. } } - -export default Filter; diff --git a/src/core/renderers/webgl/filters/extractUniformsFromSrc.js b/src/core/renderers/webgl/filters/extractUniformsFromSrc.js index d78a0b1..4893214 100644 --- a/src/core/renderers/webgl/filters/extractUniformsFromSrc.js +++ b/src/core/renderers/webgl/filters/extractUniformsFromSrc.js @@ -12,7 +12,7 @@ function extractUniformsFromString(string) { - const maskRegex = new RegExp('^(projectionMatrix|uSampler|filterArea)$'); + const maskRegex = new RegExp('^(projectionMatrix|uSampler|filterArea|filterClamp)$'); const uniforms = {}; let nameSplit; diff --git a/src/core/renderers/webgl/managers/FilterManager.js b/src/core/renderers/webgl/managers/FilterManager.js index 43fc869..6c7422d 100644 --- a/src/core/renderers/webgl/managers/FilterManager.js +++ b/src/core/renderers/webgl/managers/FilterManager.js @@ -2,6 +2,7 @@ import RenderTarget from '../utils/RenderTarget'; import Quad from '../utils/Quad'; import { Rectangle } from '../../../math'; +import Shader from '../../../Shader'; import * as filterTransforms from '../filters/filterTransforms'; import bitTwiddle from 'bit-twiddle'; @@ -22,8 +23,6 @@ this.filters = []; this.target = null; this.resolution = 1; - - this.firstRun = true; } } @@ -155,7 +154,7 @@ if (filters.length === 1) { - filters[0].apply(this, currentState.renderTarget, lastState.renderTarget, false); + filters[0].apply(this, currentState.renderTarget, lastState.renderTarget, false, currentState); this.freePotRenderTarget(currentState.renderTarget); } else @@ -177,7 +176,7 @@ for (i = 0; i < filters.length - 1; ++i) { - filters[i].apply(this, flip, flop, true); + filters[i].apply(this, flip, flop, true, currentState); const t = flip; @@ -185,7 +184,7 @@ flop = t; } - filters[i].apply(this, flip, lastState.renderTarget, false); + filters[i].apply(this, flip, lastState.renderTarget, false, currentState); this.freePotRenderTarget(flip); this.freePotRenderTarget(flop); @@ -212,16 +211,28 @@ const renderer = this.renderer; const gl = renderer.gl; - renderer.bindShader(filter, true); + let shader = filter.glShaders[renderer.CONTEXT_UID]; - // TODO theres a better way! - // quad can be a mesh and we then should be able to pull this off without accessing the shader directly - const shader = renderer.shaderManager.getGLShader(); - - // TODO change quad to mesh.. - if (!this.firstRun) + // cacheing.. + if (!shader) { - this.firstRun = true; + if (filter.glShaderKey) + { + shader = this.shaderCache[filter.glShaderKey]; + + if (!shader) + { + shader = new Shader(this.gl, filter.vertexSrc, filter.fragmentSrc); + + filter.glShaders[renderer.CONTEXT_UID] = this.shaderCache[filter.glShaderKey] = shader; + } + } + else + { + shader = filter.glShaders[renderer.CONTEXT_UID] = new Shader(this.gl, filter.vertexSrc, filter.fragmentSrc); + } + + // TODO - this only needs to be done once? renderer.bindVao(null); this.quad.initVao(shader); @@ -244,58 +255,146 @@ renderer.maskManager.pushScissorMask(null, renderer.maskManager.scissorData); } - // renderer._bindGLShader(shader); + renderer.bindShader(shader); - let currentState; - const uniforms = filter.uniforms; + // free unit 0 for us, doesn't matter what was there + // don't try to restore it, because syncUniforms can upload it to another slot + // and it'll be a problem + const tex = this.renderer.emptyTextures[0]; - if (uniforms.filterArea) - { - currentState = this.filterData.stack[this.filterData.index]; - - const filterArea = uniforms.filterArea; - - filterArea[0] = currentState.renderTarget.size.width; - filterArea[1] = currentState.renderTarget.size.height; - filterArea[2] = currentState.sourceFrame.x; - filterArea[3] = currentState.sourceFrame.y; - - filter.uniforms.filterArea = filterArea; - } - - // use this to clamp displaced texture coords so they belong to filterArea - // see displacementFilter fragment shader for an example - if (uniforms.filterClamp) - { - currentState = this.filterData.stack[this.filterData.index]; - - const filterClamp = uniforms.filterClamp; - - filterClamp[0] = 0; - filterClamp[1] = 0; - filterClamp[2] = (currentState.sourceFrame.width - 1) / currentState.renderTarget.size.width; - filterClamp[3] = (currentState.sourceFrame.height - 1) / currentState.renderTarget.size.height; - - filter.uniforms.filterClamp = filterClamp; - } - - renderer.shaderManager.setUniforms(filter.uniforms); + this.renderer.boundTextures[0] = tex; + // this syncs the pixi filters uniforms with glsl uniforms + this.syncUniforms(shader, filter); renderer.state.setBlendMode(filter.blendMode); - // temporary bypass cache.. - const tex = this.renderer.boundTextures[0]; - gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, input.texture.texture); this.quad.vao.draw(this.renderer.gl.TRIANGLES, 6, 0); - // restore cache. gl.bindTexture(gl.TEXTURE_2D, tex._glTextures[this.renderer.CONTEXT_UID].texture); } /** + * Uploads the uniforms of the filter. + * + * @param {GLShader} shader - The underlying gl shader. + * @param {PIXI.Filter} filter - The filter we are synchronizing. + */ + syncUniforms(shader, filter) + { + const uniformData = filter.uniformData; + const uniforms = filter.uniforms; + + // 0 is reserved for the pixi texture so we start at 1! + let textureCount = 1; + let currentState; + + // filterArea and filterClamp that are handled by FilterManager directly + // they must not appear in uniformData + + if (shader.uniforms.filterArea) + { + currentState = this.filterData.stack[this.filterData.index]; + + const filterArea = shader.uniforms.filterArea; + + filterArea[0] = currentState.renderTarget.size.width; + filterArea[1] = currentState.renderTarget.size.height; + filterArea[2] = currentState.sourceFrame.x; + filterArea[3] = currentState.sourceFrame.y; + + shader.uniforms.filterArea = filterArea; + } + + // use this to clamp displaced texture coords so they belong to filterArea + // see displacementFilter fragment shader for an example + if (shader.uniforms.filterClamp) + { + currentState = currentState || this.filterData.stack[this.filterData.index]; + + const filterClamp = shader.uniforms.filterClamp; + + filterClamp[0] = 0; + filterClamp[1] = 0; + filterClamp[2] = (currentState.sourceFrame.width - 1) / currentState.renderTarget.size.width; + filterClamp[3] = (currentState.sourceFrame.height - 1) / currentState.renderTarget.size.height; + + shader.uniforms.filterClamp = filterClamp; + } + + // TODO Cacheing layer.. + for (const i in uniformData) + { + if (uniformData[i].type === 'sampler2D' && uniforms[i] !== 0) + { + if (uniforms[i].baseTexture) + { + shader.uniforms[i] = this.renderer.bindTexture(uniforms[i].baseTexture, textureCount); + } + else + { + shader.uniforms[i] = textureCount; + + // TODO + // this is helpful as renderTargets can also be set. + // Although thinking about it, we could probably + // make the filter texture cache return a RenderTexture + // rather than a renderTarget + const gl = this.renderer.gl; + + this.renderer.boundTextures[textureCount] = this.renderer.emptyTextures[textureCount]; + gl.activeTexture(gl.TEXTURE0 + textureCount); + + uniforms[i].texture.bind(); + } + + textureCount++; + } + else if (uniformData[i].type === 'mat3') + { + // check if its pixi matrix.. + if (uniforms[i].a !== undefined) + { + shader.uniforms[i] = uniforms[i].toArray(true); + } + else + { + shader.uniforms[i] = uniforms[i]; + } + } + else if (uniformData[i].type === 'vec2') + { + // check if its a point.. + if (uniforms[i].x !== undefined) + { + const val = shader.uniforms[i] || new Float32Array(2); + + val[0] = uniforms[i].x; + val[1] = uniforms[i].y; + shader.uniforms[i] = val; + } + else + { + shader.uniforms[i] = uniforms[i]; + } + } + else if (uniformData[i].type === 'float') + { + if (shader.uniforms.data[i].value !== uniformData[i]) + { + shader.uniforms[i] = uniforms[i]; + } + } + else + { + shader.uniforms[i] = uniforms[i]; + } + } + } + + /** * Gets a render target from the pool, or creates a new one. * * @param {boolean} clear - Should we clear the render texture when we get it? @@ -442,7 +541,6 @@ renderTarget.resolution = resolution; renderTarget.defaultFrame.width = renderTarget.size.width = minWidth / resolution; renderTarget.defaultFrame.height = renderTarget.size.height = minHeight / resolution; - renderTarget.attachStencilBuffer(); return renderTarget; } diff --git a/src/core/settings.js b/src/core/settings.js index bb08cb0..f4dd087 100644 --- a/src/core/settings.js +++ b/src/core/settings.js @@ -77,11 +77,11 @@ * * @static * @memberof PIXI.settings - * @type {RegExp|string} + * @type {RegExp} * @example `@2x` - * @default /@(.+)x/ + * @default /@([0-9\.]+)x/ */ - RETINA_PREFIX: /@(.+)x/, + RETINA_PREFIX: /@([0-9\.]+)x/, /** * The default render options if none are supplied to {@link PIXI.WebGLRenderer} @@ -175,14 +175,24 @@ SCALE_MODE: 0, /** - * Default specify float precision in shaders. + * Default specify float precision in vertex shader. + * + * @static + * @memberof PIXI.settings + * @type {PIXI.PRECISION} + * @default PIXI.PRECISION.HIGH + */ + PRECISION_VERTEX: 'highp', + + /** + * Default specify float precision in fragment shader. * * @static * @memberof PIXI.settings * @type {PIXI.PRECISION} * @default PIXI.PRECISION.MEDIUM */ - PRECISION: 'mediump', + PRECISION_FRAGMENT: 'mediump', /** * Can we upload the same buffer in a single frame? diff --git a/src/core/sprites/Sprite.js b/src/core/sprites/Sprite.js index 28b0b18..121bf58 100644 --- a/src/core/sprites/Sprite.js +++ b/src/core/sprites/Sprite.js @@ -212,11 +212,11 @@ } else { - w0 = orig.width * (1 - anchor._x); - w1 = orig.width * -anchor._x; + w1 = -anchor._x * orig.width; + w0 = w1 + orig.width; - h0 = orig.height * (1 - anchor._y); - h1 = orig.height * -anchor._y; + h1 = -anchor._y * orig.height; + h0 = h1 + orig.height; } // xy @@ -269,11 +269,11 @@ const tx = wt.tx; const ty = wt.ty; - const w0 = (orig.width) * (1 - anchor._x); - const w1 = (orig.width) * -anchor._x; + const w1 = -anchor._x * orig.width; + const w0 = w1 + orig.width; - const h0 = orig.height * (1 - anchor._y); - const h1 = orig.height * -anchor._y; + const h1 = -anchor._y * orig.height; + const h0 = h1 + orig.height; // xy vertexData[0] = (a * w1) + (c * h1) + tx; @@ -440,7 +440,7 @@ * * @static * @param {number|string|PIXI.BaseTexture|HTMLCanvasElement|HTMLVideoElement} source Source to create texture from - * @return {PIXI.Sprite} The newly created texture + * @return {PIXI.Sprite} The newly created sprite */ static from(source) { @@ -538,8 +538,8 @@ } /** - * The tint applied to the sprite. This is a hex value. A value of - * 0xFFFFFF will remove any tint effect. + * The tint applied to the sprite. This is a hex value. + * A value of 0xFFFFFF will remove any tint effect. * * @member {number} * @default 0xFFFFFF diff --git a/src/core/sprites/canvas/CanvasTinter.js b/src/core/sprites/canvas/CanvasTinter.js index 1cf316b..27e5e0c 100644 --- a/src/core/sprites/canvas/CanvasTinter.js +++ b/src/core/sprites/canvas/CanvasTinter.js @@ -75,8 +75,8 @@ crop.width *= resolution; crop.height *= resolution; - canvas.width = crop.width; - canvas.height = crop.height; + canvas.width = Math.ceil(crop.width); + canvas.height = Math.ceil(crop.height); context.fillStyle = `#${(`00000${(color | 0).toString(16)}`).substr(-6)}`; @@ -130,8 +130,8 @@ crop.width *= resolution; crop.height *= resolution; - canvas.width = crop.width; - canvas.height = crop.height; + canvas.width = Math.ceil(crop.width); + canvas.height = Math.ceil(crop.height); context.globalCompositeOperation = 'copy'; context.fillStyle = `#${(`00000${(color | 0).toString(16)}`).substr(-6)}`; @@ -172,8 +172,8 @@ crop.width *= resolution; crop.height *= resolution; - canvas.width = crop.width; - canvas.height = crop.height; + canvas.width = Math.ceil(crop.width); + canvas.height = Math.ceil(crop.height); context.globalCompositeOperation = 'copy'; context.drawImage( diff --git a/src/core/sprites/webgl/SpriteRenderer.js b/src/core/sprites/webgl/SpriteRenderer.js index 2affcd5..7c352e2 100644 --- a/src/core/sprites/webgl/SpriteRenderer.js +++ b/src/core/sprites/webgl/SpriteRenderer.js @@ -30,7 +30,7 @@ /** * Number of values sent in the vertex buffer. - * positionX, positionY, colorR, colorG, colorB = 5 + * aVertexPosition(2), aTextureCoord(1), aColor(1), aTextureId(1) = 5 * * @member {number} */ @@ -75,7 +75,6 @@ this.shader = null; this.currentIndex = 0; - TICK = 0; this.groups = []; for (let k = 0; k < this.size; k++) diff --git a/src/core/textures/BaseTexture.js b/src/core/textures/BaseTexture.js index 853de7e..aeccf39 100644 --- a/src/core/textures/BaseTexture.js +++ b/src/core/textures/BaseTexture.js @@ -232,16 +232,24 @@ this.realWidth = this.source.naturalWidth || this.source.videoWidth || this.source.width; this.realHeight = this.source.naturalHeight || this.source.videoHeight || this.source.height; - this.width = this.realWidth / this.resolution; - this.height = this.realHeight / this.resolution; - - this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); + this._updateDimensions(); } this.emit('update', this); } /** + * Update dimensions from real values + */ + _updateDimensions() + { + this.width = this.realWidth / this.resolution; + this.height = this.realHeight / this.resolution; + + this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); + } + + /** * Load a source. * * If the source is not-immediately-available, such as an image that needs to be @@ -525,11 +533,7 @@ this.realWidth = Math.round(svgWidth * this.sourceScale); this.realHeight = Math.round(svgHeight * this.sourceScale); - this.width = this.realWidth / this.resolution; - this.height = this.realHeight / this.resolution; - - // Check pow2 after scale - this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); + this._updateDimensions(); // Create a canvas element const canvas = document.createElement('canvas'); @@ -690,4 +694,52 @@ return baseTexture; } + + /** + * Helper function that creates a base texture based on the source you provide. + * The source can be - image url, image element, canvas element. + * + * @static + * @param {string|HTMLImageElement|HTMLCanvasElement} source - The source to create base texture from. + * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values + * @param {number} [sourceScale=(auto)] - Scale for the original image, used with Svg images. + * @return {PIXI.BaseTexture} The new base texture. + */ + static from(source, scaleMode, sourceScale) + { + if (typeof source === 'string') + { + return BaseTexture.fromImage(source, undefined, scaleMode, sourceScale); + } + else if (source instanceof HTMLImageElement) + { + const imageUrl = source.src; + let baseTexture = BaseTextureCache[imageUrl]; + + if (!baseTexture) + { + baseTexture = new BaseTexture(source, scaleMode); + baseTexture.imageUrl = imageUrl; + + if (sourceScale) + { + baseTexture.sourceScale = sourceScale; + } + + // if there is an @2x at the end of the url we are going to assume its a highres image + baseTexture.resolution = getResolutionOfUrl(imageUrl); + + BaseTextureCache[imageUrl] = baseTexture; + } + + return baseTexture; + } + else if (source instanceof HTMLCanvasElement) + { + return BaseTexture.fromCanvas(source, scaleMode); + } + + // lets assume its a base texture! + return source; + } } diff --git a/src/core/textures/Spritesheet.js b/src/core/textures/Spritesheet.js new file mode 100644 index 0000000..d469c62 --- /dev/null +++ b/src/core/textures/Spritesheet.js @@ -0,0 +1,274 @@ +import { Rectangle, Texture } from '../'; +import { getResolutionOfUrl, TextureCache } from '../utils'; + +/** + * Utility class for maintaining reference to a collection + * of Textures on a single Spritesheet. + * + * @class + * @memberof PIXI + */ +export default class Spritesheet +{ + /** + * The maximum number of Textures to build per process. + * + * @type {number} + * @default 1000 + */ + static get BATCH_SIZE() + { + return 1000; + } + + /** + * @param {PIXI.BaseTexture} baseTexture Reference to the source BaseTexture object. + * @param {Object} data - Spritesheet image data. + * @param {string} [resolutionFilename] - The filename to consider when determining + * the resolution of the spritesheet. If not provided, the imageUrl will + * be used on the BaseTexture. + */ + constructor(baseTexture, data, resolutionFilename = null) + { + /** + * Reference to ths source texture + * @type {PIXI.BaseTexture} + */ + this.baseTexture = baseTexture; + + /** + * Map of spritesheet textures. + * @type {Object} + */ + this.textures = {}; + + /** + * Reference to the original JSON data. + * @type {Object} + */ + this.data = data; + + /** + * The resolution of the spritesheet. + * @type {number} + */ + this.resolution = this._updateResolution( + resolutionFilename || this.baseTexture.imageUrl + ); + + /** + * Map of spritesheet frames. + * @type {Object} + * @private + */ + this._frames = this.data.frames; + + /** + * Collection of frame names. + * @type {string[]} + * @private + */ + this._frameKeys = Object.keys(this._frames); + + /** + * Current batch index being processed. + * @type {number} + * @private + */ + this._batchIndex = 0; + + /** + * Callback when parse is completed. + * @type {Function} + * @private + */ + this._callback = null; + } + + /** + * Generate the resolution from the filename or fallback + * to the meta.scale field of the JSON data. + * + * @private + * @param {string} resolutionFilename - The filename to use for resolving + * the default resolution. + * @return {number} Resolution to use for spritesheet. + */ + _updateResolution(resolutionFilename) + { + const scale = this.data.meta.scale; + + // Use a defaultValue of `null` to check if a url-based resolution is set + let resolution = getResolutionOfUrl(resolutionFilename, null); + + // No resolution found via URL + if (resolution === null) + { + // Use the scale value or default to 1 + resolution = scale !== undefined ? parseFloat(scale) : 1; + } + + // For non-1 resolutions, update baseTexture + if (resolution !== 1) + { + this.baseTexture.resolution = resolution; + this.baseTexture.update(); + } + + return resolution; + } + + /** + * Parser spritesheet from loaded data. This is done asynchronously + * to prevent creating too many Texture within a single process. + * + * @param {Function} callback - Callback when complete returns + * a map of the Textures for this spritesheet. + */ + parse(callback) + { + this._batchIndex = 0; + this._callback = callback; + + if (this._frameKeys.length <= Spritesheet.BATCH_SIZE) + { + this._processFrames(0); + this._parseComplete(); + } + else + { + this._nextBatch(); + } + } + + /** + * Process a batch of frames + * + * @private + * @param {number} initialFrameIndex - The index of frame to start. + */ + _processFrames(initialFrameIndex) + { + let frameIndex = initialFrameIndex; + const maxFrames = Spritesheet.BATCH_SIZE; + + while (frameIndex - initialFrameIndex < maxFrames && frameIndex < this._frameKeys.length) + { + const i = this._frameKeys[frameIndex]; + const rect = this._frames[i].frame; + + if (rect) + { + let frame = null; + let trim = null; + const orig = new Rectangle( + 0, + 0, + this._frames[i].sourceSize.w / this.resolution, + this._frames[i].sourceSize.h / this.resolution + ); + + if (this._frames[i].rotated) + { + frame = new Rectangle( + rect.x / this.resolution, + rect.y / this.resolution, + rect.h / this.resolution, + rect.w / this.resolution + ); + } + else + { + frame = new Rectangle( + rect.x / this.resolution, + rect.y / this.resolution, + rect.w / this.resolution, + rect.h / this.resolution + ); + } + + // Check to see if the sprite is trimmed + if (this._frames[i].trimmed) + { + trim = new Rectangle( + this._frames[i].spriteSourceSize.x / this.resolution, + this._frames[i].spriteSourceSize.y / this.resolution, + rect.w / this.resolution, + rect.h / this.resolution + ); + } + + this.textures[i] = new Texture( + this.baseTexture, + frame, + orig, + trim, + this._frames[i].rotated ? 2 : 0 + ); + + // lets also add the frame to pixi's global cache for fromFrame and fromImage functions + TextureCache[i] = this.textures[i]; + } + + frameIndex++; + } + } + + /** + * The parse has completed. + * + * @private + */ + _parseComplete() + { + const callback = this._callback; + + this._callback = null; + this._batchIndex = 0; + callback.call(this, this.textures); + } + + /** + * Begin the next batch of textures. + * + * @private + */ + _nextBatch() + { + this._processFrames(this._batchIndex * Spritesheet.BATCH_SIZE); + this._batchIndex++; + setTimeout(() => + { + if (this._batchIndex * Spritesheet.BATCH_SIZE < this._frameKeys.length) + { + this._nextBatch(); + } + else + { + this._parseComplete(); + } + }, 0); + } + + /** + * Destroy Spritesheet and don't use after this. + * + * @param {boolean} [destroyBase=false] Whether to destroy the base texture as well + */ + destroy(destroyBase = false) + { + for (const i in this.textures) + { + this.textures[i].destroy(); + } + this._frames = null; + this._frameKeys = null; + this.data = null; + this.textures = null; + if (destroyBase) + { + this.baseTexture.destroy(); + } + this.baseTexture = null; + } +} diff --git a/src/core/textures/Texture.js b/src/core/textures/Texture.js index 6403d4f..afe4254 100644 --- a/src/core/textures/Texture.js +++ b/src/core/textures/Texture.js @@ -3,7 +3,7 @@ import TextureUvs from './TextureUvs'; import EventEmitter from 'eventemitter3'; import { Rectangle } from '../math'; -import { TextureCache, BaseTextureCache } from '../utils'; +import { TextureCache, BaseTextureCache, getResolutionOfUrl } from '../utils'; /** * A texture stores the information that represents an image or part of an image. It cannot be added @@ -365,7 +365,8 @@ * The source can be - frame id, image url, video url, canvas element, video element, base texture * * @static - * @param {number|string|PIXI.BaseTexture|HTMLCanvasElement|HTMLVideoElement} source - Source to create texture from + * @param {number|string|HTMLImageElement|HTMLCanvasElement|HTMLVideoElement|PIXI.BaseTexture} + * source - Source to create texture from * @return {PIXI.Texture} The newly created texture */ static from(source) @@ -393,7 +394,7 @@ } else if (source instanceof HTMLImageElement) { - return new Texture(new BaseTexture(source)); + return new Texture(BaseTexture.from(source)); } else if (source instanceof HTMLCanvasElement) { @@ -413,6 +414,43 @@ } /** + * Create a texture from a source and add to the cache. + * + * @static + * @param {HTMLImageElement|HTMLCanvasElement} source - The input source. + * @param {String} imageUrl - File name of texture, for cache and resolving resolution. + * @param {String} [name] - Human readible name for the texture cache. If no name is + * specified, only `imageUrl` will be used as the cache ID. + * @return {PIXI.Texture} Output texture + */ + static fromLoader(source, imageUrl, name) + { + const baseTexture = new BaseTexture(source, null, getResolutionOfUrl(imageUrl)); + const texture = new Texture(baseTexture); + + baseTexture.imageUrl = imageUrl; + + // No name, use imageUrl instead + if (!name) + { + name = imageUrl; + } + + // lets also add the frame to pixi's global cache for fromFrame and fromImage fucntions + BaseTextureCache[name] = baseTexture; + TextureCache[name] = texture; + + // also add references by url if they are different. + if (name !== imageUrl) + { + BaseTextureCache[imageUrl] = baseTexture; + TextureCache[imageUrl] = texture; + } + + return texture; + } + + /** * Adds a texture to the global TextureCache. This cache is shared across the whole PIXI object. * * @static @@ -522,6 +560,29 @@ } } +function createWhiteTexture() +{ + const canvas = document.createElement('canvas'); + + canvas.width = 10; + canvas.height = 10; + + const context = canvas.getContext('2d'); + + context.fillStyle = 'white'; + context.fillRect(0, 0, 10, 10); + + return new Texture(new BaseTexture(canvas)); +} + +function removeAllHandlers(tex) +{ + tex.destroy = function _emptyDestroy() { /* empty */ }; + tex.on = function _emptyOn() { /* empty */ }; + tex.once = function _emptyOnce() { /* empty */ }; + tex.emit = function _emptyEmit() { /* empty */ }; +} + /** * An empty texture, used often to not have to create multiple empty textures. * Can not be destroyed. @@ -530,7 +591,14 @@ * @constant */ Texture.EMPTY = new Texture(new BaseTexture()); -Texture.EMPTY.destroy = function _emptyDestroy() { /* empty */ }; -Texture.EMPTY.on = function _emptyOn() { /* empty */ }; -Texture.EMPTY.once = function _emptyOnce() { /* empty */ }; -Texture.EMPTY.emit = function _emptyEmit() { /* empty */ }; +removeAllHandlers(Texture.EMPTY); + +/** + * A white texture of 10x10 size, used for graphics and other things + * Can not be destroyed. + * + * @static + * @constant + */ +Texture.WHITE = createWhiteTexture(); +removeAllHandlers(Texture.WHITE); diff --git a/src/core/ticker/Ticker.js b/src/core/ticker/Ticker.js index d98a1f0..17a1517 100644 --- a/src/core/ticker/Ticker.js +++ b/src/core/ticker/Ticker.js @@ -67,11 +67,12 @@ * is based, this value is neither capped nor scaled. * If the platform supports DOMHighResTimeStamp, * this value will have a precision of 1 µs. + * Defaults to target frame time * * @member {number} - * @default 1 / TARGET_FPMS + * @default 16.66 */ - this.elapsedMS = 1 / settings.TARGET_FPMS; // default to target frame time + this.elapsedMS = 1 / settings.TARGET_FPMS; /** * The last time {@link PIXI.ticker.Ticker#update} was invoked. diff --git a/src/core/utils/index.js b/src/core/utils/index.js index 6ff609f..be44750 100644 --- a/src/core/utils/index.js +++ b/src/core/utils/index.js @@ -2,6 +2,7 @@ import settings from '../settings'; import EventEmitter from 'eventemitter3'; import pluginTarget from './pluginTarget'; +import * as mixins from './mixin'; import * as isMobile from 'ismobilejs'; let nextUid = 0; @@ -33,6 +34,7 @@ * @type {mixin} */ pluginTarget, + mixins, }; /** @@ -93,7 +95,7 @@ */ export function rgb2hex(rgb) { - return (((rgb[0] * 255) << 16) + ((rgb[1] * 255) << 8) + (rgb[2] * 255)); + return (((rgb[0] * 255) << 16) + ((rgb[1] * 255) << 8) + (rgb[2] * 255 | 0)); } /** @@ -368,3 +370,43 @@ * @private */ export const ProgramCache = {}; + +/** + * Destroys all texture in the cache + * + * @memberof PIXI.utils + * @function destroyTextureCache + */ +export function destroyTextureCache() +{ + let key; + + for (key in TextureCache) + { + TextureCache[key].destroy(); + } + for (key in BaseTextureCache) + { + BaseTextureCache[key].destroy(); + } +} + +/** + * Removes all textures from cache, but does not destroy them + * + * @memberof PIXI.utils + * @function clearTextureCache + */ +export function clearTextureCache() +{ + let key; + + for (key in TextureCache) + { + delete TextureCache[key]; + } + for (key in BaseTextureCache) + { + delete BaseTextureCache[key]; + } +} diff --git a/src/core/utils/mixin.js b/src/core/utils/mixin.js new file mode 100644 index 0000000..114de2a --- /dev/null +++ b/src/core/utils/mixin.js @@ -0,0 +1,59 @@ +/** + * Mixes all enumerable properties and methods from a source object to a target object. + * + * @memberof PIXI.utils.mixins + * @function mixin + * @param {object} target The prototype or instance that properties and methods should be added to. + * @param {object} source The source of properties and methods to mix in. + */ +export function mixin(target, source) +{ + if (!target || !source) return; + // in ES8/ES2017, this would be really easy: + // Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); + + // get all the enumerable property keys + const keys = Object.keys(source); + + // loop through properties + for (let i = 0; i < keys.length; ++i) + { + const propertyName = keys[i]; + + // Set the property using the property descriptor - this works for accessors and normal value properties + Object.defineProperty(target, propertyName, Object.getOwnPropertyDescriptor(source, propertyName)); + } +} + +const mixins = []; + +/** + * Queues a mixin to be handled towards the end of the initialization of PIXI, so that deprecation + * can take effect. + * + * @memberof PIXI.utils.mixins + * @function delayMixin + * @private + * @param {object} target The prototype or instance that properties and methods should be added to. + * @param {object} source The source of properties and methods to mix in. + */ +export function delayMixin(target, source) +{ + mixins.push(target, source); +} + +/** + * Handles all mixins queued via delayMixin(). + * + * @memberof PIXI.utils.mixins + * @function performMixins + * @private + */ +export function performMixins() +{ + for (let i = 0; i < mixins.length; i += 2) + { + mixin(mixins[i], mixins[i + 1]); + } + mixins.length = 0; +} diff --git a/src/deprecation.js b/src/deprecation.js index 045a19f..b9c73b3 100644 --- a/src/deprecation.js +++ b/src/deprecation.js @@ -5,6 +5,7 @@ import * as filters from './filters'; import * as prepare from './prepare'; import * as loaders from './loaders'; +import * as interaction from './interaction'; // provide method to give a stack track for warnings // useful for tracking-down where deprecated methods/properties/classes @@ -534,7 +535,7 @@ { parent: 'GC_MODES', target: 'GC_MODE' }, { parent: 'WRAP_MODES', target: 'WRAP_MODE' }, { parent: 'SCALE_MODES', target: 'SCALE_MODE' }, - { parent: 'PRECISION', target: 'PRECISION' }, + { parent: 'PRECISION', target: 'PRECISION_FRAGMENT' }, ]; for (let i = 0; i < defaults.length; i++) @@ -558,6 +559,32 @@ }); } +Object.defineProperties(core.settings, { + + /** + * @static + * @name PRECISION + * @memberof PIXI.settings + * @see PIXI.PRECISION + * @deprecated since version 4.4.0 + */ + PRECISION: { + enumerable: true, + get() + { + warn('PIXI.settings.PRECISION has been deprecated, please use PIXI.settings.PRECISION_FRAGMENT'); + + return core.settings.PRECISION_FRAGMENT; + }, + set(value) + { + warn('PIXI.settings.PRECISION has been deprecated, please use PIXI.settings.PRECISION_FRAGMENT'); + + core.settings.PRECISION_FRAGMENT = value; + }, + }, +}); + Object.defineProperties(extras, { /** @@ -694,7 +721,7 @@ warn('determineFontProperties is now deprecated, please use the static calculateFontProperties method, ' + 'e.g : Text.calculateFontProperties(fontStyle);'); - return Text.calculateFontProperties(fontStyle); + return core.Text.calculateFontProperties(fontStyle); }; Object.defineProperties(core.TextStyle.prototype, { @@ -1003,3 +1030,69 @@ }, }, }); + +/** + * @name PIXI.interaction.interactiveTarget#defaultCursor + * @static + * @type {number} + * @see PIXI.interaction.interactiveTarget#cursor + * @deprecated since 4.3.0 + */ +Object.defineProperty(interaction.interactiveTarget, 'defaultCursor', { + set(value) + { + warn('Property defaultCursor has been replaced with \'cursor\'. '); + this.cursor = value; + }, + get() + { + warn('Property defaultCursor has been replaced with \'cursor\'. '); + + return this.cursor; + }, + enumerable: true, +}); + +/** + * @name PIXI.interaction.InteractionManager#defaultCursorStyle + * @static + * @type {string} + * @see PIXI.interaction.InteractionManager#cursorStyles + * @deprecated since 4.3.0 + */ +Object.defineProperty(interaction.InteractionManager, 'defaultCursorStyle', { + set(value) + { + warn('Property defaultCursorStyle has been replaced with \'cursorStyles.default\'. '); + this.cursorStyles.default = value; + }, + get() + { + warn('Property defaultCursorStyle has been replaced with \'cursorStyles.default\'. '); + + return this.cursorStyles.default; + }, +}); + +/** + * @name PIXI.interaction.InteractionManager#currentCursorStyle + * @static + * @type {string} + * @see PIXI.interaction.InteractionManager#cursorStyles + * @deprecated since 4.3.0 + */ +Object.defineProperty(interaction.InteractionManager, 'currentCursorStyle', { + set(value) + { + warn('Property currentCursorStyle has been removed.' + + 'See the currentCursorMode property, which works differently.'); + this.currentCursorMode = value; + }, + get() + { + warn('Property currentCursorStyle has been removed.' + + 'See the currentCursorMode property, which works differently.'); + + return this.currentCursorMode; + }, +}); diff --git a/src/extras/BitmapText.js b/src/extras/BitmapText.js index fb6c343..0a0e150 100644 --- a/src/extras/BitmapText.js +++ b/src/extras/BitmapText.js @@ -455,6 +455,73 @@ return this._textHeight; } + + /** + * Register a bitmap font with data and a texture. + * + * @static + * @param {XMLDocument} xml - The XML document data. + * @param {PIXI.Texture} texture - Texture with all symbols. + * @return {Object} Result font object with font, size, lineHeight and char fields. + */ + static registerFont(xml, texture) + { + const data = {}; + const info = xml.getElementsByTagName('info')[0]; + const common = xml.getElementsByTagName('common')[0]; + + data.font = info.getAttribute('face'); + data.size = parseInt(info.getAttribute('size'), 10); + data.lineHeight = parseInt(common.getAttribute('lineHeight'), 10); + data.chars = {}; + + // parse letters + const letters = xml.getElementsByTagName('char'); + + for (let i = 0; i < letters.length; i++) + { + const letter = letters[i]; + const charCode = parseInt(letter.getAttribute('id'), 10); + + const textureRect = new core.Rectangle( + parseInt(letter.getAttribute('x'), 10) + texture.frame.x, + parseInt(letter.getAttribute('y'), 10) + texture.frame.y, + parseInt(letter.getAttribute('width'), 10), + parseInt(letter.getAttribute('height'), 10) + ); + + data.chars[charCode] = { + xOffset: parseInt(letter.getAttribute('xoffset'), 10), + yOffset: parseInt(letter.getAttribute('yoffset'), 10), + xAdvance: parseInt(letter.getAttribute('xadvance'), 10), + kerning: {}, + texture: new core.Texture(texture.baseTexture, textureRect), + + }; + } + + // parse kernings + const kernings = xml.getElementsByTagName('kerning'); + + for (let i = 0; i < kernings.length; i++) + { + const kerning = kernings[i]; + const first = parseInt(kerning.getAttribute('first'), 10); + const second = parseInt(kerning.getAttribute('second'), 10); + const amount = parseInt(kerning.getAttribute('amount'), 10); + + if (data.chars[second]) + { + data.chars[second].kerning[first] = amount; + } + } + + // I'm leaving this as a temporary fix so we can test the bitmap fonts in v3 + // but it's very likely to change + BitmapText.fonts[data.font] = data; + + return data; + } } BitmapText.fonts = {}; diff --git a/src/index.js b/src/index.js index 59356cf..c553427 100644 --- a/src/index.js +++ b/src/index.js @@ -16,6 +16,10 @@ import * as particles from './particles'; import * as prepare from './prepare'; +// handle mixins now, after all code has been added, including deprecation +import { utils } from './core'; +utils.mixins.performMixins(); + export { accessibility, extract, diff --git a/package.json b/package.json index 6a366ba..71ade15 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pixi.js", - "version": "4.3.4", + "version": "4.3.5", "description": "Pixi.js is a fast lightweight 2D library that works across all devices.", "author": "Mat Groves", "contributors": [ @@ -61,7 +61,7 @@ "ismobilejs": "^0.4.0", "object-assign": "^4.0.1", "pixi-gl-core": "^1.0.3", - "resource-loader": "^2.0.4" + "resource-loader": "^2.0.6" }, "devDependencies": { "babel-cli": "^6.18.0", @@ -70,9 +70,9 @@ "babel-preset-es2015": "^6.14.0", "babelify": "^7.3.0", "del": "^2.2.0", - "electron-prebuilt": "^1.3.2", + "electron": "^1.4.15", "eslint": "^3.5.0", - "floss": "^1.2.0", + "floss": "^2.0.1", "gh-pages": "^0.11.0", "jaguarjs-jsdoc": "^1.0.1", "js-md5": "^0.4.1", diff --git a/src/accessibility/AccessibilityManager.js b/src/accessibility/AccessibilityManager.js index df2716a..f16bcb1 100644 --- a/src/accessibility/AccessibilityManager.js +++ b/src/accessibility/AccessibilityManager.js @@ -3,7 +3,7 @@ import accessibleTarget from './accessibleTarget'; // add some extra variables to the container.. -Object.assign( +core.utils.mixins.delayMixin( core.DisplayObject.prototype, accessibleTarget ); diff --git a/src/core/Application.js b/src/core/Application.js index ea0f058..5340578 100644 --- a/src/core/Application.js +++ b/src/core/Application.js @@ -1,6 +1,6 @@ import { autoDetectRenderer } from './autoDetectRenderer'; import Container from './display/Container'; -import Ticker from './ticker/Ticker'; +import { shared, Ticker } from './ticker'; /** * Convenience class to create a new PIXI application. @@ -35,9 +35,9 @@ * @param {boolean} [noWebGL=false] - prevents selection of WebGL renderer, even if such is present * @param {boolean} [options.legacy=false] - If true Pixi will aim to ensure compatibility * with older / less advanced devices. If you experience unexplained flickering try setting this to true. - + * @param {boolean} [useSharedTicker=false] - `true` to use PIXI.ticker.shared, `false` to create new ticker. */ - constructor(width, height, options, noWebGL) + constructor(width, height, options, noWebGL, useSharedTicker = false) { /** * WebGL renderer if available, otherwise CanvasRenderer @@ -46,23 +46,46 @@ this.renderer = autoDetectRenderer(width, height, options, noWebGL); /** - * The root display container that's renderered. + * The root display container that's rendered. * @member {PIXI.Container} */ this.stage = new Container(); /** + * Internal reference to the ticker + * @member {PIXI.ticker.Ticker} + * @private + */ + this._ticker = null; + + /** * Ticker for doing render updates. * @member {PIXI.ticker.Ticker} + * @default PIXI.ticker.shared */ - this.ticker = new Ticker(); - - this.ticker.add(this.render, this); + this.ticker = useSharedTicker ? shared : new Ticker(); // Start the rendering this.start(); } + set ticker(ticker) // eslint-disable-line require-jsdoc + { + if (this._ticker) + { + this._ticker.remove(this.render, this); + } + this._ticker = ticker; + if (ticker) + { + ticker.add(this.render, this); + } + } + get ticker() // eslint-disable-line require-jsdoc + { + return this._ticker; + } + /** * Render the current stage. */ @@ -76,7 +99,7 @@ */ stop() { - this.ticker.stop(); + this._ticker.stop(); } /** @@ -84,7 +107,7 @@ */ start() { - this.ticker.start(); + this._ticker.start(); } /** @@ -98,13 +121,22 @@ } /** + * Reference to the renderer's screen rectangle. Its safe to use as filterArea or hitArea for whole screen + * @member {PIXI.Rectangle} + * @readonly + */ + get screen() + { + return this.renderer.screen; + } + + /** * Destroy and don't use after this. * @param {Boolean} [removeView=false] Automatically remove canvas from DOM. */ destroy(removeView) { this.stop(); - this.ticker.remove(this.render, this); this.ticker = null; this.stage.destroy(); diff --git a/src/core/Shader.js b/src/core/Shader.js new file mode 100644 index 0000000..78b17e6 --- /dev/null +++ b/src/core/Shader.js @@ -0,0 +1,46 @@ +import { GLShader } from 'pixi-gl-core'; +import settings from './settings'; + +function checkPrecision(src, def) +{ + if (src instanceof Array) + { + if (src[0].substring(0, 9) !== 'precision') + { + const copy = src.slice(0); + + copy.unshift(`precision ${def} float;`); + + return copy; + } + } + else if (src.substring(0, 9) !== 'precision') + { + return `precision ${def} float;\n${src}`; + } + + return src; +} + +/** + * Wrapper class, webGL Shader for Pixi. + * Adds precision string if vertexSrc or fragmentSrc have no mention of it. + * + * @class + * @extends GLShader + * @memberof PIXI + */ +export default class Shader extends GLShader +{ + /** + * + * @param {WebGLRenderingContext} gl - The current WebGL rendering context + * @param {string|string[]} vertexSrc - The vertex shader source as an array of strings. + * @param {string|string[]} fragmentSrc - The fragment shader source as an array of strings. + */ + constructor(gl, vertexSrc, fragmentSrc) + { + super(gl, checkPrecision(vertexSrc, settings.PRECISION_VERTEX), + checkPrecision(fragmentSrc, settings.PRECISION_FRAGMENT)); + } +} diff --git a/src/core/const.js b/src/core/const.js index ef6f662..3de0efe 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -188,7 +188,7 @@ * The gc modes that are supported by pixi. * * The {@link PIXI.settings.GC_MODE} Garbage Collection mode for pixi textures is AUTO - * If set to GC_MODE, the renderer will occasianally check textures usage. If they are not + * If set to GC_MODE, the renderer will occasionally check textures usage. If they are not * used for a specified period of time they will be removed from the GPU. They will of course * be uploaded again when they are required. This is a silent behind the scenes process that * should ensure that the GPU does not get filled up. diff --git a/src/core/display/Container.js b/src/core/display/Container.js index ed8cd5f..17fde8d 100644 --- a/src/core/display/Container.js +++ b/src/core/display/Container.js @@ -73,11 +73,12 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.push(child); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -108,11 +109,12 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.splice(index, 0, child); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -225,10 +227,11 @@ if (index === -1) return null; child.parent = null; + // ensure child transform will be recalculated + child.transform._parentID = -1; removeItems(this.children, index, 1); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -249,11 +252,12 @@ { const child = this.getChildAt(index); + // ensure child transform will be recalculated.. child.parent = null; + child.transform._parentID = -1; removeItems(this.children, index, 1); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -284,13 +288,12 @@ for (let i = 0; i < removed.length; ++i) { removed[i].parent = null; + if (removed[i].transform) + { + removed[i].transform._parentID = -1; + } } - // ensure a transform will be recalculated.. - if (this.transform) - { - this.transform._parentID = -1; - } this._boundsID++; this.onChildrenChange(beginIndex); diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index f37f2f5..be4c4fb 100644 --- a/src/core/graphics/Graphics.js +++ b/src/core/graphics/Graphics.js @@ -28,8 +28,9 @@ { /** * + * @param {boolean} [nativeLines=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP */ - constructor() + constructor(nativeLines = false) { super(); @@ -50,6 +51,13 @@ this.lineWidth = 0; /** + * If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * + * @member {boolean} + */ + this.nativeLines = nativeLines; + + /** * The color of any lines drawn. * * @member {string} @@ -757,28 +765,14 @@ if (!this._spriteRect) { - if (!Graphics._SPRITE_TEXTURE) - { - Graphics._SPRITE_TEXTURE = RenderTexture.create(10, 10); - - const canvas = document.createElement('canvas'); - - canvas.width = 10; - canvas.height = 10; - - const context = canvas.getContext('2d'); - - context.fillStyle = 'white'; - context.fillRect(0, 0, 10, 10); - - Graphics._SPRITE_TEXTURE = Texture.fromCanvas(canvas); - } - - this._spriteRect = new Sprite(Graphics._SPRITE_TEXTURE); + this._spriteRect = new Sprite(new Texture(Texture.WHITE)); } + + const sprite = this._spriteRect; + if (this.tint === 0xffffff) { - this._spriteRect.tint = this.graphicsData[0].fillColor; + sprite.tint = this.graphicsData[0].fillColor; } else { @@ -792,20 +786,21 @@ t1[1] *= t2[1]; t1[2] *= t2[2]; - this._spriteRect.tint = rgb2hex(t1); + sprite.tint = rgb2hex(t1); } - this._spriteRect.alpha = this.graphicsData[0].fillAlpha; - this._spriteRect.worldAlpha = this.worldAlpha * this._spriteRect.alpha; + sprite.alpha = this.graphicsData[0].fillAlpha; + sprite.worldAlpha = this.worldAlpha * sprite.alpha; + sprite.blendMode = this.blendMode; - Graphics._SPRITE_TEXTURE._frame.width = rect.width; - Graphics._SPRITE_TEXTURE._frame.height = rect.height; + sprite.texture._frame.width = rect.width; + sprite.texture._frame.height = rect.height; - this._spriteRect.transform.worldTransform = this.transform.worldTransform; + sprite.transform.worldTransform = this.transform.worldTransform; - this._spriteRect.anchor.set(-rect.x / rect.width, -rect.y / rect.height); - this._spriteRect._onAnchorUpdate(); + sprite.anchor.set(-rect.x / rect.width, -rect.y / rect.height); + sprite._onAnchorUpdate(); - this._spriteRect._renderWebGL(renderer); + sprite._renderWebGL(renderer); } /** @@ -1031,6 +1026,7 @@ this.fillColor, this.fillAlpha, this.filling, + this.nativeLines, shape ); diff --git a/src/core/graphics/GraphicsData.js b/src/core/graphics/GraphicsData.js index f5a87ae..b6f03c0 100644 --- a/src/core/graphics/GraphicsData.js +++ b/src/core/graphics/GraphicsData.js @@ -14,14 +14,19 @@ * @param {number} fillColor - the color of the fill * @param {number} fillAlpha - the alpha of the fill * @param {boolean} fill - whether or not the shape is filled with a colour + * @param {boolean} nativeLines - the method for drawing lines * @param {PIXI.Circle|PIXI.Rectangle|PIXI.Ellipse|PIXI.Polygon} shape - The shape object to draw. */ - constructor(lineWidth, lineColor, lineAlpha, fillColor, fillAlpha, fill, shape) + constructor(lineWidth, lineColor, lineAlpha, fillColor, fillAlpha, fill, nativeLines, shape) { /** * @member {number} the width of the line to draw */ this.lineWidth = lineWidth; + /** + * @member {boolean} if true the liens will be draw using LINES instead of TRIANGLE_STRIP + */ + this.nativeLines = nativeLines; /** * @member {number} the color of the line to draw @@ -85,6 +90,7 @@ this.fillColor, this.fillAlpha, this.fill, + this.nativeLines, this.shape ); } diff --git a/src/core/graphics/webgl/GraphicsRenderer.js b/src/core/graphics/webgl/GraphicsRenderer.js index 14083fe..67f5e92 100644 --- a/src/core/graphics/webgl/GraphicsRenderer.js +++ b/src/core/graphics/webgl/GraphicsRenderer.js @@ -102,7 +102,15 @@ shaderTemp.uniforms.alpha = graphics.worldAlpha; renderer.bindVao(webGLData.vao); - webGLData.vao.draw(gl.TRIANGLE_STRIP, webGLData.indices.length); + + if (graphics.nativeLines) + { + gl.drawArrays(gl.LINES, 0, webGLData.points.length / 6); + } + else + { + webGLData.vao.draw(gl.TRIANGLE_STRIP, webGLData.indices.length); + } } } diff --git a/src/core/graphics/webgl/utils/buildLine.js b/src/core/graphics/webgl/utils/buildLine.js index ecf43f7..4d6862e 100644 --- a/src/core/graphics/webgl/utils/buildLine.js +++ b/src/core/graphics/webgl/utils/buildLine.js @@ -11,7 +11,29 @@ * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties * @param {object} webGLData - an object containing all the webGL-specific information to create this shape */ -export default function buildLine(graphicsData, webGLData) +export default function (graphicsData, webGLData) +{ + if (graphicsData.nativeLines) + { + buildNativeLine(graphicsData, webGLData); + } + else + { + buildLine(graphicsData, webGLData); + } +} + +/** + * Builds a line to draw using the poligon method. + * + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @private + * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties + * @param {object} webGLData - an object containing all the webGL-specific information to create this shape + */ +function buildLine(graphicsData, webGLData) { // TODO OPTIMISE! let points = graphicsData.points; @@ -224,3 +246,46 @@ indices.push(indexStart - 1); } + +/** + * Builds a line to draw using the gl.drawArrays(gl.LINES) method + * + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @private + * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties + * @param {object} webGLData - an object containing all the webGL-specific information to create this shape + */ +function buildNativeLine(graphicsData, webGLData) +{ + let i = 0; + const points = graphicsData.points; + + if (points.length === 0) return; + + const verts = webGLData.points; + const length = points.length / 2; + + // sort color + const color = hex2rgb(graphicsData.lineColor); + const alpha = graphicsData.lineAlpha; + const r = color[0] * alpha; + const g = color[1] * alpha; + const b = color[2] * alpha; + + for (i = 1; i < length; i++) + { + const p1x = points[(i - 1) * 2]; + const p1y = points[((i - 1) * 2) + 1]; + + const p2x = points[i * 2]; + const p2y = points[(i * 2) + 1]; + + verts.push(p1x, p1y); + verts.push(r, g, b, alpha); + + verts.push(p2x, p2y); + verts.push(r, g, b, alpha); + } +} diff --git a/src/core/graphics/webgl/utils/buildRoundedRectangle.js b/src/core/graphics/webgl/utils/buildRoundedRectangle.js index 9abc807..a6c8461 100644 --- a/src/core/graphics/webgl/utils/buildRoundedRectangle.js +++ b/src/core/graphics/webgl/utils/buildRoundedRectangle.js @@ -77,6 +77,26 @@ } /** + * Calculate a single point for a quadratic bezier curve. + * Utility function used by quadraticBezierCurve. + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @private + * @param {number} n1 - first number + * @param {number} n2 - second number + * @param {number} perc - percentage + * @return {number} the result + * + */ +function getPt(n1, n2, perc) +{ + const diff = n2 - n1; + + return n1 + (diff * perc); +} + +/** * Calculate the points for a quadratic bezier curve. (helper function..) * Based on: https://stackoverflow.com/questions/785097/how-do-i-implement-a-bezier-curve-in-c * @@ -105,13 +125,6 @@ let x = 0; let y = 0; - function getPt(n1, n2, perc) - { - const diff = n2 - n1; - - return n1 + (diff * perc); - } - for (let i = 0, j = 0; i <= n; ++i) { j = i / n; diff --git a/src/core/index.js b/src/core/index.js index f7a817c..4f414e8 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -30,6 +30,7 @@ export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; export { default as CanvasGraphicsRenderer } from './graphics/canvas/CanvasGraphicsRenderer'; +export { default as Spritesheet } from './textures/Spritesheet'; export { default as Texture } from './textures/Texture'; export { default as BaseTexture } from './textures/BaseTexture'; export { default as RenderTexture } from './textures/RenderTexture'; diff --git a/src/core/math/Matrix.js b/src/core/math/Matrix.js index 0b77477..3679860 100644 --- a/src/core/math/Matrix.js +++ b/src/core/math/Matrix.js @@ -13,45 +13,50 @@ export default class Matrix { /** - * + * @param {number} [a=1] - x scale + * @param {number} [b=0] - y skew + * @param {number} [c=0] - x skew + * @param {number} [d=1] - y scale + * @param {number} [tx=0] - x translation + * @param {number} [ty=0] - y translation */ - constructor() + constructor(a = 1, b = 0, c = 0, d = 1, tx = 0, ty = 0) { /** * @member {number} * @default 1 */ - this.a = 1; + this.a = a; /** * @member {number} * @default 0 */ - this.b = 0; + this.b = b; /** * @member {number} * @default 0 */ - this.c = 0; + this.c = c; /** * @member {number} * @default 1 */ - this.d = 1; + this.d = d; /** * @member {number} * @default 0 */ - this.tx = 0; + this.tx = tx; /** * @member {number} * @default 0 */ - this.ty = 0; + this.ty = ty; this.array = null; } diff --git a/src/core/renderers/SystemRenderer.js b/src/core/renderers/SystemRenderer.js index fa116a6..a24c775 100644 --- a/src/core/renderers/SystemRenderer.js +++ b/src/core/renderers/SystemRenderer.js @@ -1,5 +1,5 @@ import { sayHello, hex2string, hex2rgb } from '../utils'; -import { Matrix } from '../math'; +import { Matrix, Rectangle } from '../math'; import { RENDERER_TYPE } from '../const'; import settings from '../settings'; import Container from '../display/Container'; @@ -21,8 +21,8 @@ { /** * @param {string} system - The name of the system this renderer is for. - * @param {number} [width=800] - the width of the canvas view - * @param {number} [height=600] - the height of the canvas view + * @param {number} [screenWidth=800] - the width of the screen + * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false @@ -37,7 +37,7 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(system, width, height, options) + constructor(system, screenWidth, screenHeight, options) { super(); @@ -69,20 +69,13 @@ this.type = RENDERER_TYPE.UNKNOWN; /** - * The width of the canvas view + * Measurements of the screen. (0, 0, screenWidth, screenHeight) * - * @member {number} - * @default 800 - */ - this.width = width || 800; - - /** - * The height of the canvas view + * Its safe to use as filterArea or hitArea for whole stage * - * @member {number} - * @default 600 + * @member {PIXI.Rectangle} */ - this.height = height || 600; + this.screen = new Rectangle(0, 0, screenWidth || 800, screenHeight || 600); /** * The canvas element that everything is drawn to @@ -107,7 +100,7 @@ this.transparent = options.transparent; /** - * Whether the render view should be resized automatically + * Whether css dimensions of canvas view should be resized to screen dimensions automatically * * @member {boolean} */ @@ -192,23 +185,48 @@ } /** - * Resizes the canvas view to the specified width and height + * Same as view.width, actual number of pixels in the canvas by horizontal * - * @param {number} width - the new width of the canvas view - * @param {number} height - the new height of the canvas view + * @member {number} + * @readonly + * @default 800 */ - resize(width, height) + get width() { - this.width = width * this.resolution; - this.height = height * this.resolution; + return this.view.width; + } - this.view.width = this.width; - this.view.height = this.height; + /** + * Same as view.height, actual number of pixels in the canvas by vertical + * + * @member {number} + * @readonly + * @default 600 + */ + get height() + { + return this.view.height; + } + + /** + * Resizes the screen and canvas to the specified width and height + * Canvas dimensions are multiplied by resolution + * + * @param {number} screenWidth - the new width of the screen + * @param {number} screenHeight - the new height of the screen + */ + resize(screenWidth, screenHeight) + { + this.screen.width = screenWidth; + this.screen.height = screenHeight; + + this.view.width = screenWidth * this.resolution; + this.view.height = screenHeight * this.resolution; if (this.autoResize) { - this.view.style.width = `${this.width / this.resolution}px`; - this.view.style.height = `${this.height / this.resolution}px`; + this.view.style.width = `${screenWidth}px`; + this.view.style.height = `${screenHeight}px`; } } @@ -249,11 +267,10 @@ this.type = RENDERER_TYPE.UNKNOWN; - this.width = 0; - this.height = 0; - this.view = null; + this.screen = null; + this.resolution = 0; this.transparent = false; diff --git a/src/core/renderers/canvas/CanvasRenderer.js b/src/core/renderers/canvas/CanvasRenderer.js index 1bd2d5e..e84e49c 100644 --- a/src/core/renderers/canvas/CanvasRenderer.js +++ b/src/core/renderers/canvas/CanvasRenderer.js @@ -18,8 +18,8 @@ export default class CanvasRenderer extends SystemRenderer { /** - * @param {number} [width=800] - the width of the canvas view - * @param {number} [height=600] - the height of the canvas view + * @param {number} [screenWidth=800] - the width of the screen + * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false @@ -34,9 +34,9 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(width, height, options = {}) + constructor(screenWidth, screenHeight, options = {}) { - super('Canvas', width, height, options); + super('Canvas', screenWidth, screenHeight, options); this.type = RENDERER_TYPE.CANVAS; @@ -96,7 +96,7 @@ this.context = null; this.renderingToScreen = false; - this.resize(width, height); + this.resize(screenWidth, screenHeight); } /** @@ -283,12 +283,12 @@ * * @extends PIXI.SystemRenderer#resize * - * @param {number} width - The new width of the canvas view - * @param {number} height - The new height of the canvas view + * @param {number} screenWidth - the new width of the screen + * @param {number} screenHeight - the new height of the screen */ - resize(width, height) + resize(screenWidth, screenHeight) { - super.resize(width, height); + super.resize(screenWidth, screenHeight); // reset the scale mode.. oddly this seems to be reset when the canvas is resized. // surely a browser bug?? Let pixi fix that for you.. diff --git a/src/core/renderers/webgl/TextureManager.js b/src/core/renderers/webgl/TextureManager.js index b685c1a..8227289 100644 --- a/src/core/renderers/webgl/TextureManager.js +++ b/src/core/renderers/webgl/TextureManager.js @@ -61,7 +61,7 @@ * Updates and/or Creates a WebGL texture for the renderer's context. * * @param {PIXI.BaseTexture|PIXI.Texture} texture - the texture to update - * @param {Number} location - the location the texture will be bound to. + * @param {number} location - the location the texture will be bound to. * @return {GLTexture} The gl texture. */ updateTexture(texture, location) diff --git a/src/core/renderers/webgl/WebGLRenderer.js b/src/core/renderers/webgl/WebGLRenderer.js index e4cd8a0..830b382 100644 --- a/src/core/renderers/webgl/WebGLRenderer.js +++ b/src/core/renderers/webgl/WebGLRenderer.js @@ -31,8 +31,8 @@ { /** * - * @param {number} [width=0] - the width of the canvas view - * @param {number} [height=0] - the height of the canvas view + * @param {number} [screenWidth=800] - the width of the screen + * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false @@ -53,9 +53,9 @@ * @param {boolean} [options.legacy=false] - If true Pixi will aim to ensure compatibility * with older / less advanced devices. If you experiance unexplained flickering try setting this to true. */ - constructor(width, height, options = {}) + constructor(screenWidth, screenHeight, options = {}) { - super('WebGL', width, height, options); + super('WebGL', screenWidth, screenHeight, options); this.legacy = !!options.legacy; @@ -245,7 +245,7 @@ glCore._testingContext = gl; // setup the width/height properties and gl viewport - this.resize(this.width, this.height); + this.resize(this.screen.width, this.screen.height); } /** @@ -343,16 +343,16 @@ /** * Resizes the webGL view to the specified width and height. * - * @param {number} width - the new width of the webGL view - * @param {number} height - the new height of the webGL view + * @param {number} screenWidth - the new width of the screen + * @param {number} screenHeight - the new height of the screen */ - resize(width, height) + resize(screenWidth, screenHeight) { // if(width * this.resolution === this.width && height * this.resolution === this.height)return; - SystemRenderer.prototype.resize.call(this, width, height); + SystemRenderer.prototype.resize.call(this, screenWidth, screenHeight); - this.rootRenderTarget.resize(width, height); + this.rootRenderTarget.resize(screenWidth, screenHeight); if (this._activeRenderTarget === this.rootRenderTarget) { @@ -396,6 +396,26 @@ } /** + * Erases the render texture and fills the drawing area with a colour + * + * @param {PIXI.RenderTexture} renderTexture - The render texture to clear + * @param {number} [clearColor] - The colour + * @return {PIXI.WebGLRenderer} Returns itself. + */ + clearRenderTexture(renderTexture, clearColor) + { + const baseTexture = renderTexture.baseTexture; + const renderTarget = baseTexture._glRenderTargets[this.CONTEXT_UID]; + + if (renderTarget) + { + renderTarget.clear(clearColor); + } + + return this; + } + + /** * Binds a render texture for rendering * * @param {PIXI.RenderTexture} renderTexture - The render texture to render @@ -474,9 +494,10 @@ * Changes the current GLShader to the one given in parameter * * @param {PIXI.glCore.Shader} shader - the new glShader + * @param {boolean} [autoProject=true] - Whether automatically set the projection matrix * @return {PIXI.WebGLRenderer} Returns itself. */ - _bindGLShader(shader) + _bindGLShader(shader, autoProject) { // TODO cache if (this._activeShader !== shader) @@ -484,8 +505,14 @@ this._activeShader = shader; shader.bind(); - // automatically set the projection matrix - shader.uniforms.projectionMatrix = this._activeRenderTarget.projectionMatrix.toArray(true); + // `autoProject` normally would be a default parameter set to true + // but because of how Babel transpiles default parameters + // it hinders the performance of this method. + if (autoProject !== false) + { + // automatically set the projection matrix + shader.uniforms.projectionMatrix = this._activeRenderTarget.projectionMatrix.toArray(true); + } } return this; diff --git a/src/core/renderers/webgl/WebGLState.js b/src/core/renderers/webgl/WebGLState.js index ba6ecc5..6c846d5 100755 --- a/src/core/renderers/webgl/WebGLState.js +++ b/src/core/renderers/webgl/WebGLState.js @@ -82,18 +82,20 @@ push() { // next state.. - let state = this.stack[++this.stackIndex]; + let state = this.stack[this.stackIndex]; if (!state) { state = this.stack[this.stackIndex] = new Uint8Array(16); } + ++this.stackIndex; + // copy state.. // set active state so we can force overrides of gl state for (let i = 0; i < this.activeState.length; i++) { - this.activeState[i] = state[i]; + state[i] = this.activeState[i]; } } diff --git a/src/core/renderers/webgl/filters/Filter.js b/src/core/renderers/webgl/filters/Filter.js index 2c3eaad..4eb6128 100644 --- a/src/core/renderers/webgl/filters/Filter.js +++ b/src/core/renderers/webgl/filters/Filter.js @@ -9,7 +9,7 @@ * @memberof PIXI * @extends PIXI.Shader */ -class Filter extends Shader +export default class Filter extends Shader { /** * @param {string} [vertexSrc] - The source of the vertex shader. @@ -56,15 +56,16 @@ * @param {PIXI.RenderTarget} input - The input render target. * @param {PIXI.RenderTarget} output - The target to output to. * @param {boolean} clear - Should the output be cleared before rendering to it + * @param {object} [currentState] - It's current state of filter. + * There are some useful properties in the currentState : + * target, filters, sourceFrame, destinationFrame, renderTarget, resolution */ - apply(filterManager, input, output, clear) + apply(filterManager, input, output, clear, currentState) // eslint-disable-line no-unused-vars { // do as you please! - filterManager.applyFilter(this, input, output, clear); + filterManager.applyFilter(this, input, output, clear, currentState); // or just do a regular render.. } } - -export default Filter; diff --git a/src/core/renderers/webgl/filters/extractUniformsFromSrc.js b/src/core/renderers/webgl/filters/extractUniformsFromSrc.js index d78a0b1..4893214 100644 --- a/src/core/renderers/webgl/filters/extractUniformsFromSrc.js +++ b/src/core/renderers/webgl/filters/extractUniformsFromSrc.js @@ -12,7 +12,7 @@ function extractUniformsFromString(string) { - const maskRegex = new RegExp('^(projectionMatrix|uSampler|filterArea)$'); + const maskRegex = new RegExp('^(projectionMatrix|uSampler|filterArea|filterClamp)$'); const uniforms = {}; let nameSplit; diff --git a/src/core/renderers/webgl/managers/FilterManager.js b/src/core/renderers/webgl/managers/FilterManager.js index 43fc869..6c7422d 100644 --- a/src/core/renderers/webgl/managers/FilterManager.js +++ b/src/core/renderers/webgl/managers/FilterManager.js @@ -2,6 +2,7 @@ import RenderTarget from '../utils/RenderTarget'; import Quad from '../utils/Quad'; import { Rectangle } from '../../../math'; +import Shader from '../../../Shader'; import * as filterTransforms from '../filters/filterTransforms'; import bitTwiddle from 'bit-twiddle'; @@ -22,8 +23,6 @@ this.filters = []; this.target = null; this.resolution = 1; - - this.firstRun = true; } } @@ -155,7 +154,7 @@ if (filters.length === 1) { - filters[0].apply(this, currentState.renderTarget, lastState.renderTarget, false); + filters[0].apply(this, currentState.renderTarget, lastState.renderTarget, false, currentState); this.freePotRenderTarget(currentState.renderTarget); } else @@ -177,7 +176,7 @@ for (i = 0; i < filters.length - 1; ++i) { - filters[i].apply(this, flip, flop, true); + filters[i].apply(this, flip, flop, true, currentState); const t = flip; @@ -185,7 +184,7 @@ flop = t; } - filters[i].apply(this, flip, lastState.renderTarget, false); + filters[i].apply(this, flip, lastState.renderTarget, false, currentState); this.freePotRenderTarget(flip); this.freePotRenderTarget(flop); @@ -212,16 +211,28 @@ const renderer = this.renderer; const gl = renderer.gl; - renderer.bindShader(filter, true); + let shader = filter.glShaders[renderer.CONTEXT_UID]; - // TODO theres a better way! - // quad can be a mesh and we then should be able to pull this off without accessing the shader directly - const shader = renderer.shaderManager.getGLShader(); - - // TODO change quad to mesh.. - if (!this.firstRun) + // cacheing.. + if (!shader) { - this.firstRun = true; + if (filter.glShaderKey) + { + shader = this.shaderCache[filter.glShaderKey]; + + if (!shader) + { + shader = new Shader(this.gl, filter.vertexSrc, filter.fragmentSrc); + + filter.glShaders[renderer.CONTEXT_UID] = this.shaderCache[filter.glShaderKey] = shader; + } + } + else + { + shader = filter.glShaders[renderer.CONTEXT_UID] = new Shader(this.gl, filter.vertexSrc, filter.fragmentSrc); + } + + // TODO - this only needs to be done once? renderer.bindVao(null); this.quad.initVao(shader); @@ -244,58 +255,146 @@ renderer.maskManager.pushScissorMask(null, renderer.maskManager.scissorData); } - // renderer._bindGLShader(shader); + renderer.bindShader(shader); - let currentState; - const uniforms = filter.uniforms; + // free unit 0 for us, doesn't matter what was there + // don't try to restore it, because syncUniforms can upload it to another slot + // and it'll be a problem + const tex = this.renderer.emptyTextures[0]; - if (uniforms.filterArea) - { - currentState = this.filterData.stack[this.filterData.index]; - - const filterArea = uniforms.filterArea; - - filterArea[0] = currentState.renderTarget.size.width; - filterArea[1] = currentState.renderTarget.size.height; - filterArea[2] = currentState.sourceFrame.x; - filterArea[3] = currentState.sourceFrame.y; - - filter.uniforms.filterArea = filterArea; - } - - // use this to clamp displaced texture coords so they belong to filterArea - // see displacementFilter fragment shader for an example - if (uniforms.filterClamp) - { - currentState = this.filterData.stack[this.filterData.index]; - - const filterClamp = uniforms.filterClamp; - - filterClamp[0] = 0; - filterClamp[1] = 0; - filterClamp[2] = (currentState.sourceFrame.width - 1) / currentState.renderTarget.size.width; - filterClamp[3] = (currentState.sourceFrame.height - 1) / currentState.renderTarget.size.height; - - filter.uniforms.filterClamp = filterClamp; - } - - renderer.shaderManager.setUniforms(filter.uniforms); + this.renderer.boundTextures[0] = tex; + // this syncs the pixi filters uniforms with glsl uniforms + this.syncUniforms(shader, filter); renderer.state.setBlendMode(filter.blendMode); - // temporary bypass cache.. - const tex = this.renderer.boundTextures[0]; - gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, input.texture.texture); this.quad.vao.draw(this.renderer.gl.TRIANGLES, 6, 0); - // restore cache. gl.bindTexture(gl.TEXTURE_2D, tex._glTextures[this.renderer.CONTEXT_UID].texture); } /** + * Uploads the uniforms of the filter. + * + * @param {GLShader} shader - The underlying gl shader. + * @param {PIXI.Filter} filter - The filter we are synchronizing. + */ + syncUniforms(shader, filter) + { + const uniformData = filter.uniformData; + const uniforms = filter.uniforms; + + // 0 is reserved for the pixi texture so we start at 1! + let textureCount = 1; + let currentState; + + // filterArea and filterClamp that are handled by FilterManager directly + // they must not appear in uniformData + + if (shader.uniforms.filterArea) + { + currentState = this.filterData.stack[this.filterData.index]; + + const filterArea = shader.uniforms.filterArea; + + filterArea[0] = currentState.renderTarget.size.width; + filterArea[1] = currentState.renderTarget.size.height; + filterArea[2] = currentState.sourceFrame.x; + filterArea[3] = currentState.sourceFrame.y; + + shader.uniforms.filterArea = filterArea; + } + + // use this to clamp displaced texture coords so they belong to filterArea + // see displacementFilter fragment shader for an example + if (shader.uniforms.filterClamp) + { + currentState = currentState || this.filterData.stack[this.filterData.index]; + + const filterClamp = shader.uniforms.filterClamp; + + filterClamp[0] = 0; + filterClamp[1] = 0; + filterClamp[2] = (currentState.sourceFrame.width - 1) / currentState.renderTarget.size.width; + filterClamp[3] = (currentState.sourceFrame.height - 1) / currentState.renderTarget.size.height; + + shader.uniforms.filterClamp = filterClamp; + } + + // TODO Cacheing layer.. + for (const i in uniformData) + { + if (uniformData[i].type === 'sampler2D' && uniforms[i] !== 0) + { + if (uniforms[i].baseTexture) + { + shader.uniforms[i] = this.renderer.bindTexture(uniforms[i].baseTexture, textureCount); + } + else + { + shader.uniforms[i] = textureCount; + + // TODO + // this is helpful as renderTargets can also be set. + // Although thinking about it, we could probably + // make the filter texture cache return a RenderTexture + // rather than a renderTarget + const gl = this.renderer.gl; + + this.renderer.boundTextures[textureCount] = this.renderer.emptyTextures[textureCount]; + gl.activeTexture(gl.TEXTURE0 + textureCount); + + uniforms[i].texture.bind(); + } + + textureCount++; + } + else if (uniformData[i].type === 'mat3') + { + // check if its pixi matrix.. + if (uniforms[i].a !== undefined) + { + shader.uniforms[i] = uniforms[i].toArray(true); + } + else + { + shader.uniforms[i] = uniforms[i]; + } + } + else if (uniformData[i].type === 'vec2') + { + // check if its a point.. + if (uniforms[i].x !== undefined) + { + const val = shader.uniforms[i] || new Float32Array(2); + + val[0] = uniforms[i].x; + val[1] = uniforms[i].y; + shader.uniforms[i] = val; + } + else + { + shader.uniforms[i] = uniforms[i]; + } + } + else if (uniformData[i].type === 'float') + { + if (shader.uniforms.data[i].value !== uniformData[i]) + { + shader.uniforms[i] = uniforms[i]; + } + } + else + { + shader.uniforms[i] = uniforms[i]; + } + } + } + + /** * Gets a render target from the pool, or creates a new one. * * @param {boolean} clear - Should we clear the render texture when we get it? @@ -442,7 +541,6 @@ renderTarget.resolution = resolution; renderTarget.defaultFrame.width = renderTarget.size.width = minWidth / resolution; renderTarget.defaultFrame.height = renderTarget.size.height = minHeight / resolution; - renderTarget.attachStencilBuffer(); return renderTarget; } diff --git a/src/core/settings.js b/src/core/settings.js index bb08cb0..f4dd087 100644 --- a/src/core/settings.js +++ b/src/core/settings.js @@ -77,11 +77,11 @@ * * @static * @memberof PIXI.settings - * @type {RegExp|string} + * @type {RegExp} * @example `@2x` - * @default /@(.+)x/ + * @default /@([0-9\.]+)x/ */ - RETINA_PREFIX: /@(.+)x/, + RETINA_PREFIX: /@([0-9\.]+)x/, /** * The default render options if none are supplied to {@link PIXI.WebGLRenderer} @@ -175,14 +175,24 @@ SCALE_MODE: 0, /** - * Default specify float precision in shaders. + * Default specify float precision in vertex shader. + * + * @static + * @memberof PIXI.settings + * @type {PIXI.PRECISION} + * @default PIXI.PRECISION.HIGH + */ + PRECISION_VERTEX: 'highp', + + /** + * Default specify float precision in fragment shader. * * @static * @memberof PIXI.settings * @type {PIXI.PRECISION} * @default PIXI.PRECISION.MEDIUM */ - PRECISION: 'mediump', + PRECISION_FRAGMENT: 'mediump', /** * Can we upload the same buffer in a single frame? diff --git a/src/core/sprites/Sprite.js b/src/core/sprites/Sprite.js index 28b0b18..121bf58 100644 --- a/src/core/sprites/Sprite.js +++ b/src/core/sprites/Sprite.js @@ -212,11 +212,11 @@ } else { - w0 = orig.width * (1 - anchor._x); - w1 = orig.width * -anchor._x; + w1 = -anchor._x * orig.width; + w0 = w1 + orig.width; - h0 = orig.height * (1 - anchor._y); - h1 = orig.height * -anchor._y; + h1 = -anchor._y * orig.height; + h0 = h1 + orig.height; } // xy @@ -269,11 +269,11 @@ const tx = wt.tx; const ty = wt.ty; - const w0 = (orig.width) * (1 - anchor._x); - const w1 = (orig.width) * -anchor._x; + const w1 = -anchor._x * orig.width; + const w0 = w1 + orig.width; - const h0 = orig.height * (1 - anchor._y); - const h1 = orig.height * -anchor._y; + const h1 = -anchor._y * orig.height; + const h0 = h1 + orig.height; // xy vertexData[0] = (a * w1) + (c * h1) + tx; @@ -440,7 +440,7 @@ * * @static * @param {number|string|PIXI.BaseTexture|HTMLCanvasElement|HTMLVideoElement} source Source to create texture from - * @return {PIXI.Sprite} The newly created texture + * @return {PIXI.Sprite} The newly created sprite */ static from(source) { @@ -538,8 +538,8 @@ } /** - * The tint applied to the sprite. This is a hex value. A value of - * 0xFFFFFF will remove any tint effect. + * The tint applied to the sprite. This is a hex value. + * A value of 0xFFFFFF will remove any tint effect. * * @member {number} * @default 0xFFFFFF diff --git a/src/core/sprites/canvas/CanvasTinter.js b/src/core/sprites/canvas/CanvasTinter.js index 1cf316b..27e5e0c 100644 --- a/src/core/sprites/canvas/CanvasTinter.js +++ b/src/core/sprites/canvas/CanvasTinter.js @@ -75,8 +75,8 @@ crop.width *= resolution; crop.height *= resolution; - canvas.width = crop.width; - canvas.height = crop.height; + canvas.width = Math.ceil(crop.width); + canvas.height = Math.ceil(crop.height); context.fillStyle = `#${(`00000${(color | 0).toString(16)}`).substr(-6)}`; @@ -130,8 +130,8 @@ crop.width *= resolution; crop.height *= resolution; - canvas.width = crop.width; - canvas.height = crop.height; + canvas.width = Math.ceil(crop.width); + canvas.height = Math.ceil(crop.height); context.globalCompositeOperation = 'copy'; context.fillStyle = `#${(`00000${(color | 0).toString(16)}`).substr(-6)}`; @@ -172,8 +172,8 @@ crop.width *= resolution; crop.height *= resolution; - canvas.width = crop.width; - canvas.height = crop.height; + canvas.width = Math.ceil(crop.width); + canvas.height = Math.ceil(crop.height); context.globalCompositeOperation = 'copy'; context.drawImage( diff --git a/src/core/sprites/webgl/SpriteRenderer.js b/src/core/sprites/webgl/SpriteRenderer.js index 2affcd5..7c352e2 100644 --- a/src/core/sprites/webgl/SpriteRenderer.js +++ b/src/core/sprites/webgl/SpriteRenderer.js @@ -30,7 +30,7 @@ /** * Number of values sent in the vertex buffer. - * positionX, positionY, colorR, colorG, colorB = 5 + * aVertexPosition(2), aTextureCoord(1), aColor(1), aTextureId(1) = 5 * * @member {number} */ @@ -75,7 +75,6 @@ this.shader = null; this.currentIndex = 0; - TICK = 0; this.groups = []; for (let k = 0; k < this.size; k++) diff --git a/src/core/textures/BaseTexture.js b/src/core/textures/BaseTexture.js index 853de7e..aeccf39 100644 --- a/src/core/textures/BaseTexture.js +++ b/src/core/textures/BaseTexture.js @@ -232,16 +232,24 @@ this.realWidth = this.source.naturalWidth || this.source.videoWidth || this.source.width; this.realHeight = this.source.naturalHeight || this.source.videoHeight || this.source.height; - this.width = this.realWidth / this.resolution; - this.height = this.realHeight / this.resolution; - - this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); + this._updateDimensions(); } this.emit('update', this); } /** + * Update dimensions from real values + */ + _updateDimensions() + { + this.width = this.realWidth / this.resolution; + this.height = this.realHeight / this.resolution; + + this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); + } + + /** * Load a source. * * If the source is not-immediately-available, such as an image that needs to be @@ -525,11 +533,7 @@ this.realWidth = Math.round(svgWidth * this.sourceScale); this.realHeight = Math.round(svgHeight * this.sourceScale); - this.width = this.realWidth / this.resolution; - this.height = this.realHeight / this.resolution; - - // Check pow2 after scale - this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); + this._updateDimensions(); // Create a canvas element const canvas = document.createElement('canvas'); @@ -690,4 +694,52 @@ return baseTexture; } + + /** + * Helper function that creates a base texture based on the source you provide. + * The source can be - image url, image element, canvas element. + * + * @static + * @param {string|HTMLImageElement|HTMLCanvasElement} source - The source to create base texture from. + * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values + * @param {number} [sourceScale=(auto)] - Scale for the original image, used with Svg images. + * @return {PIXI.BaseTexture} The new base texture. + */ + static from(source, scaleMode, sourceScale) + { + if (typeof source === 'string') + { + return BaseTexture.fromImage(source, undefined, scaleMode, sourceScale); + } + else if (source instanceof HTMLImageElement) + { + const imageUrl = source.src; + let baseTexture = BaseTextureCache[imageUrl]; + + if (!baseTexture) + { + baseTexture = new BaseTexture(source, scaleMode); + baseTexture.imageUrl = imageUrl; + + if (sourceScale) + { + baseTexture.sourceScale = sourceScale; + } + + // if there is an @2x at the end of the url we are going to assume its a highres image + baseTexture.resolution = getResolutionOfUrl(imageUrl); + + BaseTextureCache[imageUrl] = baseTexture; + } + + return baseTexture; + } + else if (source instanceof HTMLCanvasElement) + { + return BaseTexture.fromCanvas(source, scaleMode); + } + + // lets assume its a base texture! + return source; + } } diff --git a/src/core/textures/Spritesheet.js b/src/core/textures/Spritesheet.js new file mode 100644 index 0000000..d469c62 --- /dev/null +++ b/src/core/textures/Spritesheet.js @@ -0,0 +1,274 @@ +import { Rectangle, Texture } from '../'; +import { getResolutionOfUrl, TextureCache } from '../utils'; + +/** + * Utility class for maintaining reference to a collection + * of Textures on a single Spritesheet. + * + * @class + * @memberof PIXI + */ +export default class Spritesheet +{ + /** + * The maximum number of Textures to build per process. + * + * @type {number} + * @default 1000 + */ + static get BATCH_SIZE() + { + return 1000; + } + + /** + * @param {PIXI.BaseTexture} baseTexture Reference to the source BaseTexture object. + * @param {Object} data - Spritesheet image data. + * @param {string} [resolutionFilename] - The filename to consider when determining + * the resolution of the spritesheet. If not provided, the imageUrl will + * be used on the BaseTexture. + */ + constructor(baseTexture, data, resolutionFilename = null) + { + /** + * Reference to ths source texture + * @type {PIXI.BaseTexture} + */ + this.baseTexture = baseTexture; + + /** + * Map of spritesheet textures. + * @type {Object} + */ + this.textures = {}; + + /** + * Reference to the original JSON data. + * @type {Object} + */ + this.data = data; + + /** + * The resolution of the spritesheet. + * @type {number} + */ + this.resolution = this._updateResolution( + resolutionFilename || this.baseTexture.imageUrl + ); + + /** + * Map of spritesheet frames. + * @type {Object} + * @private + */ + this._frames = this.data.frames; + + /** + * Collection of frame names. + * @type {string[]} + * @private + */ + this._frameKeys = Object.keys(this._frames); + + /** + * Current batch index being processed. + * @type {number} + * @private + */ + this._batchIndex = 0; + + /** + * Callback when parse is completed. + * @type {Function} + * @private + */ + this._callback = null; + } + + /** + * Generate the resolution from the filename or fallback + * to the meta.scale field of the JSON data. + * + * @private + * @param {string} resolutionFilename - The filename to use for resolving + * the default resolution. + * @return {number} Resolution to use for spritesheet. + */ + _updateResolution(resolutionFilename) + { + const scale = this.data.meta.scale; + + // Use a defaultValue of `null` to check if a url-based resolution is set + let resolution = getResolutionOfUrl(resolutionFilename, null); + + // No resolution found via URL + if (resolution === null) + { + // Use the scale value or default to 1 + resolution = scale !== undefined ? parseFloat(scale) : 1; + } + + // For non-1 resolutions, update baseTexture + if (resolution !== 1) + { + this.baseTexture.resolution = resolution; + this.baseTexture.update(); + } + + return resolution; + } + + /** + * Parser spritesheet from loaded data. This is done asynchronously + * to prevent creating too many Texture within a single process. + * + * @param {Function} callback - Callback when complete returns + * a map of the Textures for this spritesheet. + */ + parse(callback) + { + this._batchIndex = 0; + this._callback = callback; + + if (this._frameKeys.length <= Spritesheet.BATCH_SIZE) + { + this._processFrames(0); + this._parseComplete(); + } + else + { + this._nextBatch(); + } + } + + /** + * Process a batch of frames + * + * @private + * @param {number} initialFrameIndex - The index of frame to start. + */ + _processFrames(initialFrameIndex) + { + let frameIndex = initialFrameIndex; + const maxFrames = Spritesheet.BATCH_SIZE; + + while (frameIndex - initialFrameIndex < maxFrames && frameIndex < this._frameKeys.length) + { + const i = this._frameKeys[frameIndex]; + const rect = this._frames[i].frame; + + if (rect) + { + let frame = null; + let trim = null; + const orig = new Rectangle( + 0, + 0, + this._frames[i].sourceSize.w / this.resolution, + this._frames[i].sourceSize.h / this.resolution + ); + + if (this._frames[i].rotated) + { + frame = new Rectangle( + rect.x / this.resolution, + rect.y / this.resolution, + rect.h / this.resolution, + rect.w / this.resolution + ); + } + else + { + frame = new Rectangle( + rect.x / this.resolution, + rect.y / this.resolution, + rect.w / this.resolution, + rect.h / this.resolution + ); + } + + // Check to see if the sprite is trimmed + if (this._frames[i].trimmed) + { + trim = new Rectangle( + this._frames[i].spriteSourceSize.x / this.resolution, + this._frames[i].spriteSourceSize.y / this.resolution, + rect.w / this.resolution, + rect.h / this.resolution + ); + } + + this.textures[i] = new Texture( + this.baseTexture, + frame, + orig, + trim, + this._frames[i].rotated ? 2 : 0 + ); + + // lets also add the frame to pixi's global cache for fromFrame and fromImage functions + TextureCache[i] = this.textures[i]; + } + + frameIndex++; + } + } + + /** + * The parse has completed. + * + * @private + */ + _parseComplete() + { + const callback = this._callback; + + this._callback = null; + this._batchIndex = 0; + callback.call(this, this.textures); + } + + /** + * Begin the next batch of textures. + * + * @private + */ + _nextBatch() + { + this._processFrames(this._batchIndex * Spritesheet.BATCH_SIZE); + this._batchIndex++; + setTimeout(() => + { + if (this._batchIndex * Spritesheet.BATCH_SIZE < this._frameKeys.length) + { + this._nextBatch(); + } + else + { + this._parseComplete(); + } + }, 0); + } + + /** + * Destroy Spritesheet and don't use after this. + * + * @param {boolean} [destroyBase=false] Whether to destroy the base texture as well + */ + destroy(destroyBase = false) + { + for (const i in this.textures) + { + this.textures[i].destroy(); + } + this._frames = null; + this._frameKeys = null; + this.data = null; + this.textures = null; + if (destroyBase) + { + this.baseTexture.destroy(); + } + this.baseTexture = null; + } +} diff --git a/src/core/textures/Texture.js b/src/core/textures/Texture.js index 6403d4f..afe4254 100644 --- a/src/core/textures/Texture.js +++ b/src/core/textures/Texture.js @@ -3,7 +3,7 @@ import TextureUvs from './TextureUvs'; import EventEmitter from 'eventemitter3'; import { Rectangle } from '../math'; -import { TextureCache, BaseTextureCache } from '../utils'; +import { TextureCache, BaseTextureCache, getResolutionOfUrl } from '../utils'; /** * A texture stores the information that represents an image or part of an image. It cannot be added @@ -365,7 +365,8 @@ * The source can be - frame id, image url, video url, canvas element, video element, base texture * * @static - * @param {number|string|PIXI.BaseTexture|HTMLCanvasElement|HTMLVideoElement} source - Source to create texture from + * @param {number|string|HTMLImageElement|HTMLCanvasElement|HTMLVideoElement|PIXI.BaseTexture} + * source - Source to create texture from * @return {PIXI.Texture} The newly created texture */ static from(source) @@ -393,7 +394,7 @@ } else if (source instanceof HTMLImageElement) { - return new Texture(new BaseTexture(source)); + return new Texture(BaseTexture.from(source)); } else if (source instanceof HTMLCanvasElement) { @@ -413,6 +414,43 @@ } /** + * Create a texture from a source and add to the cache. + * + * @static + * @param {HTMLImageElement|HTMLCanvasElement} source - The input source. + * @param {String} imageUrl - File name of texture, for cache and resolving resolution. + * @param {String} [name] - Human readible name for the texture cache. If no name is + * specified, only `imageUrl` will be used as the cache ID. + * @return {PIXI.Texture} Output texture + */ + static fromLoader(source, imageUrl, name) + { + const baseTexture = new BaseTexture(source, null, getResolutionOfUrl(imageUrl)); + const texture = new Texture(baseTexture); + + baseTexture.imageUrl = imageUrl; + + // No name, use imageUrl instead + if (!name) + { + name = imageUrl; + } + + // lets also add the frame to pixi's global cache for fromFrame and fromImage fucntions + BaseTextureCache[name] = baseTexture; + TextureCache[name] = texture; + + // also add references by url if they are different. + if (name !== imageUrl) + { + BaseTextureCache[imageUrl] = baseTexture; + TextureCache[imageUrl] = texture; + } + + return texture; + } + + /** * Adds a texture to the global TextureCache. This cache is shared across the whole PIXI object. * * @static @@ -522,6 +560,29 @@ } } +function createWhiteTexture() +{ + const canvas = document.createElement('canvas'); + + canvas.width = 10; + canvas.height = 10; + + const context = canvas.getContext('2d'); + + context.fillStyle = 'white'; + context.fillRect(0, 0, 10, 10); + + return new Texture(new BaseTexture(canvas)); +} + +function removeAllHandlers(tex) +{ + tex.destroy = function _emptyDestroy() { /* empty */ }; + tex.on = function _emptyOn() { /* empty */ }; + tex.once = function _emptyOnce() { /* empty */ }; + tex.emit = function _emptyEmit() { /* empty */ }; +} + /** * An empty texture, used often to not have to create multiple empty textures. * Can not be destroyed. @@ -530,7 +591,14 @@ * @constant */ Texture.EMPTY = new Texture(new BaseTexture()); -Texture.EMPTY.destroy = function _emptyDestroy() { /* empty */ }; -Texture.EMPTY.on = function _emptyOn() { /* empty */ }; -Texture.EMPTY.once = function _emptyOnce() { /* empty */ }; -Texture.EMPTY.emit = function _emptyEmit() { /* empty */ }; +removeAllHandlers(Texture.EMPTY); + +/** + * A white texture of 10x10 size, used for graphics and other things + * Can not be destroyed. + * + * @static + * @constant + */ +Texture.WHITE = createWhiteTexture(); +removeAllHandlers(Texture.WHITE); diff --git a/src/core/ticker/Ticker.js b/src/core/ticker/Ticker.js index d98a1f0..17a1517 100644 --- a/src/core/ticker/Ticker.js +++ b/src/core/ticker/Ticker.js @@ -67,11 +67,12 @@ * is based, this value is neither capped nor scaled. * If the platform supports DOMHighResTimeStamp, * this value will have a precision of 1 µs. + * Defaults to target frame time * * @member {number} - * @default 1 / TARGET_FPMS + * @default 16.66 */ - this.elapsedMS = 1 / settings.TARGET_FPMS; // default to target frame time + this.elapsedMS = 1 / settings.TARGET_FPMS; /** * The last time {@link PIXI.ticker.Ticker#update} was invoked. diff --git a/src/core/utils/index.js b/src/core/utils/index.js index 6ff609f..be44750 100644 --- a/src/core/utils/index.js +++ b/src/core/utils/index.js @@ -2,6 +2,7 @@ import settings from '../settings'; import EventEmitter from 'eventemitter3'; import pluginTarget from './pluginTarget'; +import * as mixins from './mixin'; import * as isMobile from 'ismobilejs'; let nextUid = 0; @@ -33,6 +34,7 @@ * @type {mixin} */ pluginTarget, + mixins, }; /** @@ -93,7 +95,7 @@ */ export function rgb2hex(rgb) { - return (((rgb[0] * 255) << 16) + ((rgb[1] * 255) << 8) + (rgb[2] * 255)); + return (((rgb[0] * 255) << 16) + ((rgb[1] * 255) << 8) + (rgb[2] * 255 | 0)); } /** @@ -368,3 +370,43 @@ * @private */ export const ProgramCache = {}; + +/** + * Destroys all texture in the cache + * + * @memberof PIXI.utils + * @function destroyTextureCache + */ +export function destroyTextureCache() +{ + let key; + + for (key in TextureCache) + { + TextureCache[key].destroy(); + } + for (key in BaseTextureCache) + { + BaseTextureCache[key].destroy(); + } +} + +/** + * Removes all textures from cache, but does not destroy them + * + * @memberof PIXI.utils + * @function clearTextureCache + */ +export function clearTextureCache() +{ + let key; + + for (key in TextureCache) + { + delete TextureCache[key]; + } + for (key in BaseTextureCache) + { + delete BaseTextureCache[key]; + } +} diff --git a/src/core/utils/mixin.js b/src/core/utils/mixin.js new file mode 100644 index 0000000..114de2a --- /dev/null +++ b/src/core/utils/mixin.js @@ -0,0 +1,59 @@ +/** + * Mixes all enumerable properties and methods from a source object to a target object. + * + * @memberof PIXI.utils.mixins + * @function mixin + * @param {object} target The prototype or instance that properties and methods should be added to. + * @param {object} source The source of properties and methods to mix in. + */ +export function mixin(target, source) +{ + if (!target || !source) return; + // in ES8/ES2017, this would be really easy: + // Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); + + // get all the enumerable property keys + const keys = Object.keys(source); + + // loop through properties + for (let i = 0; i < keys.length; ++i) + { + const propertyName = keys[i]; + + // Set the property using the property descriptor - this works for accessors and normal value properties + Object.defineProperty(target, propertyName, Object.getOwnPropertyDescriptor(source, propertyName)); + } +} + +const mixins = []; + +/** + * Queues a mixin to be handled towards the end of the initialization of PIXI, so that deprecation + * can take effect. + * + * @memberof PIXI.utils.mixins + * @function delayMixin + * @private + * @param {object} target The prototype or instance that properties and methods should be added to. + * @param {object} source The source of properties and methods to mix in. + */ +export function delayMixin(target, source) +{ + mixins.push(target, source); +} + +/** + * Handles all mixins queued via delayMixin(). + * + * @memberof PIXI.utils.mixins + * @function performMixins + * @private + */ +export function performMixins() +{ + for (let i = 0; i < mixins.length; i += 2) + { + mixin(mixins[i], mixins[i + 1]); + } + mixins.length = 0; +} diff --git a/src/deprecation.js b/src/deprecation.js index 045a19f..b9c73b3 100644 --- a/src/deprecation.js +++ b/src/deprecation.js @@ -5,6 +5,7 @@ import * as filters from './filters'; import * as prepare from './prepare'; import * as loaders from './loaders'; +import * as interaction from './interaction'; // provide method to give a stack track for warnings // useful for tracking-down where deprecated methods/properties/classes @@ -534,7 +535,7 @@ { parent: 'GC_MODES', target: 'GC_MODE' }, { parent: 'WRAP_MODES', target: 'WRAP_MODE' }, { parent: 'SCALE_MODES', target: 'SCALE_MODE' }, - { parent: 'PRECISION', target: 'PRECISION' }, + { parent: 'PRECISION', target: 'PRECISION_FRAGMENT' }, ]; for (let i = 0; i < defaults.length; i++) @@ -558,6 +559,32 @@ }); } +Object.defineProperties(core.settings, { + + /** + * @static + * @name PRECISION + * @memberof PIXI.settings + * @see PIXI.PRECISION + * @deprecated since version 4.4.0 + */ + PRECISION: { + enumerable: true, + get() + { + warn('PIXI.settings.PRECISION has been deprecated, please use PIXI.settings.PRECISION_FRAGMENT'); + + return core.settings.PRECISION_FRAGMENT; + }, + set(value) + { + warn('PIXI.settings.PRECISION has been deprecated, please use PIXI.settings.PRECISION_FRAGMENT'); + + core.settings.PRECISION_FRAGMENT = value; + }, + }, +}); + Object.defineProperties(extras, { /** @@ -694,7 +721,7 @@ warn('determineFontProperties is now deprecated, please use the static calculateFontProperties method, ' + 'e.g : Text.calculateFontProperties(fontStyle);'); - return Text.calculateFontProperties(fontStyle); + return core.Text.calculateFontProperties(fontStyle); }; Object.defineProperties(core.TextStyle.prototype, { @@ -1003,3 +1030,69 @@ }, }, }); + +/** + * @name PIXI.interaction.interactiveTarget#defaultCursor + * @static + * @type {number} + * @see PIXI.interaction.interactiveTarget#cursor + * @deprecated since 4.3.0 + */ +Object.defineProperty(interaction.interactiveTarget, 'defaultCursor', { + set(value) + { + warn('Property defaultCursor has been replaced with \'cursor\'. '); + this.cursor = value; + }, + get() + { + warn('Property defaultCursor has been replaced with \'cursor\'. '); + + return this.cursor; + }, + enumerable: true, +}); + +/** + * @name PIXI.interaction.InteractionManager#defaultCursorStyle + * @static + * @type {string} + * @see PIXI.interaction.InteractionManager#cursorStyles + * @deprecated since 4.3.0 + */ +Object.defineProperty(interaction.InteractionManager, 'defaultCursorStyle', { + set(value) + { + warn('Property defaultCursorStyle has been replaced with \'cursorStyles.default\'. '); + this.cursorStyles.default = value; + }, + get() + { + warn('Property defaultCursorStyle has been replaced with \'cursorStyles.default\'. '); + + return this.cursorStyles.default; + }, +}); + +/** + * @name PIXI.interaction.InteractionManager#currentCursorStyle + * @static + * @type {string} + * @see PIXI.interaction.InteractionManager#cursorStyles + * @deprecated since 4.3.0 + */ +Object.defineProperty(interaction.InteractionManager, 'currentCursorStyle', { + set(value) + { + warn('Property currentCursorStyle has been removed.' + + 'See the currentCursorMode property, which works differently.'); + this.currentCursorMode = value; + }, + get() + { + warn('Property currentCursorStyle has been removed.' + + 'See the currentCursorMode property, which works differently.'); + + return this.currentCursorMode; + }, +}); diff --git a/src/extras/BitmapText.js b/src/extras/BitmapText.js index fb6c343..0a0e150 100644 --- a/src/extras/BitmapText.js +++ b/src/extras/BitmapText.js @@ -455,6 +455,73 @@ return this._textHeight; } + + /** + * Register a bitmap font with data and a texture. + * + * @static + * @param {XMLDocument} xml - The XML document data. + * @param {PIXI.Texture} texture - Texture with all symbols. + * @return {Object} Result font object with font, size, lineHeight and char fields. + */ + static registerFont(xml, texture) + { + const data = {}; + const info = xml.getElementsByTagName('info')[0]; + const common = xml.getElementsByTagName('common')[0]; + + data.font = info.getAttribute('face'); + data.size = parseInt(info.getAttribute('size'), 10); + data.lineHeight = parseInt(common.getAttribute('lineHeight'), 10); + data.chars = {}; + + // parse letters + const letters = xml.getElementsByTagName('char'); + + for (let i = 0; i < letters.length; i++) + { + const letter = letters[i]; + const charCode = parseInt(letter.getAttribute('id'), 10); + + const textureRect = new core.Rectangle( + parseInt(letter.getAttribute('x'), 10) + texture.frame.x, + parseInt(letter.getAttribute('y'), 10) + texture.frame.y, + parseInt(letter.getAttribute('width'), 10), + parseInt(letter.getAttribute('height'), 10) + ); + + data.chars[charCode] = { + xOffset: parseInt(letter.getAttribute('xoffset'), 10), + yOffset: parseInt(letter.getAttribute('yoffset'), 10), + xAdvance: parseInt(letter.getAttribute('xadvance'), 10), + kerning: {}, + texture: new core.Texture(texture.baseTexture, textureRect), + + }; + } + + // parse kernings + const kernings = xml.getElementsByTagName('kerning'); + + for (let i = 0; i < kernings.length; i++) + { + const kerning = kernings[i]; + const first = parseInt(kerning.getAttribute('first'), 10); + const second = parseInt(kerning.getAttribute('second'), 10); + const amount = parseInt(kerning.getAttribute('amount'), 10); + + if (data.chars[second]) + { + data.chars[second].kerning[first] = amount; + } + } + + // I'm leaving this as a temporary fix so we can test the bitmap fonts in v3 + // but it's very likely to change + BitmapText.fonts[data.font] = data; + + return data; + } } BitmapText.fonts = {}; diff --git a/src/index.js b/src/index.js index 59356cf..c553427 100644 --- a/src/index.js +++ b/src/index.js @@ -16,6 +16,10 @@ import * as particles from './particles'; import * as prepare from './prepare'; +// handle mixins now, after all code has been added, including deprecation +import { utils } from './core'; +utils.mixins.performMixins(); + export { accessibility, extract, diff --git a/src/interaction/InteractionManager.js b/src/interaction/InteractionManager.js index 7d51134..8baf13a 100644 --- a/src/interaction/InteractionManager.js +++ b/src/interaction/InteractionManager.js @@ -1,18 +1,20 @@ 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'; -// Mix interactiveTarget into core.DisplayObject.prototype -Object.assign( +// Mix interactiveTarget into core.DisplayObject.prototype, after deprecation has been handled +core.utils.mixins.delayMixin( core.DisplayObject.prototype, 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 +70,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 +101,6 @@ this.eventData = new InteractionEvent(); /** - * Tiny little interactiveData pool ! - * - * @member {PIXI.interaction.InteractionData[]} - */ - this.interactiveDataPool = []; - - /** * The DOM element to bind to. * * @private @@ -153,66 +155,12 @@ */ this.supportsPointerEvents = !!window.PointerEvent; - /** - * Are touch events being 'normalized' and converted into pointer events if pointer events are not supported - * For example, on a touch screen mobile device, a touchstart would also be emitted as a pointerdown - * - * @private - * @readonly - * @member {boolean} - */ - this.normalizeTouchEvents = !this.supportsPointerEvents && this.supportsTouchEvents; - - /** - * Are mouse events being 'normalized' and converted into pointer events if pointer events are not supported - * For example, on a desktop pc, a mousedown would also be emitted as a pointerdown - * - * @private - * @readonly - * @member {boolean} - */ - this.normalizeMouseEvents = !this.supportsPointerEvents && !MobileDevice.any; - // this will make it so that you don't have to call bind all the time /** * @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 +168,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,41 +199,32 @@ this.onPointerOver = this.onPointerOver.bind(this); /** - * @private - * @member {Function} + * Dictionary of how different cursor modes are handled. Strings are handled as CSS cursor + * values, objects are handled as dictionaries of CSS values for interactionDOMElement, + * and functions are called instead of changing the CSS. + * Default CSS cursor values are provided for 'default' and 'pointer' modes. + * @member {Object.)>} */ - this.onTouchStart = this.onTouchStart.bind(this); - this.processTouchStart = this.processTouchStart.bind(this); + this.cursorStyles = { + default: 'inherit', + pointer: 'pointer', + }; /** - * @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. - * - * @member {string} - * @default 'inherit' - */ - this.defaultCursorStyle = 'inherit'; - - /** - * The css style of the cursor that is being used. + * The mode of the cursor that is being used. + * The value of this is a key from the cursorStyles dictionary. * * @member {string} */ - this.currentCursorStyle = 'inherit'; + this.currentCursorMode = null; + + /** + * Internal cached let. + * + * @private + * @member {string} + */ + this.cursor = null; /** * Internal cached let. @@ -303,6 +249,7 @@ * object. * * @event mousedown + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -311,6 +258,7 @@ * on the display object. * * @event rightdown + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -319,6 +267,7 @@ * object. * * @event mouseup + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -327,6 +276,7 @@ * over the display object. * * @event rightup + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -335,6 +285,7 @@ * the display object. * * @event click + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -343,6 +294,7 @@ * and released on the display object. * * @event rightclick + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -352,6 +304,7 @@ * [mousedown]{@link PIXI.interaction.InteractionManager#event:mousedown}. * * @event mouseupoutside + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -361,6 +314,7 @@ * [rightdown]{@link PIXI.interaction.InteractionManager#event:rightdown}. * * @event rightupoutside + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -368,6 +322,7 @@ * Fired when a pointer device (usually a mouse) is moved while over the display object * * @event mousemove + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -375,6 +330,7 @@ * Fired when a pointer device (usually a mouse) is moved onto the display object * * @event mouseover + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -382,6 +338,7 @@ * Fired when a pointer device (usually a mouse) is moved off the display object * * @event mouseout + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -389,6 +346,7 @@ * Fired when a pointer device button is pressed on the display object. * * @event pointerdown + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -396,6 +354,14 @@ * Fired when a pointer device button is released over the display object. * * @event pointerup + * @type {PIXI.interaction.InteractionData} + * @memberof PIXI.interaction.InteractionManager# + */ + + /** + * Fired when the operating system cancels a pointer event + * + * @event pointercancel * @memberof PIXI.interaction.InteractionManager# */ @@ -403,6 +369,7 @@ * Fired when a pointer device button is pressed and released on the display object. * * @event pointertap + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -411,6 +378,7 @@ * registered a [pointerdown]{@link PIXI.interaction.InteractionManager#event:pointerdown}. * * @event pointerupoutside + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -418,6 +386,7 @@ * Fired when a pointer device is moved while over the display object * * @event pointermove + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -425,6 +394,7 @@ * Fired when a pointer device is moved onto the display object * * @event pointerover + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -432,6 +402,7 @@ * Fired when a pointer device is moved off the display object * * @event pointerout + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -439,6 +410,7 @@ * Fired when a touch point is placed on the display object. * * @event touchstart + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -446,6 +418,14 @@ * Fired when a touch point is removed from the display object. * * @event touchend + * @type {PIXI.interaction.InteractionData} + * @memberof PIXI.interaction.InteractionManager# + */ + + /** + * Fired when the operating system cancels a touch + * + * @event touchcancel * @memberof PIXI.interaction.InteractionManager# */ @@ -453,6 +433,7 @@ * Fired when a touch point is placed and removed from the display object. * * @event tap + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -461,6 +442,7 @@ * registered a [touchstart]{@link PIXI.interaction.InteractionManager#event:touchstart}. * * @event touchendoutside + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -468,6 +450,7 @@ * Fired when a touch point is moved along the display object. * * @event touchmove + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ } @@ -524,45 +507,30 @@ { window.document.addEventListener('pointermove', this.onPointerMove, true); this.interactionDOMElement.addEventListener('pointerdown', this.onPointerDown, true); - this.interactionDOMElement.addEventListener('pointerout', this.onPointerOut, true); + // pointerout is fired in addition to pointerup (for touch events) and pointercancel + // we already handle those, so for the purposes of what we do in onPointerOut, we only + // care about the pointerleave event + this.interactionDOMElement.addEventListener('pointerleave', 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; @@ -596,45 +564,26 @@ { window.document.removeEventListener('pointermove', this.onPointerMove, true); this.interactionDOMElement.removeEventListener('pointerdown', this.onPointerDown, true); - this.interactionDOMElement.removeEventListener('pointerout', this.onPointerOut, true); + this.interactionDOMElement.removeEventListener('pointerleave', 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) + 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.onPointerDown, true); + this.interactionDOMElement.removeEventListener('touchcancel', this.onPointerCancel, 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); - - 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 = null; @@ -664,7 +613,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; @@ -672,25 +621,80 @@ return; } - this.cursor = this.defaultCursorStyle; + this.cursor = null; // 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(); - - this.processInteractive(this.mouse.global, this.renderer._lastObjectRendered, this.processMouseOverOut, true); - - if (this.currentCursorStyle !== this.cursor) + for (const k in this.activeInteractionData) { - this.currentCursorStyle = this.cursor; - this.interactionDOMElement.style.cursor = this.cursor; + // eslint-disable-next-line no-prototype-builtins + if (this.activeInteractionData.hasOwnProperty(k)) + { + const interactionData = this.activeInteractionData[k]; + + if (interactionData.originalEvent && interactionData.pointerType !== 'touch') + { + const interactionEvent = this.configureInteractionEventForDOMEvent( + this.eventData, + interactionData.originalEvent, + interactionData + ); + + this.processInteractive( + interactionEvent, + this.renderer._lastObjectRendered, + this.processPointerOverOut, + true + ); + } + } } + this.setCursorMode(this.cursor); + // TODO } /** + * Sets the current cursor mode, handling any callbacks or CSS style changes. + * + * @param {string} mode - cursor mode, a key from the cursorStyles dictionary + */ + setCursorMode(mode) + { + mode = mode || 'default'; + // if the mode didn't actually change, bail early + if (this.currentCursorMode === mode) + { + return; + } + this.currentCursorMode = mode; + const style = this.cursorStyles[mode]; + + // only do things if there is a cursor style for it + if (style) + { + switch (typeof style) + { + case 'string': + // string styles are handled as cursor CSS + this.interactionDOMElement.style.cursor = style; + break; + case 'function': + // functions are just called, and passed the cursor mode + style(mode); + break; + case 'object': + // if it is an object, assume that it is a dictionary of CSS styles, + // apply it to the interactionDOMElement + Object.assign(this.interactionDOMElement.style, style); + break; + } + } + } + + /** * Dispatches an event on the display object that was interacted with * * @param {PIXI.Container|PIXI.Sprite|PIXI.extras.TilingSprite} displayObject - the display object in question @@ -748,22 +752,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 @@ -787,9 +795,8 @@ { interactiveParent = false; } - - // it has a mask! Then lets hit test that before continuing.. - if (hitTest && displayObject._mask) + // it has a mask! Then lets hit test that before continuing + else if (hitTest && displayObject._mask) { if (!displayObject._mask.containsPoint(point)) { @@ -797,14 +804,7 @@ } } - // it has a filterArea! Same as mask but easier, its a rectangle - if (hitTest && displayObject.filterArea) - { - if (!displayObject.filterArea.contains(point.x, point.y)) - { - hitTest = false; - } - } + let keepHitTestingAfterChildren = hitTest; // ** FREE TIP **! If an object is not interactive or has no buttons in it // (such as a game scene!) set interactiveChildren to false for that displayObject. @@ -818,7 +818,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 @@ -838,15 +838,20 @@ // This means we no longer need to hit test anything else. We still need to run // through all objects, but we don't need to perform any hit tests. - // { - hitTest = false; - // } + keepHitTestingAfterChildren = false; + + if (child.interactive) + { + hitTest = false; + } // we can break now as we have hit an object. } } } + hitTest = keepHitTestingAfterChildren; + // no point running this if the item is not interactive or does not have an interactive parent. if (interactive) { @@ -868,14 +873,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 +886,175 @@ } /** - * 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].isNormalized) { - 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); + if (!displayObject.trackedPointers[id]) + { + displayObject.trackedPointers[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.trackedPointers[id].rightDown = true; + } + else + { + displayObject.trackedPointers[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.trackedPointers[id] !== undefined) + { + delete displayObject.trackedPointers[id]; + this.dispatchEvent(displayObject, 'pointercancel', interactionEvent); + + if (e.type === 'touchcancel' || e.pointerType === 'touch') + { + this.dispatchEvent(displayObject, 'touchcancel', interactionEvent); + } } } @@ -1159,42 +1066,94 @@ */ 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) { - if (hit) - { - this.dispatchEvent(displayObject, 'pointerup', this.eventData); + const e = interactionEvent.data.originalEvent; - if (displayObject._pointerDown) + const id = interactionEvent.data.identifier; + + const trackingData = displayObject.trackedPointers[id]; + + const isTouch = (e.type === 'touchend' || e.pointerType === 'touch'); + + const isMouse = (e.type.indexOf('mouse') === 0 || e.pointerType === 'mouse'); + + // 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) { - displayObject._pointerDown = false; - this.dispatchEvent(displayObject, 'pointertap', this.eventData); + 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); + } + // update the down state of the tracking data + if (trackingData) + { + if (isRightButton) + { + trackingData.rightDown = hit; + } + else + { + trackingData.leftDown = hit; + } } } - else if (displayObject._pointerDown) + + // Pointers and Touches, and Mouse + if (hit) { - displayObject._pointerDown = false; - this.dispatchEvent(displayObject, 'pointerupoutside', this.eventData); + this.dispatchEvent(displayObject, 'pointerup', interactionEvent); + if (isTouch) this.dispatchEvent(displayObject, 'touchend', interactionEvent); + + if (trackingData) + { + this.dispatchEvent(displayObject, 'pointertap', interactionEvent); + if (isTouch) + { + this.dispatchEvent(displayObject, 'tap', interactionEvent); + // touches are no longer over (if they ever were) when we get the touchend + // so we should ensure that we don't keep pretending that they are + trackingData.over = false; + } + } + } + else if (trackingData) + { + this.dispatchEvent(displayObject, 'pointerupoutside', interactionEvent); + if (isTouch) this.dispatchEvent(displayObject, 'touchendoutside', interactionEvent); + } + // Only remove the tracking data if there is no over/down state still associated with it + if (trackingData && trackingData.none) + { + delete displayObject.trackedPointers[id]; } } @@ -1202,39 +1161,78 @@ * 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 = null; + } - 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') + { + this.setCursorMode(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 (isMouse) { - 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 +1240,94 @@ * 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.setCursorMode(null); + } - 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'); + + let trackingData = displayObject.trackedPointers[id]; + + // if we just moused over the display object, then we need to track that state + if (hit && !trackingData) + { + trackingData = displayObject.trackedPointers[id] = new InteractionTrackingData(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); + } + } + + // only change the cursor if it has not already been changed (by something deeper in the + // display tree) + if (isMouse && this.cursor === null) + { + this.cursor = displayObject.cursor; } } - 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); + } + // if there is no mouse down information for the pointer, then it is safe to delete + if (trackingData.none) + { + delete displayObject.trackedPointers[id]; + } } } @@ -1287,253 +1335,154 @@ * 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 && pointerEvent.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; + + // mark the touch as normalized, just so that we know we did it + touch.isNormalized = true; + + normalizedEvents.push(touch); + } } - else if (this.normalizeMouseEvents) + // apparently PointerEvent subclasses MouseEvent, so yay + else if (event instanceof MouseEvent && (!this.supportsPointerEvents || !(event instanceof window.PointerEvent))) { if (typeof event.isPrimary === 'undefined') event.isPrimary = true; if (typeof event.width === 'undefined') event.width = 1; @@ -1541,10 +1490,21 @@ 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; + + // mark the mouse event as normalized, just so that we know we did it + event.isNormalized = true; + + normalizedEvents.push(event); } + else + { + normalizedEvents.push(event); + } + + return normalizedEvents; } /** @@ -1563,30 +1523,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 +1542,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/package.json b/package.json index 6a366ba..71ade15 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pixi.js", - "version": "4.3.4", + "version": "4.3.5", "description": "Pixi.js is a fast lightweight 2D library that works across all devices.", "author": "Mat Groves", "contributors": [ @@ -61,7 +61,7 @@ "ismobilejs": "^0.4.0", "object-assign": "^4.0.1", "pixi-gl-core": "^1.0.3", - "resource-loader": "^2.0.4" + "resource-loader": "^2.0.6" }, "devDependencies": { "babel-cli": "^6.18.0", @@ -70,9 +70,9 @@ "babel-preset-es2015": "^6.14.0", "babelify": "^7.3.0", "del": "^2.2.0", - "electron-prebuilt": "^1.3.2", + "electron": "^1.4.15", "eslint": "^3.5.0", - "floss": "^1.2.0", + "floss": "^2.0.1", "gh-pages": "^0.11.0", "jaguarjs-jsdoc": "^1.0.1", "js-md5": "^0.4.1", diff --git a/src/accessibility/AccessibilityManager.js b/src/accessibility/AccessibilityManager.js index df2716a..f16bcb1 100644 --- a/src/accessibility/AccessibilityManager.js +++ b/src/accessibility/AccessibilityManager.js @@ -3,7 +3,7 @@ import accessibleTarget from './accessibleTarget'; // add some extra variables to the container.. -Object.assign( +core.utils.mixins.delayMixin( core.DisplayObject.prototype, accessibleTarget ); diff --git a/src/core/Application.js b/src/core/Application.js index ea0f058..5340578 100644 --- a/src/core/Application.js +++ b/src/core/Application.js @@ -1,6 +1,6 @@ import { autoDetectRenderer } from './autoDetectRenderer'; import Container from './display/Container'; -import Ticker from './ticker/Ticker'; +import { shared, Ticker } from './ticker'; /** * Convenience class to create a new PIXI application. @@ -35,9 +35,9 @@ * @param {boolean} [noWebGL=false] - prevents selection of WebGL renderer, even if such is present * @param {boolean} [options.legacy=false] - If true Pixi will aim to ensure compatibility * with older / less advanced devices. If you experience unexplained flickering try setting this to true. - + * @param {boolean} [useSharedTicker=false] - `true` to use PIXI.ticker.shared, `false` to create new ticker. */ - constructor(width, height, options, noWebGL) + constructor(width, height, options, noWebGL, useSharedTicker = false) { /** * WebGL renderer if available, otherwise CanvasRenderer @@ -46,23 +46,46 @@ this.renderer = autoDetectRenderer(width, height, options, noWebGL); /** - * The root display container that's renderered. + * The root display container that's rendered. * @member {PIXI.Container} */ this.stage = new Container(); /** + * Internal reference to the ticker + * @member {PIXI.ticker.Ticker} + * @private + */ + this._ticker = null; + + /** * Ticker for doing render updates. * @member {PIXI.ticker.Ticker} + * @default PIXI.ticker.shared */ - this.ticker = new Ticker(); - - this.ticker.add(this.render, this); + this.ticker = useSharedTicker ? shared : new Ticker(); // Start the rendering this.start(); } + set ticker(ticker) // eslint-disable-line require-jsdoc + { + if (this._ticker) + { + this._ticker.remove(this.render, this); + } + this._ticker = ticker; + if (ticker) + { + ticker.add(this.render, this); + } + } + get ticker() // eslint-disable-line require-jsdoc + { + return this._ticker; + } + /** * Render the current stage. */ @@ -76,7 +99,7 @@ */ stop() { - this.ticker.stop(); + this._ticker.stop(); } /** @@ -84,7 +107,7 @@ */ start() { - this.ticker.start(); + this._ticker.start(); } /** @@ -98,13 +121,22 @@ } /** + * Reference to the renderer's screen rectangle. Its safe to use as filterArea or hitArea for whole screen + * @member {PIXI.Rectangle} + * @readonly + */ + get screen() + { + return this.renderer.screen; + } + + /** * Destroy and don't use after this. * @param {Boolean} [removeView=false] Automatically remove canvas from DOM. */ destroy(removeView) { this.stop(); - this.ticker.remove(this.render, this); this.ticker = null; this.stage.destroy(); diff --git a/src/core/Shader.js b/src/core/Shader.js new file mode 100644 index 0000000..78b17e6 --- /dev/null +++ b/src/core/Shader.js @@ -0,0 +1,46 @@ +import { GLShader } from 'pixi-gl-core'; +import settings from './settings'; + +function checkPrecision(src, def) +{ + if (src instanceof Array) + { + if (src[0].substring(0, 9) !== 'precision') + { + const copy = src.slice(0); + + copy.unshift(`precision ${def} float;`); + + return copy; + } + } + else if (src.substring(0, 9) !== 'precision') + { + return `precision ${def} float;\n${src}`; + } + + return src; +} + +/** + * Wrapper class, webGL Shader for Pixi. + * Adds precision string if vertexSrc or fragmentSrc have no mention of it. + * + * @class + * @extends GLShader + * @memberof PIXI + */ +export default class Shader extends GLShader +{ + /** + * + * @param {WebGLRenderingContext} gl - The current WebGL rendering context + * @param {string|string[]} vertexSrc - The vertex shader source as an array of strings. + * @param {string|string[]} fragmentSrc - The fragment shader source as an array of strings. + */ + constructor(gl, vertexSrc, fragmentSrc) + { + super(gl, checkPrecision(vertexSrc, settings.PRECISION_VERTEX), + checkPrecision(fragmentSrc, settings.PRECISION_FRAGMENT)); + } +} diff --git a/src/core/const.js b/src/core/const.js index ef6f662..3de0efe 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -188,7 +188,7 @@ * The gc modes that are supported by pixi. * * The {@link PIXI.settings.GC_MODE} Garbage Collection mode for pixi textures is AUTO - * If set to GC_MODE, the renderer will occasianally check textures usage. If they are not + * If set to GC_MODE, the renderer will occasionally check textures usage. If they are not * used for a specified period of time they will be removed from the GPU. They will of course * be uploaded again when they are required. This is a silent behind the scenes process that * should ensure that the GPU does not get filled up. diff --git a/src/core/display/Container.js b/src/core/display/Container.js index ed8cd5f..17fde8d 100644 --- a/src/core/display/Container.js +++ b/src/core/display/Container.js @@ -73,11 +73,12 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.push(child); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -108,11 +109,12 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.splice(index, 0, child); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -225,10 +227,11 @@ if (index === -1) return null; child.parent = null; + // ensure child transform will be recalculated + child.transform._parentID = -1; removeItems(this.children, index, 1); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -249,11 +252,12 @@ { const child = this.getChildAt(index); + // ensure child transform will be recalculated.. child.parent = null; + child.transform._parentID = -1; removeItems(this.children, index, 1); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -284,13 +288,12 @@ for (let i = 0; i < removed.length; ++i) { removed[i].parent = null; + if (removed[i].transform) + { + removed[i].transform._parentID = -1; + } } - // ensure a transform will be recalculated.. - if (this.transform) - { - this.transform._parentID = -1; - } this._boundsID++; this.onChildrenChange(beginIndex); diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index f37f2f5..be4c4fb 100644 --- a/src/core/graphics/Graphics.js +++ b/src/core/graphics/Graphics.js @@ -28,8 +28,9 @@ { /** * + * @param {boolean} [nativeLines=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP */ - constructor() + constructor(nativeLines = false) { super(); @@ -50,6 +51,13 @@ this.lineWidth = 0; /** + * If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * + * @member {boolean} + */ + this.nativeLines = nativeLines; + + /** * The color of any lines drawn. * * @member {string} @@ -757,28 +765,14 @@ if (!this._spriteRect) { - if (!Graphics._SPRITE_TEXTURE) - { - Graphics._SPRITE_TEXTURE = RenderTexture.create(10, 10); - - const canvas = document.createElement('canvas'); - - canvas.width = 10; - canvas.height = 10; - - const context = canvas.getContext('2d'); - - context.fillStyle = 'white'; - context.fillRect(0, 0, 10, 10); - - Graphics._SPRITE_TEXTURE = Texture.fromCanvas(canvas); - } - - this._spriteRect = new Sprite(Graphics._SPRITE_TEXTURE); + this._spriteRect = new Sprite(new Texture(Texture.WHITE)); } + + const sprite = this._spriteRect; + if (this.tint === 0xffffff) { - this._spriteRect.tint = this.graphicsData[0].fillColor; + sprite.tint = this.graphicsData[0].fillColor; } else { @@ -792,20 +786,21 @@ t1[1] *= t2[1]; t1[2] *= t2[2]; - this._spriteRect.tint = rgb2hex(t1); + sprite.tint = rgb2hex(t1); } - this._spriteRect.alpha = this.graphicsData[0].fillAlpha; - this._spriteRect.worldAlpha = this.worldAlpha * this._spriteRect.alpha; + sprite.alpha = this.graphicsData[0].fillAlpha; + sprite.worldAlpha = this.worldAlpha * sprite.alpha; + sprite.blendMode = this.blendMode; - Graphics._SPRITE_TEXTURE._frame.width = rect.width; - Graphics._SPRITE_TEXTURE._frame.height = rect.height; + sprite.texture._frame.width = rect.width; + sprite.texture._frame.height = rect.height; - this._spriteRect.transform.worldTransform = this.transform.worldTransform; + sprite.transform.worldTransform = this.transform.worldTransform; - this._spriteRect.anchor.set(-rect.x / rect.width, -rect.y / rect.height); - this._spriteRect._onAnchorUpdate(); + sprite.anchor.set(-rect.x / rect.width, -rect.y / rect.height); + sprite._onAnchorUpdate(); - this._spriteRect._renderWebGL(renderer); + sprite._renderWebGL(renderer); } /** @@ -1031,6 +1026,7 @@ this.fillColor, this.fillAlpha, this.filling, + this.nativeLines, shape ); diff --git a/src/core/graphics/GraphicsData.js b/src/core/graphics/GraphicsData.js index f5a87ae..b6f03c0 100644 --- a/src/core/graphics/GraphicsData.js +++ b/src/core/graphics/GraphicsData.js @@ -14,14 +14,19 @@ * @param {number} fillColor - the color of the fill * @param {number} fillAlpha - the alpha of the fill * @param {boolean} fill - whether or not the shape is filled with a colour + * @param {boolean} nativeLines - the method for drawing lines * @param {PIXI.Circle|PIXI.Rectangle|PIXI.Ellipse|PIXI.Polygon} shape - The shape object to draw. */ - constructor(lineWidth, lineColor, lineAlpha, fillColor, fillAlpha, fill, shape) + constructor(lineWidth, lineColor, lineAlpha, fillColor, fillAlpha, fill, nativeLines, shape) { /** * @member {number} the width of the line to draw */ this.lineWidth = lineWidth; + /** + * @member {boolean} if true the liens will be draw using LINES instead of TRIANGLE_STRIP + */ + this.nativeLines = nativeLines; /** * @member {number} the color of the line to draw @@ -85,6 +90,7 @@ this.fillColor, this.fillAlpha, this.fill, + this.nativeLines, this.shape ); } diff --git a/src/core/graphics/webgl/GraphicsRenderer.js b/src/core/graphics/webgl/GraphicsRenderer.js index 14083fe..67f5e92 100644 --- a/src/core/graphics/webgl/GraphicsRenderer.js +++ b/src/core/graphics/webgl/GraphicsRenderer.js @@ -102,7 +102,15 @@ shaderTemp.uniforms.alpha = graphics.worldAlpha; renderer.bindVao(webGLData.vao); - webGLData.vao.draw(gl.TRIANGLE_STRIP, webGLData.indices.length); + + if (graphics.nativeLines) + { + gl.drawArrays(gl.LINES, 0, webGLData.points.length / 6); + } + else + { + webGLData.vao.draw(gl.TRIANGLE_STRIP, webGLData.indices.length); + } } } diff --git a/src/core/graphics/webgl/utils/buildLine.js b/src/core/graphics/webgl/utils/buildLine.js index ecf43f7..4d6862e 100644 --- a/src/core/graphics/webgl/utils/buildLine.js +++ b/src/core/graphics/webgl/utils/buildLine.js @@ -11,7 +11,29 @@ * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties * @param {object} webGLData - an object containing all the webGL-specific information to create this shape */ -export default function buildLine(graphicsData, webGLData) +export default function (graphicsData, webGLData) +{ + if (graphicsData.nativeLines) + { + buildNativeLine(graphicsData, webGLData); + } + else + { + buildLine(graphicsData, webGLData); + } +} + +/** + * Builds a line to draw using the poligon method. + * + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @private + * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties + * @param {object} webGLData - an object containing all the webGL-specific information to create this shape + */ +function buildLine(graphicsData, webGLData) { // TODO OPTIMISE! let points = graphicsData.points; @@ -224,3 +246,46 @@ indices.push(indexStart - 1); } + +/** + * Builds a line to draw using the gl.drawArrays(gl.LINES) method + * + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @private + * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties + * @param {object} webGLData - an object containing all the webGL-specific information to create this shape + */ +function buildNativeLine(graphicsData, webGLData) +{ + let i = 0; + const points = graphicsData.points; + + if (points.length === 0) return; + + const verts = webGLData.points; + const length = points.length / 2; + + // sort color + const color = hex2rgb(graphicsData.lineColor); + const alpha = graphicsData.lineAlpha; + const r = color[0] * alpha; + const g = color[1] * alpha; + const b = color[2] * alpha; + + for (i = 1; i < length; i++) + { + const p1x = points[(i - 1) * 2]; + const p1y = points[((i - 1) * 2) + 1]; + + const p2x = points[i * 2]; + const p2y = points[(i * 2) + 1]; + + verts.push(p1x, p1y); + verts.push(r, g, b, alpha); + + verts.push(p2x, p2y); + verts.push(r, g, b, alpha); + } +} diff --git a/src/core/graphics/webgl/utils/buildRoundedRectangle.js b/src/core/graphics/webgl/utils/buildRoundedRectangle.js index 9abc807..a6c8461 100644 --- a/src/core/graphics/webgl/utils/buildRoundedRectangle.js +++ b/src/core/graphics/webgl/utils/buildRoundedRectangle.js @@ -77,6 +77,26 @@ } /** + * Calculate a single point for a quadratic bezier curve. + * Utility function used by quadraticBezierCurve. + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @private + * @param {number} n1 - first number + * @param {number} n2 - second number + * @param {number} perc - percentage + * @return {number} the result + * + */ +function getPt(n1, n2, perc) +{ + const diff = n2 - n1; + + return n1 + (diff * perc); +} + +/** * Calculate the points for a quadratic bezier curve. (helper function..) * Based on: https://stackoverflow.com/questions/785097/how-do-i-implement-a-bezier-curve-in-c * @@ -105,13 +125,6 @@ let x = 0; let y = 0; - function getPt(n1, n2, perc) - { - const diff = n2 - n1; - - return n1 + (diff * perc); - } - for (let i = 0, j = 0; i <= n; ++i) { j = i / n; diff --git a/src/core/index.js b/src/core/index.js index f7a817c..4f414e8 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -30,6 +30,7 @@ export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; export { default as CanvasGraphicsRenderer } from './graphics/canvas/CanvasGraphicsRenderer'; +export { default as Spritesheet } from './textures/Spritesheet'; export { default as Texture } from './textures/Texture'; export { default as BaseTexture } from './textures/BaseTexture'; export { default as RenderTexture } from './textures/RenderTexture'; diff --git a/src/core/math/Matrix.js b/src/core/math/Matrix.js index 0b77477..3679860 100644 --- a/src/core/math/Matrix.js +++ b/src/core/math/Matrix.js @@ -13,45 +13,50 @@ export default class Matrix { /** - * + * @param {number} [a=1] - x scale + * @param {number} [b=0] - y skew + * @param {number} [c=0] - x skew + * @param {number} [d=1] - y scale + * @param {number} [tx=0] - x translation + * @param {number} [ty=0] - y translation */ - constructor() + constructor(a = 1, b = 0, c = 0, d = 1, tx = 0, ty = 0) { /** * @member {number} * @default 1 */ - this.a = 1; + this.a = a; /** * @member {number} * @default 0 */ - this.b = 0; + this.b = b; /** * @member {number} * @default 0 */ - this.c = 0; + this.c = c; /** * @member {number} * @default 1 */ - this.d = 1; + this.d = d; /** * @member {number} * @default 0 */ - this.tx = 0; + this.tx = tx; /** * @member {number} * @default 0 */ - this.ty = 0; + this.ty = ty; this.array = null; } diff --git a/src/core/renderers/SystemRenderer.js b/src/core/renderers/SystemRenderer.js index fa116a6..a24c775 100644 --- a/src/core/renderers/SystemRenderer.js +++ b/src/core/renderers/SystemRenderer.js @@ -1,5 +1,5 @@ import { sayHello, hex2string, hex2rgb } from '../utils'; -import { Matrix } from '../math'; +import { Matrix, Rectangle } from '../math'; import { RENDERER_TYPE } from '../const'; import settings from '../settings'; import Container from '../display/Container'; @@ -21,8 +21,8 @@ { /** * @param {string} system - The name of the system this renderer is for. - * @param {number} [width=800] - the width of the canvas view - * @param {number} [height=600] - the height of the canvas view + * @param {number} [screenWidth=800] - the width of the screen + * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false @@ -37,7 +37,7 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(system, width, height, options) + constructor(system, screenWidth, screenHeight, options) { super(); @@ -69,20 +69,13 @@ this.type = RENDERER_TYPE.UNKNOWN; /** - * The width of the canvas view + * Measurements of the screen. (0, 0, screenWidth, screenHeight) * - * @member {number} - * @default 800 - */ - this.width = width || 800; - - /** - * The height of the canvas view + * Its safe to use as filterArea or hitArea for whole stage * - * @member {number} - * @default 600 + * @member {PIXI.Rectangle} */ - this.height = height || 600; + this.screen = new Rectangle(0, 0, screenWidth || 800, screenHeight || 600); /** * The canvas element that everything is drawn to @@ -107,7 +100,7 @@ this.transparent = options.transparent; /** - * Whether the render view should be resized automatically + * Whether css dimensions of canvas view should be resized to screen dimensions automatically * * @member {boolean} */ @@ -192,23 +185,48 @@ } /** - * Resizes the canvas view to the specified width and height + * Same as view.width, actual number of pixels in the canvas by horizontal * - * @param {number} width - the new width of the canvas view - * @param {number} height - the new height of the canvas view + * @member {number} + * @readonly + * @default 800 */ - resize(width, height) + get width() { - this.width = width * this.resolution; - this.height = height * this.resolution; + return this.view.width; + } - this.view.width = this.width; - this.view.height = this.height; + /** + * Same as view.height, actual number of pixels in the canvas by vertical + * + * @member {number} + * @readonly + * @default 600 + */ + get height() + { + return this.view.height; + } + + /** + * Resizes the screen and canvas to the specified width and height + * Canvas dimensions are multiplied by resolution + * + * @param {number} screenWidth - the new width of the screen + * @param {number} screenHeight - the new height of the screen + */ + resize(screenWidth, screenHeight) + { + this.screen.width = screenWidth; + this.screen.height = screenHeight; + + this.view.width = screenWidth * this.resolution; + this.view.height = screenHeight * this.resolution; if (this.autoResize) { - this.view.style.width = `${this.width / this.resolution}px`; - this.view.style.height = `${this.height / this.resolution}px`; + this.view.style.width = `${screenWidth}px`; + this.view.style.height = `${screenHeight}px`; } } @@ -249,11 +267,10 @@ this.type = RENDERER_TYPE.UNKNOWN; - this.width = 0; - this.height = 0; - this.view = null; + this.screen = null; + this.resolution = 0; this.transparent = false; diff --git a/src/core/renderers/canvas/CanvasRenderer.js b/src/core/renderers/canvas/CanvasRenderer.js index 1bd2d5e..e84e49c 100644 --- a/src/core/renderers/canvas/CanvasRenderer.js +++ b/src/core/renderers/canvas/CanvasRenderer.js @@ -18,8 +18,8 @@ export default class CanvasRenderer extends SystemRenderer { /** - * @param {number} [width=800] - the width of the canvas view - * @param {number} [height=600] - the height of the canvas view + * @param {number} [screenWidth=800] - the width of the screen + * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false @@ -34,9 +34,9 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(width, height, options = {}) + constructor(screenWidth, screenHeight, options = {}) { - super('Canvas', width, height, options); + super('Canvas', screenWidth, screenHeight, options); this.type = RENDERER_TYPE.CANVAS; @@ -96,7 +96,7 @@ this.context = null; this.renderingToScreen = false; - this.resize(width, height); + this.resize(screenWidth, screenHeight); } /** @@ -283,12 +283,12 @@ * * @extends PIXI.SystemRenderer#resize * - * @param {number} width - The new width of the canvas view - * @param {number} height - The new height of the canvas view + * @param {number} screenWidth - the new width of the screen + * @param {number} screenHeight - the new height of the screen */ - resize(width, height) + resize(screenWidth, screenHeight) { - super.resize(width, height); + super.resize(screenWidth, screenHeight); // reset the scale mode.. oddly this seems to be reset when the canvas is resized. // surely a browser bug?? Let pixi fix that for you.. diff --git a/src/core/renderers/webgl/TextureManager.js b/src/core/renderers/webgl/TextureManager.js index b685c1a..8227289 100644 --- a/src/core/renderers/webgl/TextureManager.js +++ b/src/core/renderers/webgl/TextureManager.js @@ -61,7 +61,7 @@ * Updates and/or Creates a WebGL texture for the renderer's context. * * @param {PIXI.BaseTexture|PIXI.Texture} texture - the texture to update - * @param {Number} location - the location the texture will be bound to. + * @param {number} location - the location the texture will be bound to. * @return {GLTexture} The gl texture. */ updateTexture(texture, location) diff --git a/src/core/renderers/webgl/WebGLRenderer.js b/src/core/renderers/webgl/WebGLRenderer.js index e4cd8a0..830b382 100644 --- a/src/core/renderers/webgl/WebGLRenderer.js +++ b/src/core/renderers/webgl/WebGLRenderer.js @@ -31,8 +31,8 @@ { /** * - * @param {number} [width=0] - the width of the canvas view - * @param {number} [height=0] - the height of the canvas view + * @param {number} [screenWidth=800] - the width of the screen + * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false @@ -53,9 +53,9 @@ * @param {boolean} [options.legacy=false] - If true Pixi will aim to ensure compatibility * with older / less advanced devices. If you experiance unexplained flickering try setting this to true. */ - constructor(width, height, options = {}) + constructor(screenWidth, screenHeight, options = {}) { - super('WebGL', width, height, options); + super('WebGL', screenWidth, screenHeight, options); this.legacy = !!options.legacy; @@ -245,7 +245,7 @@ glCore._testingContext = gl; // setup the width/height properties and gl viewport - this.resize(this.width, this.height); + this.resize(this.screen.width, this.screen.height); } /** @@ -343,16 +343,16 @@ /** * Resizes the webGL view to the specified width and height. * - * @param {number} width - the new width of the webGL view - * @param {number} height - the new height of the webGL view + * @param {number} screenWidth - the new width of the screen + * @param {number} screenHeight - the new height of the screen */ - resize(width, height) + resize(screenWidth, screenHeight) { // if(width * this.resolution === this.width && height * this.resolution === this.height)return; - SystemRenderer.prototype.resize.call(this, width, height); + SystemRenderer.prototype.resize.call(this, screenWidth, screenHeight); - this.rootRenderTarget.resize(width, height); + this.rootRenderTarget.resize(screenWidth, screenHeight); if (this._activeRenderTarget === this.rootRenderTarget) { @@ -396,6 +396,26 @@ } /** + * Erases the render texture and fills the drawing area with a colour + * + * @param {PIXI.RenderTexture} renderTexture - The render texture to clear + * @param {number} [clearColor] - The colour + * @return {PIXI.WebGLRenderer} Returns itself. + */ + clearRenderTexture(renderTexture, clearColor) + { + const baseTexture = renderTexture.baseTexture; + const renderTarget = baseTexture._glRenderTargets[this.CONTEXT_UID]; + + if (renderTarget) + { + renderTarget.clear(clearColor); + } + + return this; + } + + /** * Binds a render texture for rendering * * @param {PIXI.RenderTexture} renderTexture - The render texture to render @@ -474,9 +494,10 @@ * Changes the current GLShader to the one given in parameter * * @param {PIXI.glCore.Shader} shader - the new glShader + * @param {boolean} [autoProject=true] - Whether automatically set the projection matrix * @return {PIXI.WebGLRenderer} Returns itself. */ - _bindGLShader(shader) + _bindGLShader(shader, autoProject) { // TODO cache if (this._activeShader !== shader) @@ -484,8 +505,14 @@ this._activeShader = shader; shader.bind(); - // automatically set the projection matrix - shader.uniforms.projectionMatrix = this._activeRenderTarget.projectionMatrix.toArray(true); + // `autoProject` normally would be a default parameter set to true + // but because of how Babel transpiles default parameters + // it hinders the performance of this method. + if (autoProject !== false) + { + // automatically set the projection matrix + shader.uniforms.projectionMatrix = this._activeRenderTarget.projectionMatrix.toArray(true); + } } return this; diff --git a/src/core/renderers/webgl/WebGLState.js b/src/core/renderers/webgl/WebGLState.js index ba6ecc5..6c846d5 100755 --- a/src/core/renderers/webgl/WebGLState.js +++ b/src/core/renderers/webgl/WebGLState.js @@ -82,18 +82,20 @@ push() { // next state.. - let state = this.stack[++this.stackIndex]; + let state = this.stack[this.stackIndex]; if (!state) { state = this.stack[this.stackIndex] = new Uint8Array(16); } + ++this.stackIndex; + // copy state.. // set active state so we can force overrides of gl state for (let i = 0; i < this.activeState.length; i++) { - this.activeState[i] = state[i]; + state[i] = this.activeState[i]; } } diff --git a/src/core/renderers/webgl/filters/Filter.js b/src/core/renderers/webgl/filters/Filter.js index 2c3eaad..4eb6128 100644 --- a/src/core/renderers/webgl/filters/Filter.js +++ b/src/core/renderers/webgl/filters/Filter.js @@ -9,7 +9,7 @@ * @memberof PIXI * @extends PIXI.Shader */ -class Filter extends Shader +export default class Filter extends Shader { /** * @param {string} [vertexSrc] - The source of the vertex shader. @@ -56,15 +56,16 @@ * @param {PIXI.RenderTarget} input - The input render target. * @param {PIXI.RenderTarget} output - The target to output to. * @param {boolean} clear - Should the output be cleared before rendering to it + * @param {object} [currentState] - It's current state of filter. + * There are some useful properties in the currentState : + * target, filters, sourceFrame, destinationFrame, renderTarget, resolution */ - apply(filterManager, input, output, clear) + apply(filterManager, input, output, clear, currentState) // eslint-disable-line no-unused-vars { // do as you please! - filterManager.applyFilter(this, input, output, clear); + filterManager.applyFilter(this, input, output, clear, currentState); // or just do a regular render.. } } - -export default Filter; diff --git a/src/core/renderers/webgl/filters/extractUniformsFromSrc.js b/src/core/renderers/webgl/filters/extractUniformsFromSrc.js index d78a0b1..4893214 100644 --- a/src/core/renderers/webgl/filters/extractUniformsFromSrc.js +++ b/src/core/renderers/webgl/filters/extractUniformsFromSrc.js @@ -12,7 +12,7 @@ function extractUniformsFromString(string) { - const maskRegex = new RegExp('^(projectionMatrix|uSampler|filterArea)$'); + const maskRegex = new RegExp('^(projectionMatrix|uSampler|filterArea|filterClamp)$'); const uniforms = {}; let nameSplit; diff --git a/src/core/renderers/webgl/managers/FilterManager.js b/src/core/renderers/webgl/managers/FilterManager.js index 43fc869..6c7422d 100644 --- a/src/core/renderers/webgl/managers/FilterManager.js +++ b/src/core/renderers/webgl/managers/FilterManager.js @@ -2,6 +2,7 @@ import RenderTarget from '../utils/RenderTarget'; import Quad from '../utils/Quad'; import { Rectangle } from '../../../math'; +import Shader from '../../../Shader'; import * as filterTransforms from '../filters/filterTransforms'; import bitTwiddle from 'bit-twiddle'; @@ -22,8 +23,6 @@ this.filters = []; this.target = null; this.resolution = 1; - - this.firstRun = true; } } @@ -155,7 +154,7 @@ if (filters.length === 1) { - filters[0].apply(this, currentState.renderTarget, lastState.renderTarget, false); + filters[0].apply(this, currentState.renderTarget, lastState.renderTarget, false, currentState); this.freePotRenderTarget(currentState.renderTarget); } else @@ -177,7 +176,7 @@ for (i = 0; i < filters.length - 1; ++i) { - filters[i].apply(this, flip, flop, true); + filters[i].apply(this, flip, flop, true, currentState); const t = flip; @@ -185,7 +184,7 @@ flop = t; } - filters[i].apply(this, flip, lastState.renderTarget, false); + filters[i].apply(this, flip, lastState.renderTarget, false, currentState); this.freePotRenderTarget(flip); this.freePotRenderTarget(flop); @@ -212,16 +211,28 @@ const renderer = this.renderer; const gl = renderer.gl; - renderer.bindShader(filter, true); + let shader = filter.glShaders[renderer.CONTEXT_UID]; - // TODO theres a better way! - // quad can be a mesh and we then should be able to pull this off without accessing the shader directly - const shader = renderer.shaderManager.getGLShader(); - - // TODO change quad to mesh.. - if (!this.firstRun) + // cacheing.. + if (!shader) { - this.firstRun = true; + if (filter.glShaderKey) + { + shader = this.shaderCache[filter.glShaderKey]; + + if (!shader) + { + shader = new Shader(this.gl, filter.vertexSrc, filter.fragmentSrc); + + filter.glShaders[renderer.CONTEXT_UID] = this.shaderCache[filter.glShaderKey] = shader; + } + } + else + { + shader = filter.glShaders[renderer.CONTEXT_UID] = new Shader(this.gl, filter.vertexSrc, filter.fragmentSrc); + } + + // TODO - this only needs to be done once? renderer.bindVao(null); this.quad.initVao(shader); @@ -244,58 +255,146 @@ renderer.maskManager.pushScissorMask(null, renderer.maskManager.scissorData); } - // renderer._bindGLShader(shader); + renderer.bindShader(shader); - let currentState; - const uniforms = filter.uniforms; + // free unit 0 for us, doesn't matter what was there + // don't try to restore it, because syncUniforms can upload it to another slot + // and it'll be a problem + const tex = this.renderer.emptyTextures[0]; - if (uniforms.filterArea) - { - currentState = this.filterData.stack[this.filterData.index]; - - const filterArea = uniforms.filterArea; - - filterArea[0] = currentState.renderTarget.size.width; - filterArea[1] = currentState.renderTarget.size.height; - filterArea[2] = currentState.sourceFrame.x; - filterArea[3] = currentState.sourceFrame.y; - - filter.uniforms.filterArea = filterArea; - } - - // use this to clamp displaced texture coords so they belong to filterArea - // see displacementFilter fragment shader for an example - if (uniforms.filterClamp) - { - currentState = this.filterData.stack[this.filterData.index]; - - const filterClamp = uniforms.filterClamp; - - filterClamp[0] = 0; - filterClamp[1] = 0; - filterClamp[2] = (currentState.sourceFrame.width - 1) / currentState.renderTarget.size.width; - filterClamp[3] = (currentState.sourceFrame.height - 1) / currentState.renderTarget.size.height; - - filter.uniforms.filterClamp = filterClamp; - } - - renderer.shaderManager.setUniforms(filter.uniforms); + this.renderer.boundTextures[0] = tex; + // this syncs the pixi filters uniforms with glsl uniforms + this.syncUniforms(shader, filter); renderer.state.setBlendMode(filter.blendMode); - // temporary bypass cache.. - const tex = this.renderer.boundTextures[0]; - gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, input.texture.texture); this.quad.vao.draw(this.renderer.gl.TRIANGLES, 6, 0); - // restore cache. gl.bindTexture(gl.TEXTURE_2D, tex._glTextures[this.renderer.CONTEXT_UID].texture); } /** + * Uploads the uniforms of the filter. + * + * @param {GLShader} shader - The underlying gl shader. + * @param {PIXI.Filter} filter - The filter we are synchronizing. + */ + syncUniforms(shader, filter) + { + const uniformData = filter.uniformData; + const uniforms = filter.uniforms; + + // 0 is reserved for the pixi texture so we start at 1! + let textureCount = 1; + let currentState; + + // filterArea and filterClamp that are handled by FilterManager directly + // they must not appear in uniformData + + if (shader.uniforms.filterArea) + { + currentState = this.filterData.stack[this.filterData.index]; + + const filterArea = shader.uniforms.filterArea; + + filterArea[0] = currentState.renderTarget.size.width; + filterArea[1] = currentState.renderTarget.size.height; + filterArea[2] = currentState.sourceFrame.x; + filterArea[3] = currentState.sourceFrame.y; + + shader.uniforms.filterArea = filterArea; + } + + // use this to clamp displaced texture coords so they belong to filterArea + // see displacementFilter fragment shader for an example + if (shader.uniforms.filterClamp) + { + currentState = currentState || this.filterData.stack[this.filterData.index]; + + const filterClamp = shader.uniforms.filterClamp; + + filterClamp[0] = 0; + filterClamp[1] = 0; + filterClamp[2] = (currentState.sourceFrame.width - 1) / currentState.renderTarget.size.width; + filterClamp[3] = (currentState.sourceFrame.height - 1) / currentState.renderTarget.size.height; + + shader.uniforms.filterClamp = filterClamp; + } + + // TODO Cacheing layer.. + for (const i in uniformData) + { + if (uniformData[i].type === 'sampler2D' && uniforms[i] !== 0) + { + if (uniforms[i].baseTexture) + { + shader.uniforms[i] = this.renderer.bindTexture(uniforms[i].baseTexture, textureCount); + } + else + { + shader.uniforms[i] = textureCount; + + // TODO + // this is helpful as renderTargets can also be set. + // Although thinking about it, we could probably + // make the filter texture cache return a RenderTexture + // rather than a renderTarget + const gl = this.renderer.gl; + + this.renderer.boundTextures[textureCount] = this.renderer.emptyTextures[textureCount]; + gl.activeTexture(gl.TEXTURE0 + textureCount); + + uniforms[i].texture.bind(); + } + + textureCount++; + } + else if (uniformData[i].type === 'mat3') + { + // check if its pixi matrix.. + if (uniforms[i].a !== undefined) + { + shader.uniforms[i] = uniforms[i].toArray(true); + } + else + { + shader.uniforms[i] = uniforms[i]; + } + } + else if (uniformData[i].type === 'vec2') + { + // check if its a point.. + if (uniforms[i].x !== undefined) + { + const val = shader.uniforms[i] || new Float32Array(2); + + val[0] = uniforms[i].x; + val[1] = uniforms[i].y; + shader.uniforms[i] = val; + } + else + { + shader.uniforms[i] = uniforms[i]; + } + } + else if (uniformData[i].type === 'float') + { + if (shader.uniforms.data[i].value !== uniformData[i]) + { + shader.uniforms[i] = uniforms[i]; + } + } + else + { + shader.uniforms[i] = uniforms[i]; + } + } + } + + /** * Gets a render target from the pool, or creates a new one. * * @param {boolean} clear - Should we clear the render texture when we get it? @@ -442,7 +541,6 @@ renderTarget.resolution = resolution; renderTarget.defaultFrame.width = renderTarget.size.width = minWidth / resolution; renderTarget.defaultFrame.height = renderTarget.size.height = minHeight / resolution; - renderTarget.attachStencilBuffer(); return renderTarget; } diff --git a/src/core/settings.js b/src/core/settings.js index bb08cb0..f4dd087 100644 --- a/src/core/settings.js +++ b/src/core/settings.js @@ -77,11 +77,11 @@ * * @static * @memberof PIXI.settings - * @type {RegExp|string} + * @type {RegExp} * @example `@2x` - * @default /@(.+)x/ + * @default /@([0-9\.]+)x/ */ - RETINA_PREFIX: /@(.+)x/, + RETINA_PREFIX: /@([0-9\.]+)x/, /** * The default render options if none are supplied to {@link PIXI.WebGLRenderer} @@ -175,14 +175,24 @@ SCALE_MODE: 0, /** - * Default specify float precision in shaders. + * Default specify float precision in vertex shader. + * + * @static + * @memberof PIXI.settings + * @type {PIXI.PRECISION} + * @default PIXI.PRECISION.HIGH + */ + PRECISION_VERTEX: 'highp', + + /** + * Default specify float precision in fragment shader. * * @static * @memberof PIXI.settings * @type {PIXI.PRECISION} * @default PIXI.PRECISION.MEDIUM */ - PRECISION: 'mediump', + PRECISION_FRAGMENT: 'mediump', /** * Can we upload the same buffer in a single frame? diff --git a/src/core/sprites/Sprite.js b/src/core/sprites/Sprite.js index 28b0b18..121bf58 100644 --- a/src/core/sprites/Sprite.js +++ b/src/core/sprites/Sprite.js @@ -212,11 +212,11 @@ } else { - w0 = orig.width * (1 - anchor._x); - w1 = orig.width * -anchor._x; + w1 = -anchor._x * orig.width; + w0 = w1 + orig.width; - h0 = orig.height * (1 - anchor._y); - h1 = orig.height * -anchor._y; + h1 = -anchor._y * orig.height; + h0 = h1 + orig.height; } // xy @@ -269,11 +269,11 @@ const tx = wt.tx; const ty = wt.ty; - const w0 = (orig.width) * (1 - anchor._x); - const w1 = (orig.width) * -anchor._x; + const w1 = -anchor._x * orig.width; + const w0 = w1 + orig.width; - const h0 = orig.height * (1 - anchor._y); - const h1 = orig.height * -anchor._y; + const h1 = -anchor._y * orig.height; + const h0 = h1 + orig.height; // xy vertexData[0] = (a * w1) + (c * h1) + tx; @@ -440,7 +440,7 @@ * * @static * @param {number|string|PIXI.BaseTexture|HTMLCanvasElement|HTMLVideoElement} source Source to create texture from - * @return {PIXI.Sprite} The newly created texture + * @return {PIXI.Sprite} The newly created sprite */ static from(source) { @@ -538,8 +538,8 @@ } /** - * The tint applied to the sprite. This is a hex value. A value of - * 0xFFFFFF will remove any tint effect. + * The tint applied to the sprite. This is a hex value. + * A value of 0xFFFFFF will remove any tint effect. * * @member {number} * @default 0xFFFFFF diff --git a/src/core/sprites/canvas/CanvasTinter.js b/src/core/sprites/canvas/CanvasTinter.js index 1cf316b..27e5e0c 100644 --- a/src/core/sprites/canvas/CanvasTinter.js +++ b/src/core/sprites/canvas/CanvasTinter.js @@ -75,8 +75,8 @@ crop.width *= resolution; crop.height *= resolution; - canvas.width = crop.width; - canvas.height = crop.height; + canvas.width = Math.ceil(crop.width); + canvas.height = Math.ceil(crop.height); context.fillStyle = `#${(`00000${(color | 0).toString(16)}`).substr(-6)}`; @@ -130,8 +130,8 @@ crop.width *= resolution; crop.height *= resolution; - canvas.width = crop.width; - canvas.height = crop.height; + canvas.width = Math.ceil(crop.width); + canvas.height = Math.ceil(crop.height); context.globalCompositeOperation = 'copy'; context.fillStyle = `#${(`00000${(color | 0).toString(16)}`).substr(-6)}`; @@ -172,8 +172,8 @@ crop.width *= resolution; crop.height *= resolution; - canvas.width = crop.width; - canvas.height = crop.height; + canvas.width = Math.ceil(crop.width); + canvas.height = Math.ceil(crop.height); context.globalCompositeOperation = 'copy'; context.drawImage( diff --git a/src/core/sprites/webgl/SpriteRenderer.js b/src/core/sprites/webgl/SpriteRenderer.js index 2affcd5..7c352e2 100644 --- a/src/core/sprites/webgl/SpriteRenderer.js +++ b/src/core/sprites/webgl/SpriteRenderer.js @@ -30,7 +30,7 @@ /** * Number of values sent in the vertex buffer. - * positionX, positionY, colorR, colorG, colorB = 5 + * aVertexPosition(2), aTextureCoord(1), aColor(1), aTextureId(1) = 5 * * @member {number} */ @@ -75,7 +75,6 @@ this.shader = null; this.currentIndex = 0; - TICK = 0; this.groups = []; for (let k = 0; k < this.size; k++) diff --git a/src/core/textures/BaseTexture.js b/src/core/textures/BaseTexture.js index 853de7e..aeccf39 100644 --- a/src/core/textures/BaseTexture.js +++ b/src/core/textures/BaseTexture.js @@ -232,16 +232,24 @@ this.realWidth = this.source.naturalWidth || this.source.videoWidth || this.source.width; this.realHeight = this.source.naturalHeight || this.source.videoHeight || this.source.height; - this.width = this.realWidth / this.resolution; - this.height = this.realHeight / this.resolution; - - this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); + this._updateDimensions(); } this.emit('update', this); } /** + * Update dimensions from real values + */ + _updateDimensions() + { + this.width = this.realWidth / this.resolution; + this.height = this.realHeight / this.resolution; + + this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); + } + + /** * Load a source. * * If the source is not-immediately-available, such as an image that needs to be @@ -525,11 +533,7 @@ this.realWidth = Math.round(svgWidth * this.sourceScale); this.realHeight = Math.round(svgHeight * this.sourceScale); - this.width = this.realWidth / this.resolution; - this.height = this.realHeight / this.resolution; - - // Check pow2 after scale - this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); + this._updateDimensions(); // Create a canvas element const canvas = document.createElement('canvas'); @@ -690,4 +694,52 @@ return baseTexture; } + + /** + * Helper function that creates a base texture based on the source you provide. + * The source can be - image url, image element, canvas element. + * + * @static + * @param {string|HTMLImageElement|HTMLCanvasElement} source - The source to create base texture from. + * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values + * @param {number} [sourceScale=(auto)] - Scale for the original image, used with Svg images. + * @return {PIXI.BaseTexture} The new base texture. + */ + static from(source, scaleMode, sourceScale) + { + if (typeof source === 'string') + { + return BaseTexture.fromImage(source, undefined, scaleMode, sourceScale); + } + else if (source instanceof HTMLImageElement) + { + const imageUrl = source.src; + let baseTexture = BaseTextureCache[imageUrl]; + + if (!baseTexture) + { + baseTexture = new BaseTexture(source, scaleMode); + baseTexture.imageUrl = imageUrl; + + if (sourceScale) + { + baseTexture.sourceScale = sourceScale; + } + + // if there is an @2x at the end of the url we are going to assume its a highres image + baseTexture.resolution = getResolutionOfUrl(imageUrl); + + BaseTextureCache[imageUrl] = baseTexture; + } + + return baseTexture; + } + else if (source instanceof HTMLCanvasElement) + { + return BaseTexture.fromCanvas(source, scaleMode); + } + + // lets assume its a base texture! + return source; + } } diff --git a/src/core/textures/Spritesheet.js b/src/core/textures/Spritesheet.js new file mode 100644 index 0000000..d469c62 --- /dev/null +++ b/src/core/textures/Spritesheet.js @@ -0,0 +1,274 @@ +import { Rectangle, Texture } from '../'; +import { getResolutionOfUrl, TextureCache } from '../utils'; + +/** + * Utility class for maintaining reference to a collection + * of Textures on a single Spritesheet. + * + * @class + * @memberof PIXI + */ +export default class Spritesheet +{ + /** + * The maximum number of Textures to build per process. + * + * @type {number} + * @default 1000 + */ + static get BATCH_SIZE() + { + return 1000; + } + + /** + * @param {PIXI.BaseTexture} baseTexture Reference to the source BaseTexture object. + * @param {Object} data - Spritesheet image data. + * @param {string} [resolutionFilename] - The filename to consider when determining + * the resolution of the spritesheet. If not provided, the imageUrl will + * be used on the BaseTexture. + */ + constructor(baseTexture, data, resolutionFilename = null) + { + /** + * Reference to ths source texture + * @type {PIXI.BaseTexture} + */ + this.baseTexture = baseTexture; + + /** + * Map of spritesheet textures. + * @type {Object} + */ + this.textures = {}; + + /** + * Reference to the original JSON data. + * @type {Object} + */ + this.data = data; + + /** + * The resolution of the spritesheet. + * @type {number} + */ + this.resolution = this._updateResolution( + resolutionFilename || this.baseTexture.imageUrl + ); + + /** + * Map of spritesheet frames. + * @type {Object} + * @private + */ + this._frames = this.data.frames; + + /** + * Collection of frame names. + * @type {string[]} + * @private + */ + this._frameKeys = Object.keys(this._frames); + + /** + * Current batch index being processed. + * @type {number} + * @private + */ + this._batchIndex = 0; + + /** + * Callback when parse is completed. + * @type {Function} + * @private + */ + this._callback = null; + } + + /** + * Generate the resolution from the filename or fallback + * to the meta.scale field of the JSON data. + * + * @private + * @param {string} resolutionFilename - The filename to use for resolving + * the default resolution. + * @return {number} Resolution to use for spritesheet. + */ + _updateResolution(resolutionFilename) + { + const scale = this.data.meta.scale; + + // Use a defaultValue of `null` to check if a url-based resolution is set + let resolution = getResolutionOfUrl(resolutionFilename, null); + + // No resolution found via URL + if (resolution === null) + { + // Use the scale value or default to 1 + resolution = scale !== undefined ? parseFloat(scale) : 1; + } + + // For non-1 resolutions, update baseTexture + if (resolution !== 1) + { + this.baseTexture.resolution = resolution; + this.baseTexture.update(); + } + + return resolution; + } + + /** + * Parser spritesheet from loaded data. This is done asynchronously + * to prevent creating too many Texture within a single process. + * + * @param {Function} callback - Callback when complete returns + * a map of the Textures for this spritesheet. + */ + parse(callback) + { + this._batchIndex = 0; + this._callback = callback; + + if (this._frameKeys.length <= Spritesheet.BATCH_SIZE) + { + this._processFrames(0); + this._parseComplete(); + } + else + { + this._nextBatch(); + } + } + + /** + * Process a batch of frames + * + * @private + * @param {number} initialFrameIndex - The index of frame to start. + */ + _processFrames(initialFrameIndex) + { + let frameIndex = initialFrameIndex; + const maxFrames = Spritesheet.BATCH_SIZE; + + while (frameIndex - initialFrameIndex < maxFrames && frameIndex < this._frameKeys.length) + { + const i = this._frameKeys[frameIndex]; + const rect = this._frames[i].frame; + + if (rect) + { + let frame = null; + let trim = null; + const orig = new Rectangle( + 0, + 0, + this._frames[i].sourceSize.w / this.resolution, + this._frames[i].sourceSize.h / this.resolution + ); + + if (this._frames[i].rotated) + { + frame = new Rectangle( + rect.x / this.resolution, + rect.y / this.resolution, + rect.h / this.resolution, + rect.w / this.resolution + ); + } + else + { + frame = new Rectangle( + rect.x / this.resolution, + rect.y / this.resolution, + rect.w / this.resolution, + rect.h / this.resolution + ); + } + + // Check to see if the sprite is trimmed + if (this._frames[i].trimmed) + { + trim = new Rectangle( + this._frames[i].spriteSourceSize.x / this.resolution, + this._frames[i].spriteSourceSize.y / this.resolution, + rect.w / this.resolution, + rect.h / this.resolution + ); + } + + this.textures[i] = new Texture( + this.baseTexture, + frame, + orig, + trim, + this._frames[i].rotated ? 2 : 0 + ); + + // lets also add the frame to pixi's global cache for fromFrame and fromImage functions + TextureCache[i] = this.textures[i]; + } + + frameIndex++; + } + } + + /** + * The parse has completed. + * + * @private + */ + _parseComplete() + { + const callback = this._callback; + + this._callback = null; + this._batchIndex = 0; + callback.call(this, this.textures); + } + + /** + * Begin the next batch of textures. + * + * @private + */ + _nextBatch() + { + this._processFrames(this._batchIndex * Spritesheet.BATCH_SIZE); + this._batchIndex++; + setTimeout(() => + { + if (this._batchIndex * Spritesheet.BATCH_SIZE < this._frameKeys.length) + { + this._nextBatch(); + } + else + { + this._parseComplete(); + } + }, 0); + } + + /** + * Destroy Spritesheet and don't use after this. + * + * @param {boolean} [destroyBase=false] Whether to destroy the base texture as well + */ + destroy(destroyBase = false) + { + for (const i in this.textures) + { + this.textures[i].destroy(); + } + this._frames = null; + this._frameKeys = null; + this.data = null; + this.textures = null; + if (destroyBase) + { + this.baseTexture.destroy(); + } + this.baseTexture = null; + } +} diff --git a/src/core/textures/Texture.js b/src/core/textures/Texture.js index 6403d4f..afe4254 100644 --- a/src/core/textures/Texture.js +++ b/src/core/textures/Texture.js @@ -3,7 +3,7 @@ import TextureUvs from './TextureUvs'; import EventEmitter from 'eventemitter3'; import { Rectangle } from '../math'; -import { TextureCache, BaseTextureCache } from '../utils'; +import { TextureCache, BaseTextureCache, getResolutionOfUrl } from '../utils'; /** * A texture stores the information that represents an image or part of an image. It cannot be added @@ -365,7 +365,8 @@ * The source can be - frame id, image url, video url, canvas element, video element, base texture * * @static - * @param {number|string|PIXI.BaseTexture|HTMLCanvasElement|HTMLVideoElement} source - Source to create texture from + * @param {number|string|HTMLImageElement|HTMLCanvasElement|HTMLVideoElement|PIXI.BaseTexture} + * source - Source to create texture from * @return {PIXI.Texture} The newly created texture */ static from(source) @@ -393,7 +394,7 @@ } else if (source instanceof HTMLImageElement) { - return new Texture(new BaseTexture(source)); + return new Texture(BaseTexture.from(source)); } else if (source instanceof HTMLCanvasElement) { @@ -413,6 +414,43 @@ } /** + * Create a texture from a source and add to the cache. + * + * @static + * @param {HTMLImageElement|HTMLCanvasElement} source - The input source. + * @param {String} imageUrl - File name of texture, for cache and resolving resolution. + * @param {String} [name] - Human readible name for the texture cache. If no name is + * specified, only `imageUrl` will be used as the cache ID. + * @return {PIXI.Texture} Output texture + */ + static fromLoader(source, imageUrl, name) + { + const baseTexture = new BaseTexture(source, null, getResolutionOfUrl(imageUrl)); + const texture = new Texture(baseTexture); + + baseTexture.imageUrl = imageUrl; + + // No name, use imageUrl instead + if (!name) + { + name = imageUrl; + } + + // lets also add the frame to pixi's global cache for fromFrame and fromImage fucntions + BaseTextureCache[name] = baseTexture; + TextureCache[name] = texture; + + // also add references by url if they are different. + if (name !== imageUrl) + { + BaseTextureCache[imageUrl] = baseTexture; + TextureCache[imageUrl] = texture; + } + + return texture; + } + + /** * Adds a texture to the global TextureCache. This cache is shared across the whole PIXI object. * * @static @@ -522,6 +560,29 @@ } } +function createWhiteTexture() +{ + const canvas = document.createElement('canvas'); + + canvas.width = 10; + canvas.height = 10; + + const context = canvas.getContext('2d'); + + context.fillStyle = 'white'; + context.fillRect(0, 0, 10, 10); + + return new Texture(new BaseTexture(canvas)); +} + +function removeAllHandlers(tex) +{ + tex.destroy = function _emptyDestroy() { /* empty */ }; + tex.on = function _emptyOn() { /* empty */ }; + tex.once = function _emptyOnce() { /* empty */ }; + tex.emit = function _emptyEmit() { /* empty */ }; +} + /** * An empty texture, used often to not have to create multiple empty textures. * Can not be destroyed. @@ -530,7 +591,14 @@ * @constant */ Texture.EMPTY = new Texture(new BaseTexture()); -Texture.EMPTY.destroy = function _emptyDestroy() { /* empty */ }; -Texture.EMPTY.on = function _emptyOn() { /* empty */ }; -Texture.EMPTY.once = function _emptyOnce() { /* empty */ }; -Texture.EMPTY.emit = function _emptyEmit() { /* empty */ }; +removeAllHandlers(Texture.EMPTY); + +/** + * A white texture of 10x10 size, used for graphics and other things + * Can not be destroyed. + * + * @static + * @constant + */ +Texture.WHITE = createWhiteTexture(); +removeAllHandlers(Texture.WHITE); diff --git a/src/core/ticker/Ticker.js b/src/core/ticker/Ticker.js index d98a1f0..17a1517 100644 --- a/src/core/ticker/Ticker.js +++ b/src/core/ticker/Ticker.js @@ -67,11 +67,12 @@ * is based, this value is neither capped nor scaled. * If the platform supports DOMHighResTimeStamp, * this value will have a precision of 1 µs. + * Defaults to target frame time * * @member {number} - * @default 1 / TARGET_FPMS + * @default 16.66 */ - this.elapsedMS = 1 / settings.TARGET_FPMS; // default to target frame time + this.elapsedMS = 1 / settings.TARGET_FPMS; /** * The last time {@link PIXI.ticker.Ticker#update} was invoked. diff --git a/src/core/utils/index.js b/src/core/utils/index.js index 6ff609f..be44750 100644 --- a/src/core/utils/index.js +++ b/src/core/utils/index.js @@ -2,6 +2,7 @@ import settings from '../settings'; import EventEmitter from 'eventemitter3'; import pluginTarget from './pluginTarget'; +import * as mixins from './mixin'; import * as isMobile from 'ismobilejs'; let nextUid = 0; @@ -33,6 +34,7 @@ * @type {mixin} */ pluginTarget, + mixins, }; /** @@ -93,7 +95,7 @@ */ export function rgb2hex(rgb) { - return (((rgb[0] * 255) << 16) + ((rgb[1] * 255) << 8) + (rgb[2] * 255)); + return (((rgb[0] * 255) << 16) + ((rgb[1] * 255) << 8) + (rgb[2] * 255 | 0)); } /** @@ -368,3 +370,43 @@ * @private */ export const ProgramCache = {}; + +/** + * Destroys all texture in the cache + * + * @memberof PIXI.utils + * @function destroyTextureCache + */ +export function destroyTextureCache() +{ + let key; + + for (key in TextureCache) + { + TextureCache[key].destroy(); + } + for (key in BaseTextureCache) + { + BaseTextureCache[key].destroy(); + } +} + +/** + * Removes all textures from cache, but does not destroy them + * + * @memberof PIXI.utils + * @function clearTextureCache + */ +export function clearTextureCache() +{ + let key; + + for (key in TextureCache) + { + delete TextureCache[key]; + } + for (key in BaseTextureCache) + { + delete BaseTextureCache[key]; + } +} diff --git a/src/core/utils/mixin.js b/src/core/utils/mixin.js new file mode 100644 index 0000000..114de2a --- /dev/null +++ b/src/core/utils/mixin.js @@ -0,0 +1,59 @@ +/** + * Mixes all enumerable properties and methods from a source object to a target object. + * + * @memberof PIXI.utils.mixins + * @function mixin + * @param {object} target The prototype or instance that properties and methods should be added to. + * @param {object} source The source of properties and methods to mix in. + */ +export function mixin(target, source) +{ + if (!target || !source) return; + // in ES8/ES2017, this would be really easy: + // Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); + + // get all the enumerable property keys + const keys = Object.keys(source); + + // loop through properties + for (let i = 0; i < keys.length; ++i) + { + const propertyName = keys[i]; + + // Set the property using the property descriptor - this works for accessors and normal value properties + Object.defineProperty(target, propertyName, Object.getOwnPropertyDescriptor(source, propertyName)); + } +} + +const mixins = []; + +/** + * Queues a mixin to be handled towards the end of the initialization of PIXI, so that deprecation + * can take effect. + * + * @memberof PIXI.utils.mixins + * @function delayMixin + * @private + * @param {object} target The prototype or instance that properties and methods should be added to. + * @param {object} source The source of properties and methods to mix in. + */ +export function delayMixin(target, source) +{ + mixins.push(target, source); +} + +/** + * Handles all mixins queued via delayMixin(). + * + * @memberof PIXI.utils.mixins + * @function performMixins + * @private + */ +export function performMixins() +{ + for (let i = 0; i < mixins.length; i += 2) + { + mixin(mixins[i], mixins[i + 1]); + } + mixins.length = 0; +} diff --git a/src/deprecation.js b/src/deprecation.js index 045a19f..b9c73b3 100644 --- a/src/deprecation.js +++ b/src/deprecation.js @@ -5,6 +5,7 @@ import * as filters from './filters'; import * as prepare from './prepare'; import * as loaders from './loaders'; +import * as interaction from './interaction'; // provide method to give a stack track for warnings // useful for tracking-down where deprecated methods/properties/classes @@ -534,7 +535,7 @@ { parent: 'GC_MODES', target: 'GC_MODE' }, { parent: 'WRAP_MODES', target: 'WRAP_MODE' }, { parent: 'SCALE_MODES', target: 'SCALE_MODE' }, - { parent: 'PRECISION', target: 'PRECISION' }, + { parent: 'PRECISION', target: 'PRECISION_FRAGMENT' }, ]; for (let i = 0; i < defaults.length; i++) @@ -558,6 +559,32 @@ }); } +Object.defineProperties(core.settings, { + + /** + * @static + * @name PRECISION + * @memberof PIXI.settings + * @see PIXI.PRECISION + * @deprecated since version 4.4.0 + */ + PRECISION: { + enumerable: true, + get() + { + warn('PIXI.settings.PRECISION has been deprecated, please use PIXI.settings.PRECISION_FRAGMENT'); + + return core.settings.PRECISION_FRAGMENT; + }, + set(value) + { + warn('PIXI.settings.PRECISION has been deprecated, please use PIXI.settings.PRECISION_FRAGMENT'); + + core.settings.PRECISION_FRAGMENT = value; + }, + }, +}); + Object.defineProperties(extras, { /** @@ -694,7 +721,7 @@ warn('determineFontProperties is now deprecated, please use the static calculateFontProperties method, ' + 'e.g : Text.calculateFontProperties(fontStyle);'); - return Text.calculateFontProperties(fontStyle); + return core.Text.calculateFontProperties(fontStyle); }; Object.defineProperties(core.TextStyle.prototype, { @@ -1003,3 +1030,69 @@ }, }, }); + +/** + * @name PIXI.interaction.interactiveTarget#defaultCursor + * @static + * @type {number} + * @see PIXI.interaction.interactiveTarget#cursor + * @deprecated since 4.3.0 + */ +Object.defineProperty(interaction.interactiveTarget, 'defaultCursor', { + set(value) + { + warn('Property defaultCursor has been replaced with \'cursor\'. '); + this.cursor = value; + }, + get() + { + warn('Property defaultCursor has been replaced with \'cursor\'. '); + + return this.cursor; + }, + enumerable: true, +}); + +/** + * @name PIXI.interaction.InteractionManager#defaultCursorStyle + * @static + * @type {string} + * @see PIXI.interaction.InteractionManager#cursorStyles + * @deprecated since 4.3.0 + */ +Object.defineProperty(interaction.InteractionManager, 'defaultCursorStyle', { + set(value) + { + warn('Property defaultCursorStyle has been replaced with \'cursorStyles.default\'. '); + this.cursorStyles.default = value; + }, + get() + { + warn('Property defaultCursorStyle has been replaced with \'cursorStyles.default\'. '); + + return this.cursorStyles.default; + }, +}); + +/** + * @name PIXI.interaction.InteractionManager#currentCursorStyle + * @static + * @type {string} + * @see PIXI.interaction.InteractionManager#cursorStyles + * @deprecated since 4.3.0 + */ +Object.defineProperty(interaction.InteractionManager, 'currentCursorStyle', { + set(value) + { + warn('Property currentCursorStyle has been removed.' + + 'See the currentCursorMode property, which works differently.'); + this.currentCursorMode = value; + }, + get() + { + warn('Property currentCursorStyle has been removed.' + + 'See the currentCursorMode property, which works differently.'); + + return this.currentCursorMode; + }, +}); diff --git a/src/extras/BitmapText.js b/src/extras/BitmapText.js index fb6c343..0a0e150 100644 --- a/src/extras/BitmapText.js +++ b/src/extras/BitmapText.js @@ -455,6 +455,73 @@ return this._textHeight; } + + /** + * Register a bitmap font with data and a texture. + * + * @static + * @param {XMLDocument} xml - The XML document data. + * @param {PIXI.Texture} texture - Texture with all symbols. + * @return {Object} Result font object with font, size, lineHeight and char fields. + */ + static registerFont(xml, texture) + { + const data = {}; + const info = xml.getElementsByTagName('info')[0]; + const common = xml.getElementsByTagName('common')[0]; + + data.font = info.getAttribute('face'); + data.size = parseInt(info.getAttribute('size'), 10); + data.lineHeight = parseInt(common.getAttribute('lineHeight'), 10); + data.chars = {}; + + // parse letters + const letters = xml.getElementsByTagName('char'); + + for (let i = 0; i < letters.length; i++) + { + const letter = letters[i]; + const charCode = parseInt(letter.getAttribute('id'), 10); + + const textureRect = new core.Rectangle( + parseInt(letter.getAttribute('x'), 10) + texture.frame.x, + parseInt(letter.getAttribute('y'), 10) + texture.frame.y, + parseInt(letter.getAttribute('width'), 10), + parseInt(letter.getAttribute('height'), 10) + ); + + data.chars[charCode] = { + xOffset: parseInt(letter.getAttribute('xoffset'), 10), + yOffset: parseInt(letter.getAttribute('yoffset'), 10), + xAdvance: parseInt(letter.getAttribute('xadvance'), 10), + kerning: {}, + texture: new core.Texture(texture.baseTexture, textureRect), + + }; + } + + // parse kernings + const kernings = xml.getElementsByTagName('kerning'); + + for (let i = 0; i < kernings.length; i++) + { + const kerning = kernings[i]; + const first = parseInt(kerning.getAttribute('first'), 10); + const second = parseInt(kerning.getAttribute('second'), 10); + const amount = parseInt(kerning.getAttribute('amount'), 10); + + if (data.chars[second]) + { + data.chars[second].kerning[first] = amount; + } + } + + // I'm leaving this as a temporary fix so we can test the bitmap fonts in v3 + // but it's very likely to change + BitmapText.fonts[data.font] = data; + + return data; + } } BitmapText.fonts = {}; diff --git a/src/index.js b/src/index.js index 59356cf..c553427 100644 --- a/src/index.js +++ b/src/index.js @@ -16,6 +16,10 @@ import * as particles from './particles'; import * as prepare from './prepare'; +// handle mixins now, after all code has been added, including deprecation +import { utils } from './core'; +utils.mixins.performMixins(); + export { accessibility, extract, diff --git a/src/interaction/InteractionManager.js b/src/interaction/InteractionManager.js index 7d51134..8baf13a 100644 --- a/src/interaction/InteractionManager.js +++ b/src/interaction/InteractionManager.js @@ -1,18 +1,20 @@ 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'; -// Mix interactiveTarget into core.DisplayObject.prototype -Object.assign( +// Mix interactiveTarget into core.DisplayObject.prototype, after deprecation has been handled +core.utils.mixins.delayMixin( core.DisplayObject.prototype, 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 +70,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 +101,6 @@ this.eventData = new InteractionEvent(); /** - * Tiny little interactiveData pool ! - * - * @member {PIXI.interaction.InteractionData[]} - */ - this.interactiveDataPool = []; - - /** * The DOM element to bind to. * * @private @@ -153,66 +155,12 @@ */ this.supportsPointerEvents = !!window.PointerEvent; - /** - * Are touch events being 'normalized' and converted into pointer events if pointer events are not supported - * For example, on a touch screen mobile device, a touchstart would also be emitted as a pointerdown - * - * @private - * @readonly - * @member {boolean} - */ - this.normalizeTouchEvents = !this.supportsPointerEvents && this.supportsTouchEvents; - - /** - * Are mouse events being 'normalized' and converted into pointer events if pointer events are not supported - * For example, on a desktop pc, a mousedown would also be emitted as a pointerdown - * - * @private - * @readonly - * @member {boolean} - */ - this.normalizeMouseEvents = !this.supportsPointerEvents && !MobileDevice.any; - // this will make it so that you don't have to call bind all the time /** * @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 +168,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,41 +199,32 @@ this.onPointerOver = this.onPointerOver.bind(this); /** - * @private - * @member {Function} + * Dictionary of how different cursor modes are handled. Strings are handled as CSS cursor + * values, objects are handled as dictionaries of CSS values for interactionDOMElement, + * and functions are called instead of changing the CSS. + * Default CSS cursor values are provided for 'default' and 'pointer' modes. + * @member {Object.)>} */ - this.onTouchStart = this.onTouchStart.bind(this); - this.processTouchStart = this.processTouchStart.bind(this); + this.cursorStyles = { + default: 'inherit', + pointer: 'pointer', + }; /** - * @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. - * - * @member {string} - * @default 'inherit' - */ - this.defaultCursorStyle = 'inherit'; - - /** - * The css style of the cursor that is being used. + * The mode of the cursor that is being used. + * The value of this is a key from the cursorStyles dictionary. * * @member {string} */ - this.currentCursorStyle = 'inherit'; + this.currentCursorMode = null; + + /** + * Internal cached let. + * + * @private + * @member {string} + */ + this.cursor = null; /** * Internal cached let. @@ -303,6 +249,7 @@ * object. * * @event mousedown + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -311,6 +258,7 @@ * on the display object. * * @event rightdown + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -319,6 +267,7 @@ * object. * * @event mouseup + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -327,6 +276,7 @@ * over the display object. * * @event rightup + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -335,6 +285,7 @@ * the display object. * * @event click + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -343,6 +294,7 @@ * and released on the display object. * * @event rightclick + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -352,6 +304,7 @@ * [mousedown]{@link PIXI.interaction.InteractionManager#event:mousedown}. * * @event mouseupoutside + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -361,6 +314,7 @@ * [rightdown]{@link PIXI.interaction.InteractionManager#event:rightdown}. * * @event rightupoutside + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -368,6 +322,7 @@ * Fired when a pointer device (usually a mouse) is moved while over the display object * * @event mousemove + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -375,6 +330,7 @@ * Fired when a pointer device (usually a mouse) is moved onto the display object * * @event mouseover + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -382,6 +338,7 @@ * Fired when a pointer device (usually a mouse) is moved off the display object * * @event mouseout + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -389,6 +346,7 @@ * Fired when a pointer device button is pressed on the display object. * * @event pointerdown + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -396,6 +354,14 @@ * Fired when a pointer device button is released over the display object. * * @event pointerup + * @type {PIXI.interaction.InteractionData} + * @memberof PIXI.interaction.InteractionManager# + */ + + /** + * Fired when the operating system cancels a pointer event + * + * @event pointercancel * @memberof PIXI.interaction.InteractionManager# */ @@ -403,6 +369,7 @@ * Fired when a pointer device button is pressed and released on the display object. * * @event pointertap + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -411,6 +378,7 @@ * registered a [pointerdown]{@link PIXI.interaction.InteractionManager#event:pointerdown}. * * @event pointerupoutside + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -418,6 +386,7 @@ * Fired when a pointer device is moved while over the display object * * @event pointermove + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -425,6 +394,7 @@ * Fired when a pointer device is moved onto the display object * * @event pointerover + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -432,6 +402,7 @@ * Fired when a pointer device is moved off the display object * * @event pointerout + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -439,6 +410,7 @@ * Fired when a touch point is placed on the display object. * * @event touchstart + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -446,6 +418,14 @@ * Fired when a touch point is removed from the display object. * * @event touchend + * @type {PIXI.interaction.InteractionData} + * @memberof PIXI.interaction.InteractionManager# + */ + + /** + * Fired when the operating system cancels a touch + * + * @event touchcancel * @memberof PIXI.interaction.InteractionManager# */ @@ -453,6 +433,7 @@ * Fired when a touch point is placed and removed from the display object. * * @event tap + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -461,6 +442,7 @@ * registered a [touchstart]{@link PIXI.interaction.InteractionManager#event:touchstart}. * * @event touchendoutside + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -468,6 +450,7 @@ * Fired when a touch point is moved along the display object. * * @event touchmove + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ } @@ -524,45 +507,30 @@ { window.document.addEventListener('pointermove', this.onPointerMove, true); this.interactionDOMElement.addEventListener('pointerdown', this.onPointerDown, true); - this.interactionDOMElement.addEventListener('pointerout', this.onPointerOut, true); + // pointerout is fired in addition to pointerup (for touch events) and pointercancel + // we already handle those, so for the purposes of what we do in onPointerOut, we only + // care about the pointerleave event + this.interactionDOMElement.addEventListener('pointerleave', 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; @@ -596,45 +564,26 @@ { window.document.removeEventListener('pointermove', this.onPointerMove, true); this.interactionDOMElement.removeEventListener('pointerdown', this.onPointerDown, true); - this.interactionDOMElement.removeEventListener('pointerout', this.onPointerOut, true); + this.interactionDOMElement.removeEventListener('pointerleave', 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) + 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.onPointerDown, true); + this.interactionDOMElement.removeEventListener('touchcancel', this.onPointerCancel, 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); - - 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 = null; @@ -664,7 +613,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; @@ -672,25 +621,80 @@ return; } - this.cursor = this.defaultCursorStyle; + this.cursor = null; // 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(); - - this.processInteractive(this.mouse.global, this.renderer._lastObjectRendered, this.processMouseOverOut, true); - - if (this.currentCursorStyle !== this.cursor) + for (const k in this.activeInteractionData) { - this.currentCursorStyle = this.cursor; - this.interactionDOMElement.style.cursor = this.cursor; + // eslint-disable-next-line no-prototype-builtins + if (this.activeInteractionData.hasOwnProperty(k)) + { + const interactionData = this.activeInteractionData[k]; + + if (interactionData.originalEvent && interactionData.pointerType !== 'touch') + { + const interactionEvent = this.configureInteractionEventForDOMEvent( + this.eventData, + interactionData.originalEvent, + interactionData + ); + + this.processInteractive( + interactionEvent, + this.renderer._lastObjectRendered, + this.processPointerOverOut, + true + ); + } + } } + this.setCursorMode(this.cursor); + // TODO } /** + * Sets the current cursor mode, handling any callbacks or CSS style changes. + * + * @param {string} mode - cursor mode, a key from the cursorStyles dictionary + */ + setCursorMode(mode) + { + mode = mode || 'default'; + // if the mode didn't actually change, bail early + if (this.currentCursorMode === mode) + { + return; + } + this.currentCursorMode = mode; + const style = this.cursorStyles[mode]; + + // only do things if there is a cursor style for it + if (style) + { + switch (typeof style) + { + case 'string': + // string styles are handled as cursor CSS + this.interactionDOMElement.style.cursor = style; + break; + case 'function': + // functions are just called, and passed the cursor mode + style(mode); + break; + case 'object': + // if it is an object, assume that it is a dictionary of CSS styles, + // apply it to the interactionDOMElement + Object.assign(this.interactionDOMElement.style, style); + break; + } + } + } + + /** * Dispatches an event on the display object that was interacted with * * @param {PIXI.Container|PIXI.Sprite|PIXI.extras.TilingSprite} displayObject - the display object in question @@ -748,22 +752,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 @@ -787,9 +795,8 @@ { interactiveParent = false; } - - // it has a mask! Then lets hit test that before continuing.. - if (hitTest && displayObject._mask) + // it has a mask! Then lets hit test that before continuing + else if (hitTest && displayObject._mask) { if (!displayObject._mask.containsPoint(point)) { @@ -797,14 +804,7 @@ } } - // it has a filterArea! Same as mask but easier, its a rectangle - if (hitTest && displayObject.filterArea) - { - if (!displayObject.filterArea.contains(point.x, point.y)) - { - hitTest = false; - } - } + let keepHitTestingAfterChildren = hitTest; // ** FREE TIP **! If an object is not interactive or has no buttons in it // (such as a game scene!) set interactiveChildren to false for that displayObject. @@ -818,7 +818,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 @@ -838,15 +838,20 @@ // This means we no longer need to hit test anything else. We still need to run // through all objects, but we don't need to perform any hit tests. - // { - hitTest = false; - // } + keepHitTestingAfterChildren = false; + + if (child.interactive) + { + hitTest = false; + } // we can break now as we have hit an object. } } } + hitTest = keepHitTestingAfterChildren; + // no point running this if the item is not interactive or does not have an interactive parent. if (interactive) { @@ -868,14 +873,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 +886,175 @@ } /** - * 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].isNormalized) { - 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); + if (!displayObject.trackedPointers[id]) + { + displayObject.trackedPointers[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.trackedPointers[id].rightDown = true; + } + else + { + displayObject.trackedPointers[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.trackedPointers[id] !== undefined) + { + delete displayObject.trackedPointers[id]; + this.dispatchEvent(displayObject, 'pointercancel', interactionEvent); + + if (e.type === 'touchcancel' || e.pointerType === 'touch') + { + this.dispatchEvent(displayObject, 'touchcancel', interactionEvent); + } } } @@ -1159,42 +1066,94 @@ */ 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) { - if (hit) - { - this.dispatchEvent(displayObject, 'pointerup', this.eventData); + const e = interactionEvent.data.originalEvent; - if (displayObject._pointerDown) + const id = interactionEvent.data.identifier; + + const trackingData = displayObject.trackedPointers[id]; + + const isTouch = (e.type === 'touchend' || e.pointerType === 'touch'); + + const isMouse = (e.type.indexOf('mouse') === 0 || e.pointerType === 'mouse'); + + // 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) { - displayObject._pointerDown = false; - this.dispatchEvent(displayObject, 'pointertap', this.eventData); + 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); + } + // update the down state of the tracking data + if (trackingData) + { + if (isRightButton) + { + trackingData.rightDown = hit; + } + else + { + trackingData.leftDown = hit; + } } } - else if (displayObject._pointerDown) + + // Pointers and Touches, and Mouse + if (hit) { - displayObject._pointerDown = false; - this.dispatchEvent(displayObject, 'pointerupoutside', this.eventData); + this.dispatchEvent(displayObject, 'pointerup', interactionEvent); + if (isTouch) this.dispatchEvent(displayObject, 'touchend', interactionEvent); + + if (trackingData) + { + this.dispatchEvent(displayObject, 'pointertap', interactionEvent); + if (isTouch) + { + this.dispatchEvent(displayObject, 'tap', interactionEvent); + // touches are no longer over (if they ever were) when we get the touchend + // so we should ensure that we don't keep pretending that they are + trackingData.over = false; + } + } + } + else if (trackingData) + { + this.dispatchEvent(displayObject, 'pointerupoutside', interactionEvent); + if (isTouch) this.dispatchEvent(displayObject, 'touchendoutside', interactionEvent); + } + // Only remove the tracking data if there is no over/down state still associated with it + if (trackingData && trackingData.none) + { + delete displayObject.trackedPointers[id]; } } @@ -1202,39 +1161,78 @@ * 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 = null; + } - 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') + { + this.setCursorMode(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 (isMouse) { - 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 +1240,94 @@ * 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.setCursorMode(null); + } - 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'); + + let trackingData = displayObject.trackedPointers[id]; + + // if we just moused over the display object, then we need to track that state + if (hit && !trackingData) + { + trackingData = displayObject.trackedPointers[id] = new InteractionTrackingData(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); + } + } + + // only change the cursor if it has not already been changed (by something deeper in the + // display tree) + if (isMouse && this.cursor === null) + { + this.cursor = displayObject.cursor; } } - 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); + } + // if there is no mouse down information for the pointer, then it is safe to delete + if (trackingData.none) + { + delete displayObject.trackedPointers[id]; + } } } @@ -1287,253 +1335,154 @@ * 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 && pointerEvent.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; + + // mark the touch as normalized, just so that we know we did it + touch.isNormalized = true; + + normalizedEvents.push(touch); + } } - else if (this.normalizeMouseEvents) + // apparently PointerEvent subclasses MouseEvent, so yay + else if (event instanceof MouseEvent && (!this.supportsPointerEvents || !(event instanceof window.PointerEvent))) { if (typeof event.isPrimary === 'undefined') event.isPrimary = true; if (typeof event.width === 'undefined') event.width = 1; @@ -1541,10 +1490,21 @@ 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; + + // mark the mouse event as normalized, just so that we know we did it + event.isNormalized = true; + + normalizedEvents.push(event); } + else + { + normalizedEvents.push(event); + } + + return normalizedEvents; } /** @@ -1563,30 +1523,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 +1542,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..6a1f69e --- /dev/null +++ b/src/interaction/InteractionTrackingData.js @@ -0,0 +1,147 @@ +/** + * 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 inactive (not over or down)? + * + * @member {number} + * @memberof PIXI.interaction.InteractionTrackingData# + */ + get none() + { + return this._flags === this.constructor.FLAGS.NONE; + } + + /** + * 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/package.json b/package.json index 6a366ba..71ade15 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pixi.js", - "version": "4.3.4", + "version": "4.3.5", "description": "Pixi.js is a fast lightweight 2D library that works across all devices.", "author": "Mat Groves", "contributors": [ @@ -61,7 +61,7 @@ "ismobilejs": "^0.4.0", "object-assign": "^4.0.1", "pixi-gl-core": "^1.0.3", - "resource-loader": "^2.0.4" + "resource-loader": "^2.0.6" }, "devDependencies": { "babel-cli": "^6.18.0", @@ -70,9 +70,9 @@ "babel-preset-es2015": "^6.14.0", "babelify": "^7.3.0", "del": "^2.2.0", - "electron-prebuilt": "^1.3.2", + "electron": "^1.4.15", "eslint": "^3.5.0", - "floss": "^1.2.0", + "floss": "^2.0.1", "gh-pages": "^0.11.0", "jaguarjs-jsdoc": "^1.0.1", "js-md5": "^0.4.1", diff --git a/src/accessibility/AccessibilityManager.js b/src/accessibility/AccessibilityManager.js index df2716a..f16bcb1 100644 --- a/src/accessibility/AccessibilityManager.js +++ b/src/accessibility/AccessibilityManager.js @@ -3,7 +3,7 @@ import accessibleTarget from './accessibleTarget'; // add some extra variables to the container.. -Object.assign( +core.utils.mixins.delayMixin( core.DisplayObject.prototype, accessibleTarget ); diff --git a/src/core/Application.js b/src/core/Application.js index ea0f058..5340578 100644 --- a/src/core/Application.js +++ b/src/core/Application.js @@ -1,6 +1,6 @@ import { autoDetectRenderer } from './autoDetectRenderer'; import Container from './display/Container'; -import Ticker from './ticker/Ticker'; +import { shared, Ticker } from './ticker'; /** * Convenience class to create a new PIXI application. @@ -35,9 +35,9 @@ * @param {boolean} [noWebGL=false] - prevents selection of WebGL renderer, even if such is present * @param {boolean} [options.legacy=false] - If true Pixi will aim to ensure compatibility * with older / less advanced devices. If you experience unexplained flickering try setting this to true. - + * @param {boolean} [useSharedTicker=false] - `true` to use PIXI.ticker.shared, `false` to create new ticker. */ - constructor(width, height, options, noWebGL) + constructor(width, height, options, noWebGL, useSharedTicker = false) { /** * WebGL renderer if available, otherwise CanvasRenderer @@ -46,23 +46,46 @@ this.renderer = autoDetectRenderer(width, height, options, noWebGL); /** - * The root display container that's renderered. + * The root display container that's rendered. * @member {PIXI.Container} */ this.stage = new Container(); /** + * Internal reference to the ticker + * @member {PIXI.ticker.Ticker} + * @private + */ + this._ticker = null; + + /** * Ticker for doing render updates. * @member {PIXI.ticker.Ticker} + * @default PIXI.ticker.shared */ - this.ticker = new Ticker(); - - this.ticker.add(this.render, this); + this.ticker = useSharedTicker ? shared : new Ticker(); // Start the rendering this.start(); } + set ticker(ticker) // eslint-disable-line require-jsdoc + { + if (this._ticker) + { + this._ticker.remove(this.render, this); + } + this._ticker = ticker; + if (ticker) + { + ticker.add(this.render, this); + } + } + get ticker() // eslint-disable-line require-jsdoc + { + return this._ticker; + } + /** * Render the current stage. */ @@ -76,7 +99,7 @@ */ stop() { - this.ticker.stop(); + this._ticker.stop(); } /** @@ -84,7 +107,7 @@ */ start() { - this.ticker.start(); + this._ticker.start(); } /** @@ -98,13 +121,22 @@ } /** + * Reference to the renderer's screen rectangle. Its safe to use as filterArea or hitArea for whole screen + * @member {PIXI.Rectangle} + * @readonly + */ + get screen() + { + return this.renderer.screen; + } + + /** * Destroy and don't use after this. * @param {Boolean} [removeView=false] Automatically remove canvas from DOM. */ destroy(removeView) { this.stop(); - this.ticker.remove(this.render, this); this.ticker = null; this.stage.destroy(); diff --git a/src/core/Shader.js b/src/core/Shader.js new file mode 100644 index 0000000..78b17e6 --- /dev/null +++ b/src/core/Shader.js @@ -0,0 +1,46 @@ +import { GLShader } from 'pixi-gl-core'; +import settings from './settings'; + +function checkPrecision(src, def) +{ + if (src instanceof Array) + { + if (src[0].substring(0, 9) !== 'precision') + { + const copy = src.slice(0); + + copy.unshift(`precision ${def} float;`); + + return copy; + } + } + else if (src.substring(0, 9) !== 'precision') + { + return `precision ${def} float;\n${src}`; + } + + return src; +} + +/** + * Wrapper class, webGL Shader for Pixi. + * Adds precision string if vertexSrc or fragmentSrc have no mention of it. + * + * @class + * @extends GLShader + * @memberof PIXI + */ +export default class Shader extends GLShader +{ + /** + * + * @param {WebGLRenderingContext} gl - The current WebGL rendering context + * @param {string|string[]} vertexSrc - The vertex shader source as an array of strings. + * @param {string|string[]} fragmentSrc - The fragment shader source as an array of strings. + */ + constructor(gl, vertexSrc, fragmentSrc) + { + super(gl, checkPrecision(vertexSrc, settings.PRECISION_VERTEX), + checkPrecision(fragmentSrc, settings.PRECISION_FRAGMENT)); + } +} diff --git a/src/core/const.js b/src/core/const.js index ef6f662..3de0efe 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -188,7 +188,7 @@ * The gc modes that are supported by pixi. * * The {@link PIXI.settings.GC_MODE} Garbage Collection mode for pixi textures is AUTO - * If set to GC_MODE, the renderer will occasianally check textures usage. If they are not + * If set to GC_MODE, the renderer will occasionally check textures usage. If they are not * used for a specified period of time they will be removed from the GPU. They will of course * be uploaded again when they are required. This is a silent behind the scenes process that * should ensure that the GPU does not get filled up. diff --git a/src/core/display/Container.js b/src/core/display/Container.js index ed8cd5f..17fde8d 100644 --- a/src/core/display/Container.js +++ b/src/core/display/Container.js @@ -73,11 +73,12 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.push(child); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -108,11 +109,12 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.splice(index, 0, child); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -225,10 +227,11 @@ if (index === -1) return null; child.parent = null; + // ensure child transform will be recalculated + child.transform._parentID = -1; removeItems(this.children, index, 1); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -249,11 +252,12 @@ { const child = this.getChildAt(index); + // ensure child transform will be recalculated.. child.parent = null; + child.transform._parentID = -1; removeItems(this.children, index, 1); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -284,13 +288,12 @@ for (let i = 0; i < removed.length; ++i) { removed[i].parent = null; + if (removed[i].transform) + { + removed[i].transform._parentID = -1; + } } - // ensure a transform will be recalculated.. - if (this.transform) - { - this.transform._parentID = -1; - } this._boundsID++; this.onChildrenChange(beginIndex); diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index f37f2f5..be4c4fb 100644 --- a/src/core/graphics/Graphics.js +++ b/src/core/graphics/Graphics.js @@ -28,8 +28,9 @@ { /** * + * @param {boolean} [nativeLines=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP */ - constructor() + constructor(nativeLines = false) { super(); @@ -50,6 +51,13 @@ this.lineWidth = 0; /** + * If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * + * @member {boolean} + */ + this.nativeLines = nativeLines; + + /** * The color of any lines drawn. * * @member {string} @@ -757,28 +765,14 @@ if (!this._spriteRect) { - if (!Graphics._SPRITE_TEXTURE) - { - Graphics._SPRITE_TEXTURE = RenderTexture.create(10, 10); - - const canvas = document.createElement('canvas'); - - canvas.width = 10; - canvas.height = 10; - - const context = canvas.getContext('2d'); - - context.fillStyle = 'white'; - context.fillRect(0, 0, 10, 10); - - Graphics._SPRITE_TEXTURE = Texture.fromCanvas(canvas); - } - - this._spriteRect = new Sprite(Graphics._SPRITE_TEXTURE); + this._spriteRect = new Sprite(new Texture(Texture.WHITE)); } + + const sprite = this._spriteRect; + if (this.tint === 0xffffff) { - this._spriteRect.tint = this.graphicsData[0].fillColor; + sprite.tint = this.graphicsData[0].fillColor; } else { @@ -792,20 +786,21 @@ t1[1] *= t2[1]; t1[2] *= t2[2]; - this._spriteRect.tint = rgb2hex(t1); + sprite.tint = rgb2hex(t1); } - this._spriteRect.alpha = this.graphicsData[0].fillAlpha; - this._spriteRect.worldAlpha = this.worldAlpha * this._spriteRect.alpha; + sprite.alpha = this.graphicsData[0].fillAlpha; + sprite.worldAlpha = this.worldAlpha * sprite.alpha; + sprite.blendMode = this.blendMode; - Graphics._SPRITE_TEXTURE._frame.width = rect.width; - Graphics._SPRITE_TEXTURE._frame.height = rect.height; + sprite.texture._frame.width = rect.width; + sprite.texture._frame.height = rect.height; - this._spriteRect.transform.worldTransform = this.transform.worldTransform; + sprite.transform.worldTransform = this.transform.worldTransform; - this._spriteRect.anchor.set(-rect.x / rect.width, -rect.y / rect.height); - this._spriteRect._onAnchorUpdate(); + sprite.anchor.set(-rect.x / rect.width, -rect.y / rect.height); + sprite._onAnchorUpdate(); - this._spriteRect._renderWebGL(renderer); + sprite._renderWebGL(renderer); } /** @@ -1031,6 +1026,7 @@ this.fillColor, this.fillAlpha, this.filling, + this.nativeLines, shape ); diff --git a/src/core/graphics/GraphicsData.js b/src/core/graphics/GraphicsData.js index f5a87ae..b6f03c0 100644 --- a/src/core/graphics/GraphicsData.js +++ b/src/core/graphics/GraphicsData.js @@ -14,14 +14,19 @@ * @param {number} fillColor - the color of the fill * @param {number} fillAlpha - the alpha of the fill * @param {boolean} fill - whether or not the shape is filled with a colour + * @param {boolean} nativeLines - the method for drawing lines * @param {PIXI.Circle|PIXI.Rectangle|PIXI.Ellipse|PIXI.Polygon} shape - The shape object to draw. */ - constructor(lineWidth, lineColor, lineAlpha, fillColor, fillAlpha, fill, shape) + constructor(lineWidth, lineColor, lineAlpha, fillColor, fillAlpha, fill, nativeLines, shape) { /** * @member {number} the width of the line to draw */ this.lineWidth = lineWidth; + /** + * @member {boolean} if true the liens will be draw using LINES instead of TRIANGLE_STRIP + */ + this.nativeLines = nativeLines; /** * @member {number} the color of the line to draw @@ -85,6 +90,7 @@ this.fillColor, this.fillAlpha, this.fill, + this.nativeLines, this.shape ); } diff --git a/src/core/graphics/webgl/GraphicsRenderer.js b/src/core/graphics/webgl/GraphicsRenderer.js index 14083fe..67f5e92 100644 --- a/src/core/graphics/webgl/GraphicsRenderer.js +++ b/src/core/graphics/webgl/GraphicsRenderer.js @@ -102,7 +102,15 @@ shaderTemp.uniforms.alpha = graphics.worldAlpha; renderer.bindVao(webGLData.vao); - webGLData.vao.draw(gl.TRIANGLE_STRIP, webGLData.indices.length); + + if (graphics.nativeLines) + { + gl.drawArrays(gl.LINES, 0, webGLData.points.length / 6); + } + else + { + webGLData.vao.draw(gl.TRIANGLE_STRIP, webGLData.indices.length); + } } } diff --git a/src/core/graphics/webgl/utils/buildLine.js b/src/core/graphics/webgl/utils/buildLine.js index ecf43f7..4d6862e 100644 --- a/src/core/graphics/webgl/utils/buildLine.js +++ b/src/core/graphics/webgl/utils/buildLine.js @@ -11,7 +11,29 @@ * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties * @param {object} webGLData - an object containing all the webGL-specific information to create this shape */ -export default function buildLine(graphicsData, webGLData) +export default function (graphicsData, webGLData) +{ + if (graphicsData.nativeLines) + { + buildNativeLine(graphicsData, webGLData); + } + else + { + buildLine(graphicsData, webGLData); + } +} + +/** + * Builds a line to draw using the poligon method. + * + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @private + * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties + * @param {object} webGLData - an object containing all the webGL-specific information to create this shape + */ +function buildLine(graphicsData, webGLData) { // TODO OPTIMISE! let points = graphicsData.points; @@ -224,3 +246,46 @@ indices.push(indexStart - 1); } + +/** + * Builds a line to draw using the gl.drawArrays(gl.LINES) method + * + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @private + * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties + * @param {object} webGLData - an object containing all the webGL-specific information to create this shape + */ +function buildNativeLine(graphicsData, webGLData) +{ + let i = 0; + const points = graphicsData.points; + + if (points.length === 0) return; + + const verts = webGLData.points; + const length = points.length / 2; + + // sort color + const color = hex2rgb(graphicsData.lineColor); + const alpha = graphicsData.lineAlpha; + const r = color[0] * alpha; + const g = color[1] * alpha; + const b = color[2] * alpha; + + for (i = 1; i < length; i++) + { + const p1x = points[(i - 1) * 2]; + const p1y = points[((i - 1) * 2) + 1]; + + const p2x = points[i * 2]; + const p2y = points[(i * 2) + 1]; + + verts.push(p1x, p1y); + verts.push(r, g, b, alpha); + + verts.push(p2x, p2y); + verts.push(r, g, b, alpha); + } +} diff --git a/src/core/graphics/webgl/utils/buildRoundedRectangle.js b/src/core/graphics/webgl/utils/buildRoundedRectangle.js index 9abc807..a6c8461 100644 --- a/src/core/graphics/webgl/utils/buildRoundedRectangle.js +++ b/src/core/graphics/webgl/utils/buildRoundedRectangle.js @@ -77,6 +77,26 @@ } /** + * Calculate a single point for a quadratic bezier curve. + * Utility function used by quadraticBezierCurve. + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @private + * @param {number} n1 - first number + * @param {number} n2 - second number + * @param {number} perc - percentage + * @return {number} the result + * + */ +function getPt(n1, n2, perc) +{ + const diff = n2 - n1; + + return n1 + (diff * perc); +} + +/** * Calculate the points for a quadratic bezier curve. (helper function..) * Based on: https://stackoverflow.com/questions/785097/how-do-i-implement-a-bezier-curve-in-c * @@ -105,13 +125,6 @@ let x = 0; let y = 0; - function getPt(n1, n2, perc) - { - const diff = n2 - n1; - - return n1 + (diff * perc); - } - for (let i = 0, j = 0; i <= n; ++i) { j = i / n; diff --git a/src/core/index.js b/src/core/index.js index f7a817c..4f414e8 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -30,6 +30,7 @@ export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; export { default as CanvasGraphicsRenderer } from './graphics/canvas/CanvasGraphicsRenderer'; +export { default as Spritesheet } from './textures/Spritesheet'; export { default as Texture } from './textures/Texture'; export { default as BaseTexture } from './textures/BaseTexture'; export { default as RenderTexture } from './textures/RenderTexture'; diff --git a/src/core/math/Matrix.js b/src/core/math/Matrix.js index 0b77477..3679860 100644 --- a/src/core/math/Matrix.js +++ b/src/core/math/Matrix.js @@ -13,45 +13,50 @@ export default class Matrix { /** - * + * @param {number} [a=1] - x scale + * @param {number} [b=0] - y skew + * @param {number} [c=0] - x skew + * @param {number} [d=1] - y scale + * @param {number} [tx=0] - x translation + * @param {number} [ty=0] - y translation */ - constructor() + constructor(a = 1, b = 0, c = 0, d = 1, tx = 0, ty = 0) { /** * @member {number} * @default 1 */ - this.a = 1; + this.a = a; /** * @member {number} * @default 0 */ - this.b = 0; + this.b = b; /** * @member {number} * @default 0 */ - this.c = 0; + this.c = c; /** * @member {number} * @default 1 */ - this.d = 1; + this.d = d; /** * @member {number} * @default 0 */ - this.tx = 0; + this.tx = tx; /** * @member {number} * @default 0 */ - this.ty = 0; + this.ty = ty; this.array = null; } diff --git a/src/core/renderers/SystemRenderer.js b/src/core/renderers/SystemRenderer.js index fa116a6..a24c775 100644 --- a/src/core/renderers/SystemRenderer.js +++ b/src/core/renderers/SystemRenderer.js @@ -1,5 +1,5 @@ import { sayHello, hex2string, hex2rgb } from '../utils'; -import { Matrix } from '../math'; +import { Matrix, Rectangle } from '../math'; import { RENDERER_TYPE } from '../const'; import settings from '../settings'; import Container from '../display/Container'; @@ -21,8 +21,8 @@ { /** * @param {string} system - The name of the system this renderer is for. - * @param {number} [width=800] - the width of the canvas view - * @param {number} [height=600] - the height of the canvas view + * @param {number} [screenWidth=800] - the width of the screen + * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false @@ -37,7 +37,7 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(system, width, height, options) + constructor(system, screenWidth, screenHeight, options) { super(); @@ -69,20 +69,13 @@ this.type = RENDERER_TYPE.UNKNOWN; /** - * The width of the canvas view + * Measurements of the screen. (0, 0, screenWidth, screenHeight) * - * @member {number} - * @default 800 - */ - this.width = width || 800; - - /** - * The height of the canvas view + * Its safe to use as filterArea or hitArea for whole stage * - * @member {number} - * @default 600 + * @member {PIXI.Rectangle} */ - this.height = height || 600; + this.screen = new Rectangle(0, 0, screenWidth || 800, screenHeight || 600); /** * The canvas element that everything is drawn to @@ -107,7 +100,7 @@ this.transparent = options.transparent; /** - * Whether the render view should be resized automatically + * Whether css dimensions of canvas view should be resized to screen dimensions automatically * * @member {boolean} */ @@ -192,23 +185,48 @@ } /** - * Resizes the canvas view to the specified width and height + * Same as view.width, actual number of pixels in the canvas by horizontal * - * @param {number} width - the new width of the canvas view - * @param {number} height - the new height of the canvas view + * @member {number} + * @readonly + * @default 800 */ - resize(width, height) + get width() { - this.width = width * this.resolution; - this.height = height * this.resolution; + return this.view.width; + } - this.view.width = this.width; - this.view.height = this.height; + /** + * Same as view.height, actual number of pixels in the canvas by vertical + * + * @member {number} + * @readonly + * @default 600 + */ + get height() + { + return this.view.height; + } + + /** + * Resizes the screen and canvas to the specified width and height + * Canvas dimensions are multiplied by resolution + * + * @param {number} screenWidth - the new width of the screen + * @param {number} screenHeight - the new height of the screen + */ + resize(screenWidth, screenHeight) + { + this.screen.width = screenWidth; + this.screen.height = screenHeight; + + this.view.width = screenWidth * this.resolution; + this.view.height = screenHeight * this.resolution; if (this.autoResize) { - this.view.style.width = `${this.width / this.resolution}px`; - this.view.style.height = `${this.height / this.resolution}px`; + this.view.style.width = `${screenWidth}px`; + this.view.style.height = `${screenHeight}px`; } } @@ -249,11 +267,10 @@ this.type = RENDERER_TYPE.UNKNOWN; - this.width = 0; - this.height = 0; - this.view = null; + this.screen = null; + this.resolution = 0; this.transparent = false; diff --git a/src/core/renderers/canvas/CanvasRenderer.js b/src/core/renderers/canvas/CanvasRenderer.js index 1bd2d5e..e84e49c 100644 --- a/src/core/renderers/canvas/CanvasRenderer.js +++ b/src/core/renderers/canvas/CanvasRenderer.js @@ -18,8 +18,8 @@ export default class CanvasRenderer extends SystemRenderer { /** - * @param {number} [width=800] - the width of the canvas view - * @param {number} [height=600] - the height of the canvas view + * @param {number} [screenWidth=800] - the width of the screen + * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false @@ -34,9 +34,9 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(width, height, options = {}) + constructor(screenWidth, screenHeight, options = {}) { - super('Canvas', width, height, options); + super('Canvas', screenWidth, screenHeight, options); this.type = RENDERER_TYPE.CANVAS; @@ -96,7 +96,7 @@ this.context = null; this.renderingToScreen = false; - this.resize(width, height); + this.resize(screenWidth, screenHeight); } /** @@ -283,12 +283,12 @@ * * @extends PIXI.SystemRenderer#resize * - * @param {number} width - The new width of the canvas view - * @param {number} height - The new height of the canvas view + * @param {number} screenWidth - the new width of the screen + * @param {number} screenHeight - the new height of the screen */ - resize(width, height) + resize(screenWidth, screenHeight) { - super.resize(width, height); + super.resize(screenWidth, screenHeight); // reset the scale mode.. oddly this seems to be reset when the canvas is resized. // surely a browser bug?? Let pixi fix that for you.. diff --git a/src/core/renderers/webgl/TextureManager.js b/src/core/renderers/webgl/TextureManager.js index b685c1a..8227289 100644 --- a/src/core/renderers/webgl/TextureManager.js +++ b/src/core/renderers/webgl/TextureManager.js @@ -61,7 +61,7 @@ * Updates and/or Creates a WebGL texture for the renderer's context. * * @param {PIXI.BaseTexture|PIXI.Texture} texture - the texture to update - * @param {Number} location - the location the texture will be bound to. + * @param {number} location - the location the texture will be bound to. * @return {GLTexture} The gl texture. */ updateTexture(texture, location) diff --git a/src/core/renderers/webgl/WebGLRenderer.js b/src/core/renderers/webgl/WebGLRenderer.js index e4cd8a0..830b382 100644 --- a/src/core/renderers/webgl/WebGLRenderer.js +++ b/src/core/renderers/webgl/WebGLRenderer.js @@ -31,8 +31,8 @@ { /** * - * @param {number} [width=0] - the width of the canvas view - * @param {number} [height=0] - the height of the canvas view + * @param {number} [screenWidth=800] - the width of the screen + * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false @@ -53,9 +53,9 @@ * @param {boolean} [options.legacy=false] - If true Pixi will aim to ensure compatibility * with older / less advanced devices. If you experiance unexplained flickering try setting this to true. */ - constructor(width, height, options = {}) + constructor(screenWidth, screenHeight, options = {}) { - super('WebGL', width, height, options); + super('WebGL', screenWidth, screenHeight, options); this.legacy = !!options.legacy; @@ -245,7 +245,7 @@ glCore._testingContext = gl; // setup the width/height properties and gl viewport - this.resize(this.width, this.height); + this.resize(this.screen.width, this.screen.height); } /** @@ -343,16 +343,16 @@ /** * Resizes the webGL view to the specified width and height. * - * @param {number} width - the new width of the webGL view - * @param {number} height - the new height of the webGL view + * @param {number} screenWidth - the new width of the screen + * @param {number} screenHeight - the new height of the screen */ - resize(width, height) + resize(screenWidth, screenHeight) { // if(width * this.resolution === this.width && height * this.resolution === this.height)return; - SystemRenderer.prototype.resize.call(this, width, height); + SystemRenderer.prototype.resize.call(this, screenWidth, screenHeight); - this.rootRenderTarget.resize(width, height); + this.rootRenderTarget.resize(screenWidth, screenHeight); if (this._activeRenderTarget === this.rootRenderTarget) { @@ -396,6 +396,26 @@ } /** + * Erases the render texture and fills the drawing area with a colour + * + * @param {PIXI.RenderTexture} renderTexture - The render texture to clear + * @param {number} [clearColor] - The colour + * @return {PIXI.WebGLRenderer} Returns itself. + */ + clearRenderTexture(renderTexture, clearColor) + { + const baseTexture = renderTexture.baseTexture; + const renderTarget = baseTexture._glRenderTargets[this.CONTEXT_UID]; + + if (renderTarget) + { + renderTarget.clear(clearColor); + } + + return this; + } + + /** * Binds a render texture for rendering * * @param {PIXI.RenderTexture} renderTexture - The render texture to render @@ -474,9 +494,10 @@ * Changes the current GLShader to the one given in parameter * * @param {PIXI.glCore.Shader} shader - the new glShader + * @param {boolean} [autoProject=true] - Whether automatically set the projection matrix * @return {PIXI.WebGLRenderer} Returns itself. */ - _bindGLShader(shader) + _bindGLShader(shader, autoProject) { // TODO cache if (this._activeShader !== shader) @@ -484,8 +505,14 @@ this._activeShader = shader; shader.bind(); - // automatically set the projection matrix - shader.uniforms.projectionMatrix = this._activeRenderTarget.projectionMatrix.toArray(true); + // `autoProject` normally would be a default parameter set to true + // but because of how Babel transpiles default parameters + // it hinders the performance of this method. + if (autoProject !== false) + { + // automatically set the projection matrix + shader.uniforms.projectionMatrix = this._activeRenderTarget.projectionMatrix.toArray(true); + } } return this; diff --git a/src/core/renderers/webgl/WebGLState.js b/src/core/renderers/webgl/WebGLState.js index ba6ecc5..6c846d5 100755 --- a/src/core/renderers/webgl/WebGLState.js +++ b/src/core/renderers/webgl/WebGLState.js @@ -82,18 +82,20 @@ push() { // next state.. - let state = this.stack[++this.stackIndex]; + let state = this.stack[this.stackIndex]; if (!state) { state = this.stack[this.stackIndex] = new Uint8Array(16); } + ++this.stackIndex; + // copy state.. // set active state so we can force overrides of gl state for (let i = 0; i < this.activeState.length; i++) { - this.activeState[i] = state[i]; + state[i] = this.activeState[i]; } } diff --git a/src/core/renderers/webgl/filters/Filter.js b/src/core/renderers/webgl/filters/Filter.js index 2c3eaad..4eb6128 100644 --- a/src/core/renderers/webgl/filters/Filter.js +++ b/src/core/renderers/webgl/filters/Filter.js @@ -9,7 +9,7 @@ * @memberof PIXI * @extends PIXI.Shader */ -class Filter extends Shader +export default class Filter extends Shader { /** * @param {string} [vertexSrc] - The source of the vertex shader. @@ -56,15 +56,16 @@ * @param {PIXI.RenderTarget} input - The input render target. * @param {PIXI.RenderTarget} output - The target to output to. * @param {boolean} clear - Should the output be cleared before rendering to it + * @param {object} [currentState] - It's current state of filter. + * There are some useful properties in the currentState : + * target, filters, sourceFrame, destinationFrame, renderTarget, resolution */ - apply(filterManager, input, output, clear) + apply(filterManager, input, output, clear, currentState) // eslint-disable-line no-unused-vars { // do as you please! - filterManager.applyFilter(this, input, output, clear); + filterManager.applyFilter(this, input, output, clear, currentState); // or just do a regular render.. } } - -export default Filter; diff --git a/src/core/renderers/webgl/filters/extractUniformsFromSrc.js b/src/core/renderers/webgl/filters/extractUniformsFromSrc.js index d78a0b1..4893214 100644 --- a/src/core/renderers/webgl/filters/extractUniformsFromSrc.js +++ b/src/core/renderers/webgl/filters/extractUniformsFromSrc.js @@ -12,7 +12,7 @@ function extractUniformsFromString(string) { - const maskRegex = new RegExp('^(projectionMatrix|uSampler|filterArea)$'); + const maskRegex = new RegExp('^(projectionMatrix|uSampler|filterArea|filterClamp)$'); const uniforms = {}; let nameSplit; diff --git a/src/core/renderers/webgl/managers/FilterManager.js b/src/core/renderers/webgl/managers/FilterManager.js index 43fc869..6c7422d 100644 --- a/src/core/renderers/webgl/managers/FilterManager.js +++ b/src/core/renderers/webgl/managers/FilterManager.js @@ -2,6 +2,7 @@ import RenderTarget from '../utils/RenderTarget'; import Quad from '../utils/Quad'; import { Rectangle } from '../../../math'; +import Shader from '../../../Shader'; import * as filterTransforms from '../filters/filterTransforms'; import bitTwiddle from 'bit-twiddle'; @@ -22,8 +23,6 @@ this.filters = []; this.target = null; this.resolution = 1; - - this.firstRun = true; } } @@ -155,7 +154,7 @@ if (filters.length === 1) { - filters[0].apply(this, currentState.renderTarget, lastState.renderTarget, false); + filters[0].apply(this, currentState.renderTarget, lastState.renderTarget, false, currentState); this.freePotRenderTarget(currentState.renderTarget); } else @@ -177,7 +176,7 @@ for (i = 0; i < filters.length - 1; ++i) { - filters[i].apply(this, flip, flop, true); + filters[i].apply(this, flip, flop, true, currentState); const t = flip; @@ -185,7 +184,7 @@ flop = t; } - filters[i].apply(this, flip, lastState.renderTarget, false); + filters[i].apply(this, flip, lastState.renderTarget, false, currentState); this.freePotRenderTarget(flip); this.freePotRenderTarget(flop); @@ -212,16 +211,28 @@ const renderer = this.renderer; const gl = renderer.gl; - renderer.bindShader(filter, true); + let shader = filter.glShaders[renderer.CONTEXT_UID]; - // TODO theres a better way! - // quad can be a mesh and we then should be able to pull this off without accessing the shader directly - const shader = renderer.shaderManager.getGLShader(); - - // TODO change quad to mesh.. - if (!this.firstRun) + // cacheing.. + if (!shader) { - this.firstRun = true; + if (filter.glShaderKey) + { + shader = this.shaderCache[filter.glShaderKey]; + + if (!shader) + { + shader = new Shader(this.gl, filter.vertexSrc, filter.fragmentSrc); + + filter.glShaders[renderer.CONTEXT_UID] = this.shaderCache[filter.glShaderKey] = shader; + } + } + else + { + shader = filter.glShaders[renderer.CONTEXT_UID] = new Shader(this.gl, filter.vertexSrc, filter.fragmentSrc); + } + + // TODO - this only needs to be done once? renderer.bindVao(null); this.quad.initVao(shader); @@ -244,58 +255,146 @@ renderer.maskManager.pushScissorMask(null, renderer.maskManager.scissorData); } - // renderer._bindGLShader(shader); + renderer.bindShader(shader); - let currentState; - const uniforms = filter.uniforms; + // free unit 0 for us, doesn't matter what was there + // don't try to restore it, because syncUniforms can upload it to another slot + // and it'll be a problem + const tex = this.renderer.emptyTextures[0]; - if (uniforms.filterArea) - { - currentState = this.filterData.stack[this.filterData.index]; - - const filterArea = uniforms.filterArea; - - filterArea[0] = currentState.renderTarget.size.width; - filterArea[1] = currentState.renderTarget.size.height; - filterArea[2] = currentState.sourceFrame.x; - filterArea[3] = currentState.sourceFrame.y; - - filter.uniforms.filterArea = filterArea; - } - - // use this to clamp displaced texture coords so they belong to filterArea - // see displacementFilter fragment shader for an example - if (uniforms.filterClamp) - { - currentState = this.filterData.stack[this.filterData.index]; - - const filterClamp = uniforms.filterClamp; - - filterClamp[0] = 0; - filterClamp[1] = 0; - filterClamp[2] = (currentState.sourceFrame.width - 1) / currentState.renderTarget.size.width; - filterClamp[3] = (currentState.sourceFrame.height - 1) / currentState.renderTarget.size.height; - - filter.uniforms.filterClamp = filterClamp; - } - - renderer.shaderManager.setUniforms(filter.uniforms); + this.renderer.boundTextures[0] = tex; + // this syncs the pixi filters uniforms with glsl uniforms + this.syncUniforms(shader, filter); renderer.state.setBlendMode(filter.blendMode); - // temporary bypass cache.. - const tex = this.renderer.boundTextures[0]; - gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, input.texture.texture); this.quad.vao.draw(this.renderer.gl.TRIANGLES, 6, 0); - // restore cache. gl.bindTexture(gl.TEXTURE_2D, tex._glTextures[this.renderer.CONTEXT_UID].texture); } /** + * Uploads the uniforms of the filter. + * + * @param {GLShader} shader - The underlying gl shader. + * @param {PIXI.Filter} filter - The filter we are synchronizing. + */ + syncUniforms(shader, filter) + { + const uniformData = filter.uniformData; + const uniforms = filter.uniforms; + + // 0 is reserved for the pixi texture so we start at 1! + let textureCount = 1; + let currentState; + + // filterArea and filterClamp that are handled by FilterManager directly + // they must not appear in uniformData + + if (shader.uniforms.filterArea) + { + currentState = this.filterData.stack[this.filterData.index]; + + const filterArea = shader.uniforms.filterArea; + + filterArea[0] = currentState.renderTarget.size.width; + filterArea[1] = currentState.renderTarget.size.height; + filterArea[2] = currentState.sourceFrame.x; + filterArea[3] = currentState.sourceFrame.y; + + shader.uniforms.filterArea = filterArea; + } + + // use this to clamp displaced texture coords so they belong to filterArea + // see displacementFilter fragment shader for an example + if (shader.uniforms.filterClamp) + { + currentState = currentState || this.filterData.stack[this.filterData.index]; + + const filterClamp = shader.uniforms.filterClamp; + + filterClamp[0] = 0; + filterClamp[1] = 0; + filterClamp[2] = (currentState.sourceFrame.width - 1) / currentState.renderTarget.size.width; + filterClamp[3] = (currentState.sourceFrame.height - 1) / currentState.renderTarget.size.height; + + shader.uniforms.filterClamp = filterClamp; + } + + // TODO Cacheing layer.. + for (const i in uniformData) + { + if (uniformData[i].type === 'sampler2D' && uniforms[i] !== 0) + { + if (uniforms[i].baseTexture) + { + shader.uniforms[i] = this.renderer.bindTexture(uniforms[i].baseTexture, textureCount); + } + else + { + shader.uniforms[i] = textureCount; + + // TODO + // this is helpful as renderTargets can also be set. + // Although thinking about it, we could probably + // make the filter texture cache return a RenderTexture + // rather than a renderTarget + const gl = this.renderer.gl; + + this.renderer.boundTextures[textureCount] = this.renderer.emptyTextures[textureCount]; + gl.activeTexture(gl.TEXTURE0 + textureCount); + + uniforms[i].texture.bind(); + } + + textureCount++; + } + else if (uniformData[i].type === 'mat3') + { + // check if its pixi matrix.. + if (uniforms[i].a !== undefined) + { + shader.uniforms[i] = uniforms[i].toArray(true); + } + else + { + shader.uniforms[i] = uniforms[i]; + } + } + else if (uniformData[i].type === 'vec2') + { + // check if its a point.. + if (uniforms[i].x !== undefined) + { + const val = shader.uniforms[i] || new Float32Array(2); + + val[0] = uniforms[i].x; + val[1] = uniforms[i].y; + shader.uniforms[i] = val; + } + else + { + shader.uniforms[i] = uniforms[i]; + } + } + else if (uniformData[i].type === 'float') + { + if (shader.uniforms.data[i].value !== uniformData[i]) + { + shader.uniforms[i] = uniforms[i]; + } + } + else + { + shader.uniforms[i] = uniforms[i]; + } + } + } + + /** * Gets a render target from the pool, or creates a new one. * * @param {boolean} clear - Should we clear the render texture when we get it? @@ -442,7 +541,6 @@ renderTarget.resolution = resolution; renderTarget.defaultFrame.width = renderTarget.size.width = minWidth / resolution; renderTarget.defaultFrame.height = renderTarget.size.height = minHeight / resolution; - renderTarget.attachStencilBuffer(); return renderTarget; } diff --git a/src/core/settings.js b/src/core/settings.js index bb08cb0..f4dd087 100644 --- a/src/core/settings.js +++ b/src/core/settings.js @@ -77,11 +77,11 @@ * * @static * @memberof PIXI.settings - * @type {RegExp|string} + * @type {RegExp} * @example `@2x` - * @default /@(.+)x/ + * @default /@([0-9\.]+)x/ */ - RETINA_PREFIX: /@(.+)x/, + RETINA_PREFIX: /@([0-9\.]+)x/, /** * The default render options if none are supplied to {@link PIXI.WebGLRenderer} @@ -175,14 +175,24 @@ SCALE_MODE: 0, /** - * Default specify float precision in shaders. + * Default specify float precision in vertex shader. + * + * @static + * @memberof PIXI.settings + * @type {PIXI.PRECISION} + * @default PIXI.PRECISION.HIGH + */ + PRECISION_VERTEX: 'highp', + + /** + * Default specify float precision in fragment shader. * * @static * @memberof PIXI.settings * @type {PIXI.PRECISION} * @default PIXI.PRECISION.MEDIUM */ - PRECISION: 'mediump', + PRECISION_FRAGMENT: 'mediump', /** * Can we upload the same buffer in a single frame? diff --git a/src/core/sprites/Sprite.js b/src/core/sprites/Sprite.js index 28b0b18..121bf58 100644 --- a/src/core/sprites/Sprite.js +++ b/src/core/sprites/Sprite.js @@ -212,11 +212,11 @@ } else { - w0 = orig.width * (1 - anchor._x); - w1 = orig.width * -anchor._x; + w1 = -anchor._x * orig.width; + w0 = w1 + orig.width; - h0 = orig.height * (1 - anchor._y); - h1 = orig.height * -anchor._y; + h1 = -anchor._y * orig.height; + h0 = h1 + orig.height; } // xy @@ -269,11 +269,11 @@ const tx = wt.tx; const ty = wt.ty; - const w0 = (orig.width) * (1 - anchor._x); - const w1 = (orig.width) * -anchor._x; + const w1 = -anchor._x * orig.width; + const w0 = w1 + orig.width; - const h0 = orig.height * (1 - anchor._y); - const h1 = orig.height * -anchor._y; + const h1 = -anchor._y * orig.height; + const h0 = h1 + orig.height; // xy vertexData[0] = (a * w1) + (c * h1) + tx; @@ -440,7 +440,7 @@ * * @static * @param {number|string|PIXI.BaseTexture|HTMLCanvasElement|HTMLVideoElement} source Source to create texture from - * @return {PIXI.Sprite} The newly created texture + * @return {PIXI.Sprite} The newly created sprite */ static from(source) { @@ -538,8 +538,8 @@ } /** - * The tint applied to the sprite. This is a hex value. A value of - * 0xFFFFFF will remove any tint effect. + * The tint applied to the sprite. This is a hex value. + * A value of 0xFFFFFF will remove any tint effect. * * @member {number} * @default 0xFFFFFF diff --git a/src/core/sprites/canvas/CanvasTinter.js b/src/core/sprites/canvas/CanvasTinter.js index 1cf316b..27e5e0c 100644 --- a/src/core/sprites/canvas/CanvasTinter.js +++ b/src/core/sprites/canvas/CanvasTinter.js @@ -75,8 +75,8 @@ crop.width *= resolution; crop.height *= resolution; - canvas.width = crop.width; - canvas.height = crop.height; + canvas.width = Math.ceil(crop.width); + canvas.height = Math.ceil(crop.height); context.fillStyle = `#${(`00000${(color | 0).toString(16)}`).substr(-6)}`; @@ -130,8 +130,8 @@ crop.width *= resolution; crop.height *= resolution; - canvas.width = crop.width; - canvas.height = crop.height; + canvas.width = Math.ceil(crop.width); + canvas.height = Math.ceil(crop.height); context.globalCompositeOperation = 'copy'; context.fillStyle = `#${(`00000${(color | 0).toString(16)}`).substr(-6)}`; @@ -172,8 +172,8 @@ crop.width *= resolution; crop.height *= resolution; - canvas.width = crop.width; - canvas.height = crop.height; + canvas.width = Math.ceil(crop.width); + canvas.height = Math.ceil(crop.height); context.globalCompositeOperation = 'copy'; context.drawImage( diff --git a/src/core/sprites/webgl/SpriteRenderer.js b/src/core/sprites/webgl/SpriteRenderer.js index 2affcd5..7c352e2 100644 --- a/src/core/sprites/webgl/SpriteRenderer.js +++ b/src/core/sprites/webgl/SpriteRenderer.js @@ -30,7 +30,7 @@ /** * Number of values sent in the vertex buffer. - * positionX, positionY, colorR, colorG, colorB = 5 + * aVertexPosition(2), aTextureCoord(1), aColor(1), aTextureId(1) = 5 * * @member {number} */ @@ -75,7 +75,6 @@ this.shader = null; this.currentIndex = 0; - TICK = 0; this.groups = []; for (let k = 0; k < this.size; k++) diff --git a/src/core/textures/BaseTexture.js b/src/core/textures/BaseTexture.js index 853de7e..aeccf39 100644 --- a/src/core/textures/BaseTexture.js +++ b/src/core/textures/BaseTexture.js @@ -232,16 +232,24 @@ this.realWidth = this.source.naturalWidth || this.source.videoWidth || this.source.width; this.realHeight = this.source.naturalHeight || this.source.videoHeight || this.source.height; - this.width = this.realWidth / this.resolution; - this.height = this.realHeight / this.resolution; - - this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); + this._updateDimensions(); } this.emit('update', this); } /** + * Update dimensions from real values + */ + _updateDimensions() + { + this.width = this.realWidth / this.resolution; + this.height = this.realHeight / this.resolution; + + this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); + } + + /** * Load a source. * * If the source is not-immediately-available, such as an image that needs to be @@ -525,11 +533,7 @@ this.realWidth = Math.round(svgWidth * this.sourceScale); this.realHeight = Math.round(svgHeight * this.sourceScale); - this.width = this.realWidth / this.resolution; - this.height = this.realHeight / this.resolution; - - // Check pow2 after scale - this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); + this._updateDimensions(); // Create a canvas element const canvas = document.createElement('canvas'); @@ -690,4 +694,52 @@ return baseTexture; } + + /** + * Helper function that creates a base texture based on the source you provide. + * The source can be - image url, image element, canvas element. + * + * @static + * @param {string|HTMLImageElement|HTMLCanvasElement} source - The source to create base texture from. + * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values + * @param {number} [sourceScale=(auto)] - Scale for the original image, used with Svg images. + * @return {PIXI.BaseTexture} The new base texture. + */ + static from(source, scaleMode, sourceScale) + { + if (typeof source === 'string') + { + return BaseTexture.fromImage(source, undefined, scaleMode, sourceScale); + } + else if (source instanceof HTMLImageElement) + { + const imageUrl = source.src; + let baseTexture = BaseTextureCache[imageUrl]; + + if (!baseTexture) + { + baseTexture = new BaseTexture(source, scaleMode); + baseTexture.imageUrl = imageUrl; + + if (sourceScale) + { + baseTexture.sourceScale = sourceScale; + } + + // if there is an @2x at the end of the url we are going to assume its a highres image + baseTexture.resolution = getResolutionOfUrl(imageUrl); + + BaseTextureCache[imageUrl] = baseTexture; + } + + return baseTexture; + } + else if (source instanceof HTMLCanvasElement) + { + return BaseTexture.fromCanvas(source, scaleMode); + } + + // lets assume its a base texture! + return source; + } } diff --git a/src/core/textures/Spritesheet.js b/src/core/textures/Spritesheet.js new file mode 100644 index 0000000..d469c62 --- /dev/null +++ b/src/core/textures/Spritesheet.js @@ -0,0 +1,274 @@ +import { Rectangle, Texture } from '../'; +import { getResolutionOfUrl, TextureCache } from '../utils'; + +/** + * Utility class for maintaining reference to a collection + * of Textures on a single Spritesheet. + * + * @class + * @memberof PIXI + */ +export default class Spritesheet +{ + /** + * The maximum number of Textures to build per process. + * + * @type {number} + * @default 1000 + */ + static get BATCH_SIZE() + { + return 1000; + } + + /** + * @param {PIXI.BaseTexture} baseTexture Reference to the source BaseTexture object. + * @param {Object} data - Spritesheet image data. + * @param {string} [resolutionFilename] - The filename to consider when determining + * the resolution of the spritesheet. If not provided, the imageUrl will + * be used on the BaseTexture. + */ + constructor(baseTexture, data, resolutionFilename = null) + { + /** + * Reference to ths source texture + * @type {PIXI.BaseTexture} + */ + this.baseTexture = baseTexture; + + /** + * Map of spritesheet textures. + * @type {Object} + */ + this.textures = {}; + + /** + * Reference to the original JSON data. + * @type {Object} + */ + this.data = data; + + /** + * The resolution of the spritesheet. + * @type {number} + */ + this.resolution = this._updateResolution( + resolutionFilename || this.baseTexture.imageUrl + ); + + /** + * Map of spritesheet frames. + * @type {Object} + * @private + */ + this._frames = this.data.frames; + + /** + * Collection of frame names. + * @type {string[]} + * @private + */ + this._frameKeys = Object.keys(this._frames); + + /** + * Current batch index being processed. + * @type {number} + * @private + */ + this._batchIndex = 0; + + /** + * Callback when parse is completed. + * @type {Function} + * @private + */ + this._callback = null; + } + + /** + * Generate the resolution from the filename or fallback + * to the meta.scale field of the JSON data. + * + * @private + * @param {string} resolutionFilename - The filename to use for resolving + * the default resolution. + * @return {number} Resolution to use for spritesheet. + */ + _updateResolution(resolutionFilename) + { + const scale = this.data.meta.scale; + + // Use a defaultValue of `null` to check if a url-based resolution is set + let resolution = getResolutionOfUrl(resolutionFilename, null); + + // No resolution found via URL + if (resolution === null) + { + // Use the scale value or default to 1 + resolution = scale !== undefined ? parseFloat(scale) : 1; + } + + // For non-1 resolutions, update baseTexture + if (resolution !== 1) + { + this.baseTexture.resolution = resolution; + this.baseTexture.update(); + } + + return resolution; + } + + /** + * Parser spritesheet from loaded data. This is done asynchronously + * to prevent creating too many Texture within a single process. + * + * @param {Function} callback - Callback when complete returns + * a map of the Textures for this spritesheet. + */ + parse(callback) + { + this._batchIndex = 0; + this._callback = callback; + + if (this._frameKeys.length <= Spritesheet.BATCH_SIZE) + { + this._processFrames(0); + this._parseComplete(); + } + else + { + this._nextBatch(); + } + } + + /** + * Process a batch of frames + * + * @private + * @param {number} initialFrameIndex - The index of frame to start. + */ + _processFrames(initialFrameIndex) + { + let frameIndex = initialFrameIndex; + const maxFrames = Spritesheet.BATCH_SIZE; + + while (frameIndex - initialFrameIndex < maxFrames && frameIndex < this._frameKeys.length) + { + const i = this._frameKeys[frameIndex]; + const rect = this._frames[i].frame; + + if (rect) + { + let frame = null; + let trim = null; + const orig = new Rectangle( + 0, + 0, + this._frames[i].sourceSize.w / this.resolution, + this._frames[i].sourceSize.h / this.resolution + ); + + if (this._frames[i].rotated) + { + frame = new Rectangle( + rect.x / this.resolution, + rect.y / this.resolution, + rect.h / this.resolution, + rect.w / this.resolution + ); + } + else + { + frame = new Rectangle( + rect.x / this.resolution, + rect.y / this.resolution, + rect.w / this.resolution, + rect.h / this.resolution + ); + } + + // Check to see if the sprite is trimmed + if (this._frames[i].trimmed) + { + trim = new Rectangle( + this._frames[i].spriteSourceSize.x / this.resolution, + this._frames[i].spriteSourceSize.y / this.resolution, + rect.w / this.resolution, + rect.h / this.resolution + ); + } + + this.textures[i] = new Texture( + this.baseTexture, + frame, + orig, + trim, + this._frames[i].rotated ? 2 : 0 + ); + + // lets also add the frame to pixi's global cache for fromFrame and fromImage functions + TextureCache[i] = this.textures[i]; + } + + frameIndex++; + } + } + + /** + * The parse has completed. + * + * @private + */ + _parseComplete() + { + const callback = this._callback; + + this._callback = null; + this._batchIndex = 0; + callback.call(this, this.textures); + } + + /** + * Begin the next batch of textures. + * + * @private + */ + _nextBatch() + { + this._processFrames(this._batchIndex * Spritesheet.BATCH_SIZE); + this._batchIndex++; + setTimeout(() => + { + if (this._batchIndex * Spritesheet.BATCH_SIZE < this._frameKeys.length) + { + this._nextBatch(); + } + else + { + this._parseComplete(); + } + }, 0); + } + + /** + * Destroy Spritesheet and don't use after this. + * + * @param {boolean} [destroyBase=false] Whether to destroy the base texture as well + */ + destroy(destroyBase = false) + { + for (const i in this.textures) + { + this.textures[i].destroy(); + } + this._frames = null; + this._frameKeys = null; + this.data = null; + this.textures = null; + if (destroyBase) + { + this.baseTexture.destroy(); + } + this.baseTexture = null; + } +} diff --git a/src/core/textures/Texture.js b/src/core/textures/Texture.js index 6403d4f..afe4254 100644 --- a/src/core/textures/Texture.js +++ b/src/core/textures/Texture.js @@ -3,7 +3,7 @@ import TextureUvs from './TextureUvs'; import EventEmitter from 'eventemitter3'; import { Rectangle } from '../math'; -import { TextureCache, BaseTextureCache } from '../utils'; +import { TextureCache, BaseTextureCache, getResolutionOfUrl } from '../utils'; /** * A texture stores the information that represents an image or part of an image. It cannot be added @@ -365,7 +365,8 @@ * The source can be - frame id, image url, video url, canvas element, video element, base texture * * @static - * @param {number|string|PIXI.BaseTexture|HTMLCanvasElement|HTMLVideoElement} source - Source to create texture from + * @param {number|string|HTMLImageElement|HTMLCanvasElement|HTMLVideoElement|PIXI.BaseTexture} + * source - Source to create texture from * @return {PIXI.Texture} The newly created texture */ static from(source) @@ -393,7 +394,7 @@ } else if (source instanceof HTMLImageElement) { - return new Texture(new BaseTexture(source)); + return new Texture(BaseTexture.from(source)); } else if (source instanceof HTMLCanvasElement) { @@ -413,6 +414,43 @@ } /** + * Create a texture from a source and add to the cache. + * + * @static + * @param {HTMLImageElement|HTMLCanvasElement} source - The input source. + * @param {String} imageUrl - File name of texture, for cache and resolving resolution. + * @param {String} [name] - Human readible name for the texture cache. If no name is + * specified, only `imageUrl` will be used as the cache ID. + * @return {PIXI.Texture} Output texture + */ + static fromLoader(source, imageUrl, name) + { + const baseTexture = new BaseTexture(source, null, getResolutionOfUrl(imageUrl)); + const texture = new Texture(baseTexture); + + baseTexture.imageUrl = imageUrl; + + // No name, use imageUrl instead + if (!name) + { + name = imageUrl; + } + + // lets also add the frame to pixi's global cache for fromFrame and fromImage fucntions + BaseTextureCache[name] = baseTexture; + TextureCache[name] = texture; + + // also add references by url if they are different. + if (name !== imageUrl) + { + BaseTextureCache[imageUrl] = baseTexture; + TextureCache[imageUrl] = texture; + } + + return texture; + } + + /** * Adds a texture to the global TextureCache. This cache is shared across the whole PIXI object. * * @static @@ -522,6 +560,29 @@ } } +function createWhiteTexture() +{ + const canvas = document.createElement('canvas'); + + canvas.width = 10; + canvas.height = 10; + + const context = canvas.getContext('2d'); + + context.fillStyle = 'white'; + context.fillRect(0, 0, 10, 10); + + return new Texture(new BaseTexture(canvas)); +} + +function removeAllHandlers(tex) +{ + tex.destroy = function _emptyDestroy() { /* empty */ }; + tex.on = function _emptyOn() { /* empty */ }; + tex.once = function _emptyOnce() { /* empty */ }; + tex.emit = function _emptyEmit() { /* empty */ }; +} + /** * An empty texture, used often to not have to create multiple empty textures. * Can not be destroyed. @@ -530,7 +591,14 @@ * @constant */ Texture.EMPTY = new Texture(new BaseTexture()); -Texture.EMPTY.destroy = function _emptyDestroy() { /* empty */ }; -Texture.EMPTY.on = function _emptyOn() { /* empty */ }; -Texture.EMPTY.once = function _emptyOnce() { /* empty */ }; -Texture.EMPTY.emit = function _emptyEmit() { /* empty */ }; +removeAllHandlers(Texture.EMPTY); + +/** + * A white texture of 10x10 size, used for graphics and other things + * Can not be destroyed. + * + * @static + * @constant + */ +Texture.WHITE = createWhiteTexture(); +removeAllHandlers(Texture.WHITE); diff --git a/src/core/ticker/Ticker.js b/src/core/ticker/Ticker.js index d98a1f0..17a1517 100644 --- a/src/core/ticker/Ticker.js +++ b/src/core/ticker/Ticker.js @@ -67,11 +67,12 @@ * is based, this value is neither capped nor scaled. * If the platform supports DOMHighResTimeStamp, * this value will have a precision of 1 µs. + * Defaults to target frame time * * @member {number} - * @default 1 / TARGET_FPMS + * @default 16.66 */ - this.elapsedMS = 1 / settings.TARGET_FPMS; // default to target frame time + this.elapsedMS = 1 / settings.TARGET_FPMS; /** * The last time {@link PIXI.ticker.Ticker#update} was invoked. diff --git a/src/core/utils/index.js b/src/core/utils/index.js index 6ff609f..be44750 100644 --- a/src/core/utils/index.js +++ b/src/core/utils/index.js @@ -2,6 +2,7 @@ import settings from '../settings'; import EventEmitter from 'eventemitter3'; import pluginTarget from './pluginTarget'; +import * as mixins from './mixin'; import * as isMobile from 'ismobilejs'; let nextUid = 0; @@ -33,6 +34,7 @@ * @type {mixin} */ pluginTarget, + mixins, }; /** @@ -93,7 +95,7 @@ */ export function rgb2hex(rgb) { - return (((rgb[0] * 255) << 16) + ((rgb[1] * 255) << 8) + (rgb[2] * 255)); + return (((rgb[0] * 255) << 16) + ((rgb[1] * 255) << 8) + (rgb[2] * 255 | 0)); } /** @@ -368,3 +370,43 @@ * @private */ export const ProgramCache = {}; + +/** + * Destroys all texture in the cache + * + * @memberof PIXI.utils + * @function destroyTextureCache + */ +export function destroyTextureCache() +{ + let key; + + for (key in TextureCache) + { + TextureCache[key].destroy(); + } + for (key in BaseTextureCache) + { + BaseTextureCache[key].destroy(); + } +} + +/** + * Removes all textures from cache, but does not destroy them + * + * @memberof PIXI.utils + * @function clearTextureCache + */ +export function clearTextureCache() +{ + let key; + + for (key in TextureCache) + { + delete TextureCache[key]; + } + for (key in BaseTextureCache) + { + delete BaseTextureCache[key]; + } +} diff --git a/src/core/utils/mixin.js b/src/core/utils/mixin.js new file mode 100644 index 0000000..114de2a --- /dev/null +++ b/src/core/utils/mixin.js @@ -0,0 +1,59 @@ +/** + * Mixes all enumerable properties and methods from a source object to a target object. + * + * @memberof PIXI.utils.mixins + * @function mixin + * @param {object} target The prototype or instance that properties and methods should be added to. + * @param {object} source The source of properties and methods to mix in. + */ +export function mixin(target, source) +{ + if (!target || !source) return; + // in ES8/ES2017, this would be really easy: + // Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); + + // get all the enumerable property keys + const keys = Object.keys(source); + + // loop through properties + for (let i = 0; i < keys.length; ++i) + { + const propertyName = keys[i]; + + // Set the property using the property descriptor - this works for accessors and normal value properties + Object.defineProperty(target, propertyName, Object.getOwnPropertyDescriptor(source, propertyName)); + } +} + +const mixins = []; + +/** + * Queues a mixin to be handled towards the end of the initialization of PIXI, so that deprecation + * can take effect. + * + * @memberof PIXI.utils.mixins + * @function delayMixin + * @private + * @param {object} target The prototype or instance that properties and methods should be added to. + * @param {object} source The source of properties and methods to mix in. + */ +export function delayMixin(target, source) +{ + mixins.push(target, source); +} + +/** + * Handles all mixins queued via delayMixin(). + * + * @memberof PIXI.utils.mixins + * @function performMixins + * @private + */ +export function performMixins() +{ + for (let i = 0; i < mixins.length; i += 2) + { + mixin(mixins[i], mixins[i + 1]); + } + mixins.length = 0; +} diff --git a/src/deprecation.js b/src/deprecation.js index 045a19f..b9c73b3 100644 --- a/src/deprecation.js +++ b/src/deprecation.js @@ -5,6 +5,7 @@ import * as filters from './filters'; import * as prepare from './prepare'; import * as loaders from './loaders'; +import * as interaction from './interaction'; // provide method to give a stack track for warnings // useful for tracking-down where deprecated methods/properties/classes @@ -534,7 +535,7 @@ { parent: 'GC_MODES', target: 'GC_MODE' }, { parent: 'WRAP_MODES', target: 'WRAP_MODE' }, { parent: 'SCALE_MODES', target: 'SCALE_MODE' }, - { parent: 'PRECISION', target: 'PRECISION' }, + { parent: 'PRECISION', target: 'PRECISION_FRAGMENT' }, ]; for (let i = 0; i < defaults.length; i++) @@ -558,6 +559,32 @@ }); } +Object.defineProperties(core.settings, { + + /** + * @static + * @name PRECISION + * @memberof PIXI.settings + * @see PIXI.PRECISION + * @deprecated since version 4.4.0 + */ + PRECISION: { + enumerable: true, + get() + { + warn('PIXI.settings.PRECISION has been deprecated, please use PIXI.settings.PRECISION_FRAGMENT'); + + return core.settings.PRECISION_FRAGMENT; + }, + set(value) + { + warn('PIXI.settings.PRECISION has been deprecated, please use PIXI.settings.PRECISION_FRAGMENT'); + + core.settings.PRECISION_FRAGMENT = value; + }, + }, +}); + Object.defineProperties(extras, { /** @@ -694,7 +721,7 @@ warn('determineFontProperties is now deprecated, please use the static calculateFontProperties method, ' + 'e.g : Text.calculateFontProperties(fontStyle);'); - return Text.calculateFontProperties(fontStyle); + return core.Text.calculateFontProperties(fontStyle); }; Object.defineProperties(core.TextStyle.prototype, { @@ -1003,3 +1030,69 @@ }, }, }); + +/** + * @name PIXI.interaction.interactiveTarget#defaultCursor + * @static + * @type {number} + * @see PIXI.interaction.interactiveTarget#cursor + * @deprecated since 4.3.0 + */ +Object.defineProperty(interaction.interactiveTarget, 'defaultCursor', { + set(value) + { + warn('Property defaultCursor has been replaced with \'cursor\'. '); + this.cursor = value; + }, + get() + { + warn('Property defaultCursor has been replaced with \'cursor\'. '); + + return this.cursor; + }, + enumerable: true, +}); + +/** + * @name PIXI.interaction.InteractionManager#defaultCursorStyle + * @static + * @type {string} + * @see PIXI.interaction.InteractionManager#cursorStyles + * @deprecated since 4.3.0 + */ +Object.defineProperty(interaction.InteractionManager, 'defaultCursorStyle', { + set(value) + { + warn('Property defaultCursorStyle has been replaced with \'cursorStyles.default\'. '); + this.cursorStyles.default = value; + }, + get() + { + warn('Property defaultCursorStyle has been replaced with \'cursorStyles.default\'. '); + + return this.cursorStyles.default; + }, +}); + +/** + * @name PIXI.interaction.InteractionManager#currentCursorStyle + * @static + * @type {string} + * @see PIXI.interaction.InteractionManager#cursorStyles + * @deprecated since 4.3.0 + */ +Object.defineProperty(interaction.InteractionManager, 'currentCursorStyle', { + set(value) + { + warn('Property currentCursorStyle has been removed.' + + 'See the currentCursorMode property, which works differently.'); + this.currentCursorMode = value; + }, + get() + { + warn('Property currentCursorStyle has been removed.' + + 'See the currentCursorMode property, which works differently.'); + + return this.currentCursorMode; + }, +}); diff --git a/src/extras/BitmapText.js b/src/extras/BitmapText.js index fb6c343..0a0e150 100644 --- a/src/extras/BitmapText.js +++ b/src/extras/BitmapText.js @@ -455,6 +455,73 @@ return this._textHeight; } + + /** + * Register a bitmap font with data and a texture. + * + * @static + * @param {XMLDocument} xml - The XML document data. + * @param {PIXI.Texture} texture - Texture with all symbols. + * @return {Object} Result font object with font, size, lineHeight and char fields. + */ + static registerFont(xml, texture) + { + const data = {}; + const info = xml.getElementsByTagName('info')[0]; + const common = xml.getElementsByTagName('common')[0]; + + data.font = info.getAttribute('face'); + data.size = parseInt(info.getAttribute('size'), 10); + data.lineHeight = parseInt(common.getAttribute('lineHeight'), 10); + data.chars = {}; + + // parse letters + const letters = xml.getElementsByTagName('char'); + + for (let i = 0; i < letters.length; i++) + { + const letter = letters[i]; + const charCode = parseInt(letter.getAttribute('id'), 10); + + const textureRect = new core.Rectangle( + parseInt(letter.getAttribute('x'), 10) + texture.frame.x, + parseInt(letter.getAttribute('y'), 10) + texture.frame.y, + parseInt(letter.getAttribute('width'), 10), + parseInt(letter.getAttribute('height'), 10) + ); + + data.chars[charCode] = { + xOffset: parseInt(letter.getAttribute('xoffset'), 10), + yOffset: parseInt(letter.getAttribute('yoffset'), 10), + xAdvance: parseInt(letter.getAttribute('xadvance'), 10), + kerning: {}, + texture: new core.Texture(texture.baseTexture, textureRect), + + }; + } + + // parse kernings + const kernings = xml.getElementsByTagName('kerning'); + + for (let i = 0; i < kernings.length; i++) + { + const kerning = kernings[i]; + const first = parseInt(kerning.getAttribute('first'), 10); + const second = parseInt(kerning.getAttribute('second'), 10); + const amount = parseInt(kerning.getAttribute('amount'), 10); + + if (data.chars[second]) + { + data.chars[second].kerning[first] = amount; + } + } + + // I'm leaving this as a temporary fix so we can test the bitmap fonts in v3 + // but it's very likely to change + BitmapText.fonts[data.font] = data; + + return data; + } } BitmapText.fonts = {}; diff --git a/src/index.js b/src/index.js index 59356cf..c553427 100644 --- a/src/index.js +++ b/src/index.js @@ -16,6 +16,10 @@ import * as particles from './particles'; import * as prepare from './prepare'; +// handle mixins now, after all code has been added, including deprecation +import { utils } from './core'; +utils.mixins.performMixins(); + export { accessibility, extract, diff --git a/src/interaction/InteractionManager.js b/src/interaction/InteractionManager.js index 7d51134..8baf13a 100644 --- a/src/interaction/InteractionManager.js +++ b/src/interaction/InteractionManager.js @@ -1,18 +1,20 @@ 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'; -// Mix interactiveTarget into core.DisplayObject.prototype -Object.assign( +// Mix interactiveTarget into core.DisplayObject.prototype, after deprecation has been handled +core.utils.mixins.delayMixin( core.DisplayObject.prototype, 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 +70,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 +101,6 @@ this.eventData = new InteractionEvent(); /** - * Tiny little interactiveData pool ! - * - * @member {PIXI.interaction.InteractionData[]} - */ - this.interactiveDataPool = []; - - /** * The DOM element to bind to. * * @private @@ -153,66 +155,12 @@ */ this.supportsPointerEvents = !!window.PointerEvent; - /** - * Are touch events being 'normalized' and converted into pointer events if pointer events are not supported - * For example, on a touch screen mobile device, a touchstart would also be emitted as a pointerdown - * - * @private - * @readonly - * @member {boolean} - */ - this.normalizeTouchEvents = !this.supportsPointerEvents && this.supportsTouchEvents; - - /** - * Are mouse events being 'normalized' and converted into pointer events if pointer events are not supported - * For example, on a desktop pc, a mousedown would also be emitted as a pointerdown - * - * @private - * @readonly - * @member {boolean} - */ - this.normalizeMouseEvents = !this.supportsPointerEvents && !MobileDevice.any; - // this will make it so that you don't have to call bind all the time /** * @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 +168,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,41 +199,32 @@ this.onPointerOver = this.onPointerOver.bind(this); /** - * @private - * @member {Function} + * Dictionary of how different cursor modes are handled. Strings are handled as CSS cursor + * values, objects are handled as dictionaries of CSS values for interactionDOMElement, + * and functions are called instead of changing the CSS. + * Default CSS cursor values are provided for 'default' and 'pointer' modes. + * @member {Object.)>} */ - this.onTouchStart = this.onTouchStart.bind(this); - this.processTouchStart = this.processTouchStart.bind(this); + this.cursorStyles = { + default: 'inherit', + pointer: 'pointer', + }; /** - * @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. - * - * @member {string} - * @default 'inherit' - */ - this.defaultCursorStyle = 'inherit'; - - /** - * The css style of the cursor that is being used. + * The mode of the cursor that is being used. + * The value of this is a key from the cursorStyles dictionary. * * @member {string} */ - this.currentCursorStyle = 'inherit'; + this.currentCursorMode = null; + + /** + * Internal cached let. + * + * @private + * @member {string} + */ + this.cursor = null; /** * Internal cached let. @@ -303,6 +249,7 @@ * object. * * @event mousedown + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -311,6 +258,7 @@ * on the display object. * * @event rightdown + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -319,6 +267,7 @@ * object. * * @event mouseup + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -327,6 +276,7 @@ * over the display object. * * @event rightup + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -335,6 +285,7 @@ * the display object. * * @event click + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -343,6 +294,7 @@ * and released on the display object. * * @event rightclick + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -352,6 +304,7 @@ * [mousedown]{@link PIXI.interaction.InteractionManager#event:mousedown}. * * @event mouseupoutside + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -361,6 +314,7 @@ * [rightdown]{@link PIXI.interaction.InteractionManager#event:rightdown}. * * @event rightupoutside + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -368,6 +322,7 @@ * Fired when a pointer device (usually a mouse) is moved while over the display object * * @event mousemove + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -375,6 +330,7 @@ * Fired when a pointer device (usually a mouse) is moved onto the display object * * @event mouseover + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -382,6 +338,7 @@ * Fired when a pointer device (usually a mouse) is moved off the display object * * @event mouseout + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -389,6 +346,7 @@ * Fired when a pointer device button is pressed on the display object. * * @event pointerdown + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -396,6 +354,14 @@ * Fired when a pointer device button is released over the display object. * * @event pointerup + * @type {PIXI.interaction.InteractionData} + * @memberof PIXI.interaction.InteractionManager# + */ + + /** + * Fired when the operating system cancels a pointer event + * + * @event pointercancel * @memberof PIXI.interaction.InteractionManager# */ @@ -403,6 +369,7 @@ * Fired when a pointer device button is pressed and released on the display object. * * @event pointertap + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -411,6 +378,7 @@ * registered a [pointerdown]{@link PIXI.interaction.InteractionManager#event:pointerdown}. * * @event pointerupoutside + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -418,6 +386,7 @@ * Fired when a pointer device is moved while over the display object * * @event pointermove + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -425,6 +394,7 @@ * Fired when a pointer device is moved onto the display object * * @event pointerover + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -432,6 +402,7 @@ * Fired when a pointer device is moved off the display object * * @event pointerout + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -439,6 +410,7 @@ * Fired when a touch point is placed on the display object. * * @event touchstart + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -446,6 +418,14 @@ * Fired when a touch point is removed from the display object. * * @event touchend + * @type {PIXI.interaction.InteractionData} + * @memberof PIXI.interaction.InteractionManager# + */ + + /** + * Fired when the operating system cancels a touch + * + * @event touchcancel * @memberof PIXI.interaction.InteractionManager# */ @@ -453,6 +433,7 @@ * Fired when a touch point is placed and removed from the display object. * * @event tap + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -461,6 +442,7 @@ * registered a [touchstart]{@link PIXI.interaction.InteractionManager#event:touchstart}. * * @event touchendoutside + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -468,6 +450,7 @@ * Fired when a touch point is moved along the display object. * * @event touchmove + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ } @@ -524,45 +507,30 @@ { window.document.addEventListener('pointermove', this.onPointerMove, true); this.interactionDOMElement.addEventListener('pointerdown', this.onPointerDown, true); - this.interactionDOMElement.addEventListener('pointerout', this.onPointerOut, true); + // pointerout is fired in addition to pointerup (for touch events) and pointercancel + // we already handle those, so for the purposes of what we do in onPointerOut, we only + // care about the pointerleave event + this.interactionDOMElement.addEventListener('pointerleave', 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; @@ -596,45 +564,26 @@ { window.document.removeEventListener('pointermove', this.onPointerMove, true); this.interactionDOMElement.removeEventListener('pointerdown', this.onPointerDown, true); - this.interactionDOMElement.removeEventListener('pointerout', this.onPointerOut, true); + this.interactionDOMElement.removeEventListener('pointerleave', 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) + 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.onPointerDown, true); + this.interactionDOMElement.removeEventListener('touchcancel', this.onPointerCancel, 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); - - 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 = null; @@ -664,7 +613,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; @@ -672,25 +621,80 @@ return; } - this.cursor = this.defaultCursorStyle; + this.cursor = null; // 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(); - - this.processInteractive(this.mouse.global, this.renderer._lastObjectRendered, this.processMouseOverOut, true); - - if (this.currentCursorStyle !== this.cursor) + for (const k in this.activeInteractionData) { - this.currentCursorStyle = this.cursor; - this.interactionDOMElement.style.cursor = this.cursor; + // eslint-disable-next-line no-prototype-builtins + if (this.activeInteractionData.hasOwnProperty(k)) + { + const interactionData = this.activeInteractionData[k]; + + if (interactionData.originalEvent && interactionData.pointerType !== 'touch') + { + const interactionEvent = this.configureInteractionEventForDOMEvent( + this.eventData, + interactionData.originalEvent, + interactionData + ); + + this.processInteractive( + interactionEvent, + this.renderer._lastObjectRendered, + this.processPointerOverOut, + true + ); + } + } } + this.setCursorMode(this.cursor); + // TODO } /** + * Sets the current cursor mode, handling any callbacks or CSS style changes. + * + * @param {string} mode - cursor mode, a key from the cursorStyles dictionary + */ + setCursorMode(mode) + { + mode = mode || 'default'; + // if the mode didn't actually change, bail early + if (this.currentCursorMode === mode) + { + return; + } + this.currentCursorMode = mode; + const style = this.cursorStyles[mode]; + + // only do things if there is a cursor style for it + if (style) + { + switch (typeof style) + { + case 'string': + // string styles are handled as cursor CSS + this.interactionDOMElement.style.cursor = style; + break; + case 'function': + // functions are just called, and passed the cursor mode + style(mode); + break; + case 'object': + // if it is an object, assume that it is a dictionary of CSS styles, + // apply it to the interactionDOMElement + Object.assign(this.interactionDOMElement.style, style); + break; + } + } + } + + /** * Dispatches an event on the display object that was interacted with * * @param {PIXI.Container|PIXI.Sprite|PIXI.extras.TilingSprite} displayObject - the display object in question @@ -748,22 +752,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 @@ -787,9 +795,8 @@ { interactiveParent = false; } - - // it has a mask! Then lets hit test that before continuing.. - if (hitTest && displayObject._mask) + // it has a mask! Then lets hit test that before continuing + else if (hitTest && displayObject._mask) { if (!displayObject._mask.containsPoint(point)) { @@ -797,14 +804,7 @@ } } - // it has a filterArea! Same as mask but easier, its a rectangle - if (hitTest && displayObject.filterArea) - { - if (!displayObject.filterArea.contains(point.x, point.y)) - { - hitTest = false; - } - } + let keepHitTestingAfterChildren = hitTest; // ** FREE TIP **! If an object is not interactive or has no buttons in it // (such as a game scene!) set interactiveChildren to false for that displayObject. @@ -818,7 +818,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 @@ -838,15 +838,20 @@ // This means we no longer need to hit test anything else. We still need to run // through all objects, but we don't need to perform any hit tests. - // { - hitTest = false; - // } + keepHitTestingAfterChildren = false; + + if (child.interactive) + { + hitTest = false; + } // we can break now as we have hit an object. } } } + hitTest = keepHitTestingAfterChildren; + // no point running this if the item is not interactive or does not have an interactive parent. if (interactive) { @@ -868,14 +873,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 +886,175 @@ } /** - * 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].isNormalized) { - 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); + if (!displayObject.trackedPointers[id]) + { + displayObject.trackedPointers[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.trackedPointers[id].rightDown = true; + } + else + { + displayObject.trackedPointers[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.trackedPointers[id] !== undefined) + { + delete displayObject.trackedPointers[id]; + this.dispatchEvent(displayObject, 'pointercancel', interactionEvent); + + if (e.type === 'touchcancel' || e.pointerType === 'touch') + { + this.dispatchEvent(displayObject, 'touchcancel', interactionEvent); + } } } @@ -1159,42 +1066,94 @@ */ 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) { - if (hit) - { - this.dispatchEvent(displayObject, 'pointerup', this.eventData); + const e = interactionEvent.data.originalEvent; - if (displayObject._pointerDown) + const id = interactionEvent.data.identifier; + + const trackingData = displayObject.trackedPointers[id]; + + const isTouch = (e.type === 'touchend' || e.pointerType === 'touch'); + + const isMouse = (e.type.indexOf('mouse') === 0 || e.pointerType === 'mouse'); + + // 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) { - displayObject._pointerDown = false; - this.dispatchEvent(displayObject, 'pointertap', this.eventData); + 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); + } + // update the down state of the tracking data + if (trackingData) + { + if (isRightButton) + { + trackingData.rightDown = hit; + } + else + { + trackingData.leftDown = hit; + } } } - else if (displayObject._pointerDown) + + // Pointers and Touches, and Mouse + if (hit) { - displayObject._pointerDown = false; - this.dispatchEvent(displayObject, 'pointerupoutside', this.eventData); + this.dispatchEvent(displayObject, 'pointerup', interactionEvent); + if (isTouch) this.dispatchEvent(displayObject, 'touchend', interactionEvent); + + if (trackingData) + { + this.dispatchEvent(displayObject, 'pointertap', interactionEvent); + if (isTouch) + { + this.dispatchEvent(displayObject, 'tap', interactionEvent); + // touches are no longer over (if they ever were) when we get the touchend + // so we should ensure that we don't keep pretending that they are + trackingData.over = false; + } + } + } + else if (trackingData) + { + this.dispatchEvent(displayObject, 'pointerupoutside', interactionEvent); + if (isTouch) this.dispatchEvent(displayObject, 'touchendoutside', interactionEvent); + } + // Only remove the tracking data if there is no over/down state still associated with it + if (trackingData && trackingData.none) + { + delete displayObject.trackedPointers[id]; } } @@ -1202,39 +1161,78 @@ * 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 = null; + } - 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') + { + this.setCursorMode(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 (isMouse) { - 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 +1240,94 @@ * 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.setCursorMode(null); + } - 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'); + + let trackingData = displayObject.trackedPointers[id]; + + // if we just moused over the display object, then we need to track that state + if (hit && !trackingData) + { + trackingData = displayObject.trackedPointers[id] = new InteractionTrackingData(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); + } + } + + // only change the cursor if it has not already been changed (by something deeper in the + // display tree) + if (isMouse && this.cursor === null) + { + this.cursor = displayObject.cursor; } } - 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); + } + // if there is no mouse down information for the pointer, then it is safe to delete + if (trackingData.none) + { + delete displayObject.trackedPointers[id]; + } } } @@ -1287,253 +1335,154 @@ * 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 && pointerEvent.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; + + // mark the touch as normalized, just so that we know we did it + touch.isNormalized = true; + + normalizedEvents.push(touch); + } } - else if (this.normalizeMouseEvents) + // apparently PointerEvent subclasses MouseEvent, so yay + else if (event instanceof MouseEvent && (!this.supportsPointerEvents || !(event instanceof window.PointerEvent))) { if (typeof event.isPrimary === 'undefined') event.isPrimary = true; if (typeof event.width === 'undefined') event.width = 1; @@ -1541,10 +1490,21 @@ 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; + + // mark the mouse event as normalized, just so that we know we did it + event.isNormalized = true; + + normalizedEvents.push(event); } + else + { + normalizedEvents.push(event); + } + + return normalizedEvents; } /** @@ -1563,30 +1523,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 +1542,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..6a1f69e --- /dev/null +++ b/src/interaction/InteractionTrackingData.js @@ -0,0 +1,147 @@ +/** + * 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 inactive (not over or down)? + * + * @member {number} + * @memberof PIXI.interaction.InteractionTrackingData# + */ + get none() + { + return this._flags === this.constructor.FLAGS.NONE; + } + + /** + * 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..d252fba 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 * ); */ @@ -38,68 +38,56 @@ hitArea: null, /** - * If enabled, the mouse cursor will change when hovered over the displayObject if it is interactive + * If enabled, the mouse cursor use the pointer behavior when hovered over the displayObject if it is interactive + * Setting this changes the 'cursor' property to `'pointer'`. * - * @inner {boolean} + * @member {boolean} + * @memberof PIXI.interaction.interactiveTarget# */ - buttonMode: false, + get buttonMode() + { + return this.cursor === 'pointer'; + }, + set buttonMode(value) + { + if (value) + { + this.cursor = 'pointer'; + } + else if (this.cursor === 'pointer') + { + this.cursor = null; + } + }, /** - * If buttonMode is enabled, this defines what CSS cursor property is used when the mouse cursor - * is hovered over the displayObject + * This defines what cursor mode is used when the mouse cursor + * is hovered over the displayObject. * * @see https://developer.mozilla.org/en/docs/Web/CSS/cursor * * @inner {string} */ - defaultCursor: 'pointer', - - // some internal checks.. - /** - * Internal check to detect if the mouse cursor is hovered over the displayObject - * - * @inner {boolean} - * @private - */ - _over: false, + cursor: null, /** - * Internal check to detect if the left mouse button is pressed on the displayObject + * Internal set of all active pointers, by identifier * - * @inner {boolean} + * @member {Map} + * @memberof PIXI.interaction.interactiveTarget# * @private */ - _isLeftDown: false, + get trackedPointers() + { + if (this._trackedPointers === undefined) this._trackedPointers = {}; + + return this._trackedPointers; + }, /** - * Internal check to detect if the right mouse button is pressed on the displayObject + * Map of all tracked pointers, by identifier. Use trackedPointers to access. * - * @inner {boolean} - * @private + * @private {Map} */ - _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, + _trackedPointers: undefined, }; diff --git a/package.json b/package.json index 6a366ba..71ade15 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pixi.js", - "version": "4.3.4", + "version": "4.3.5", "description": "Pixi.js is a fast lightweight 2D library that works across all devices.", "author": "Mat Groves", "contributors": [ @@ -61,7 +61,7 @@ "ismobilejs": "^0.4.0", "object-assign": "^4.0.1", "pixi-gl-core": "^1.0.3", - "resource-loader": "^2.0.4" + "resource-loader": "^2.0.6" }, "devDependencies": { "babel-cli": "^6.18.0", @@ -70,9 +70,9 @@ "babel-preset-es2015": "^6.14.0", "babelify": "^7.3.0", "del": "^2.2.0", - "electron-prebuilt": "^1.3.2", + "electron": "^1.4.15", "eslint": "^3.5.0", - "floss": "^1.2.0", + "floss": "^2.0.1", "gh-pages": "^0.11.0", "jaguarjs-jsdoc": "^1.0.1", "js-md5": "^0.4.1", diff --git a/src/accessibility/AccessibilityManager.js b/src/accessibility/AccessibilityManager.js index df2716a..f16bcb1 100644 --- a/src/accessibility/AccessibilityManager.js +++ b/src/accessibility/AccessibilityManager.js @@ -3,7 +3,7 @@ import accessibleTarget from './accessibleTarget'; // add some extra variables to the container.. -Object.assign( +core.utils.mixins.delayMixin( core.DisplayObject.prototype, accessibleTarget ); diff --git a/src/core/Application.js b/src/core/Application.js index ea0f058..5340578 100644 --- a/src/core/Application.js +++ b/src/core/Application.js @@ -1,6 +1,6 @@ import { autoDetectRenderer } from './autoDetectRenderer'; import Container from './display/Container'; -import Ticker from './ticker/Ticker'; +import { shared, Ticker } from './ticker'; /** * Convenience class to create a new PIXI application. @@ -35,9 +35,9 @@ * @param {boolean} [noWebGL=false] - prevents selection of WebGL renderer, even if such is present * @param {boolean} [options.legacy=false] - If true Pixi will aim to ensure compatibility * with older / less advanced devices. If you experience unexplained flickering try setting this to true. - + * @param {boolean} [useSharedTicker=false] - `true` to use PIXI.ticker.shared, `false` to create new ticker. */ - constructor(width, height, options, noWebGL) + constructor(width, height, options, noWebGL, useSharedTicker = false) { /** * WebGL renderer if available, otherwise CanvasRenderer @@ -46,23 +46,46 @@ this.renderer = autoDetectRenderer(width, height, options, noWebGL); /** - * The root display container that's renderered. + * The root display container that's rendered. * @member {PIXI.Container} */ this.stage = new Container(); /** + * Internal reference to the ticker + * @member {PIXI.ticker.Ticker} + * @private + */ + this._ticker = null; + + /** * Ticker for doing render updates. * @member {PIXI.ticker.Ticker} + * @default PIXI.ticker.shared */ - this.ticker = new Ticker(); - - this.ticker.add(this.render, this); + this.ticker = useSharedTicker ? shared : new Ticker(); // Start the rendering this.start(); } + set ticker(ticker) // eslint-disable-line require-jsdoc + { + if (this._ticker) + { + this._ticker.remove(this.render, this); + } + this._ticker = ticker; + if (ticker) + { + ticker.add(this.render, this); + } + } + get ticker() // eslint-disable-line require-jsdoc + { + return this._ticker; + } + /** * Render the current stage. */ @@ -76,7 +99,7 @@ */ stop() { - this.ticker.stop(); + this._ticker.stop(); } /** @@ -84,7 +107,7 @@ */ start() { - this.ticker.start(); + this._ticker.start(); } /** @@ -98,13 +121,22 @@ } /** + * Reference to the renderer's screen rectangle. Its safe to use as filterArea or hitArea for whole screen + * @member {PIXI.Rectangle} + * @readonly + */ + get screen() + { + return this.renderer.screen; + } + + /** * Destroy and don't use after this. * @param {Boolean} [removeView=false] Automatically remove canvas from DOM. */ destroy(removeView) { this.stop(); - this.ticker.remove(this.render, this); this.ticker = null; this.stage.destroy(); diff --git a/src/core/Shader.js b/src/core/Shader.js new file mode 100644 index 0000000..78b17e6 --- /dev/null +++ b/src/core/Shader.js @@ -0,0 +1,46 @@ +import { GLShader } from 'pixi-gl-core'; +import settings from './settings'; + +function checkPrecision(src, def) +{ + if (src instanceof Array) + { + if (src[0].substring(0, 9) !== 'precision') + { + const copy = src.slice(0); + + copy.unshift(`precision ${def} float;`); + + return copy; + } + } + else if (src.substring(0, 9) !== 'precision') + { + return `precision ${def} float;\n${src}`; + } + + return src; +} + +/** + * Wrapper class, webGL Shader for Pixi. + * Adds precision string if vertexSrc or fragmentSrc have no mention of it. + * + * @class + * @extends GLShader + * @memberof PIXI + */ +export default class Shader extends GLShader +{ + /** + * + * @param {WebGLRenderingContext} gl - The current WebGL rendering context + * @param {string|string[]} vertexSrc - The vertex shader source as an array of strings. + * @param {string|string[]} fragmentSrc - The fragment shader source as an array of strings. + */ + constructor(gl, vertexSrc, fragmentSrc) + { + super(gl, checkPrecision(vertexSrc, settings.PRECISION_VERTEX), + checkPrecision(fragmentSrc, settings.PRECISION_FRAGMENT)); + } +} diff --git a/src/core/const.js b/src/core/const.js index ef6f662..3de0efe 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -188,7 +188,7 @@ * The gc modes that are supported by pixi. * * The {@link PIXI.settings.GC_MODE} Garbage Collection mode for pixi textures is AUTO - * If set to GC_MODE, the renderer will occasianally check textures usage. If they are not + * If set to GC_MODE, the renderer will occasionally check textures usage. If they are not * used for a specified period of time they will be removed from the GPU. They will of course * be uploaded again when they are required. This is a silent behind the scenes process that * should ensure that the GPU does not get filled up. diff --git a/src/core/display/Container.js b/src/core/display/Container.js index ed8cd5f..17fde8d 100644 --- a/src/core/display/Container.js +++ b/src/core/display/Container.js @@ -73,11 +73,12 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.push(child); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -108,11 +109,12 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.splice(index, 0, child); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -225,10 +227,11 @@ if (index === -1) return null; child.parent = null; + // ensure child transform will be recalculated + child.transform._parentID = -1; removeItems(this.children, index, 1); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -249,11 +252,12 @@ { const child = this.getChildAt(index); + // ensure child transform will be recalculated.. child.parent = null; + child.transform._parentID = -1; removeItems(this.children, index, 1); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -284,13 +288,12 @@ for (let i = 0; i < removed.length; ++i) { removed[i].parent = null; + if (removed[i].transform) + { + removed[i].transform._parentID = -1; + } } - // ensure a transform will be recalculated.. - if (this.transform) - { - this.transform._parentID = -1; - } this._boundsID++; this.onChildrenChange(beginIndex); diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index f37f2f5..be4c4fb 100644 --- a/src/core/graphics/Graphics.js +++ b/src/core/graphics/Graphics.js @@ -28,8 +28,9 @@ { /** * + * @param {boolean} [nativeLines=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP */ - constructor() + constructor(nativeLines = false) { super(); @@ -50,6 +51,13 @@ this.lineWidth = 0; /** + * If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * + * @member {boolean} + */ + this.nativeLines = nativeLines; + + /** * The color of any lines drawn. * * @member {string} @@ -757,28 +765,14 @@ if (!this._spriteRect) { - if (!Graphics._SPRITE_TEXTURE) - { - Graphics._SPRITE_TEXTURE = RenderTexture.create(10, 10); - - const canvas = document.createElement('canvas'); - - canvas.width = 10; - canvas.height = 10; - - const context = canvas.getContext('2d'); - - context.fillStyle = 'white'; - context.fillRect(0, 0, 10, 10); - - Graphics._SPRITE_TEXTURE = Texture.fromCanvas(canvas); - } - - this._spriteRect = new Sprite(Graphics._SPRITE_TEXTURE); + this._spriteRect = new Sprite(new Texture(Texture.WHITE)); } + + const sprite = this._spriteRect; + if (this.tint === 0xffffff) { - this._spriteRect.tint = this.graphicsData[0].fillColor; + sprite.tint = this.graphicsData[0].fillColor; } else { @@ -792,20 +786,21 @@ t1[1] *= t2[1]; t1[2] *= t2[2]; - this._spriteRect.tint = rgb2hex(t1); + sprite.tint = rgb2hex(t1); } - this._spriteRect.alpha = this.graphicsData[0].fillAlpha; - this._spriteRect.worldAlpha = this.worldAlpha * this._spriteRect.alpha; + sprite.alpha = this.graphicsData[0].fillAlpha; + sprite.worldAlpha = this.worldAlpha * sprite.alpha; + sprite.blendMode = this.blendMode; - Graphics._SPRITE_TEXTURE._frame.width = rect.width; - Graphics._SPRITE_TEXTURE._frame.height = rect.height; + sprite.texture._frame.width = rect.width; + sprite.texture._frame.height = rect.height; - this._spriteRect.transform.worldTransform = this.transform.worldTransform; + sprite.transform.worldTransform = this.transform.worldTransform; - this._spriteRect.anchor.set(-rect.x / rect.width, -rect.y / rect.height); - this._spriteRect._onAnchorUpdate(); + sprite.anchor.set(-rect.x / rect.width, -rect.y / rect.height); + sprite._onAnchorUpdate(); - this._spriteRect._renderWebGL(renderer); + sprite._renderWebGL(renderer); } /** @@ -1031,6 +1026,7 @@ this.fillColor, this.fillAlpha, this.filling, + this.nativeLines, shape ); diff --git a/src/core/graphics/GraphicsData.js b/src/core/graphics/GraphicsData.js index f5a87ae..b6f03c0 100644 --- a/src/core/graphics/GraphicsData.js +++ b/src/core/graphics/GraphicsData.js @@ -14,14 +14,19 @@ * @param {number} fillColor - the color of the fill * @param {number} fillAlpha - the alpha of the fill * @param {boolean} fill - whether or not the shape is filled with a colour + * @param {boolean} nativeLines - the method for drawing lines * @param {PIXI.Circle|PIXI.Rectangle|PIXI.Ellipse|PIXI.Polygon} shape - The shape object to draw. */ - constructor(lineWidth, lineColor, lineAlpha, fillColor, fillAlpha, fill, shape) + constructor(lineWidth, lineColor, lineAlpha, fillColor, fillAlpha, fill, nativeLines, shape) { /** * @member {number} the width of the line to draw */ this.lineWidth = lineWidth; + /** + * @member {boolean} if true the liens will be draw using LINES instead of TRIANGLE_STRIP + */ + this.nativeLines = nativeLines; /** * @member {number} the color of the line to draw @@ -85,6 +90,7 @@ this.fillColor, this.fillAlpha, this.fill, + this.nativeLines, this.shape ); } diff --git a/src/core/graphics/webgl/GraphicsRenderer.js b/src/core/graphics/webgl/GraphicsRenderer.js index 14083fe..67f5e92 100644 --- a/src/core/graphics/webgl/GraphicsRenderer.js +++ b/src/core/graphics/webgl/GraphicsRenderer.js @@ -102,7 +102,15 @@ shaderTemp.uniforms.alpha = graphics.worldAlpha; renderer.bindVao(webGLData.vao); - webGLData.vao.draw(gl.TRIANGLE_STRIP, webGLData.indices.length); + + if (graphics.nativeLines) + { + gl.drawArrays(gl.LINES, 0, webGLData.points.length / 6); + } + else + { + webGLData.vao.draw(gl.TRIANGLE_STRIP, webGLData.indices.length); + } } } diff --git a/src/core/graphics/webgl/utils/buildLine.js b/src/core/graphics/webgl/utils/buildLine.js index ecf43f7..4d6862e 100644 --- a/src/core/graphics/webgl/utils/buildLine.js +++ b/src/core/graphics/webgl/utils/buildLine.js @@ -11,7 +11,29 @@ * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties * @param {object} webGLData - an object containing all the webGL-specific information to create this shape */ -export default function buildLine(graphicsData, webGLData) +export default function (graphicsData, webGLData) +{ + if (graphicsData.nativeLines) + { + buildNativeLine(graphicsData, webGLData); + } + else + { + buildLine(graphicsData, webGLData); + } +} + +/** + * Builds a line to draw using the poligon method. + * + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @private + * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties + * @param {object} webGLData - an object containing all the webGL-specific information to create this shape + */ +function buildLine(graphicsData, webGLData) { // TODO OPTIMISE! let points = graphicsData.points; @@ -224,3 +246,46 @@ indices.push(indexStart - 1); } + +/** + * Builds a line to draw using the gl.drawArrays(gl.LINES) method + * + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @private + * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties + * @param {object} webGLData - an object containing all the webGL-specific information to create this shape + */ +function buildNativeLine(graphicsData, webGLData) +{ + let i = 0; + const points = graphicsData.points; + + if (points.length === 0) return; + + const verts = webGLData.points; + const length = points.length / 2; + + // sort color + const color = hex2rgb(graphicsData.lineColor); + const alpha = graphicsData.lineAlpha; + const r = color[0] * alpha; + const g = color[1] * alpha; + const b = color[2] * alpha; + + for (i = 1; i < length; i++) + { + const p1x = points[(i - 1) * 2]; + const p1y = points[((i - 1) * 2) + 1]; + + const p2x = points[i * 2]; + const p2y = points[(i * 2) + 1]; + + verts.push(p1x, p1y); + verts.push(r, g, b, alpha); + + verts.push(p2x, p2y); + verts.push(r, g, b, alpha); + } +} diff --git a/src/core/graphics/webgl/utils/buildRoundedRectangle.js b/src/core/graphics/webgl/utils/buildRoundedRectangle.js index 9abc807..a6c8461 100644 --- a/src/core/graphics/webgl/utils/buildRoundedRectangle.js +++ b/src/core/graphics/webgl/utils/buildRoundedRectangle.js @@ -77,6 +77,26 @@ } /** + * Calculate a single point for a quadratic bezier curve. + * Utility function used by quadraticBezierCurve. + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @private + * @param {number} n1 - first number + * @param {number} n2 - second number + * @param {number} perc - percentage + * @return {number} the result + * + */ +function getPt(n1, n2, perc) +{ + const diff = n2 - n1; + + return n1 + (diff * perc); +} + +/** * Calculate the points for a quadratic bezier curve. (helper function..) * Based on: https://stackoverflow.com/questions/785097/how-do-i-implement-a-bezier-curve-in-c * @@ -105,13 +125,6 @@ let x = 0; let y = 0; - function getPt(n1, n2, perc) - { - const diff = n2 - n1; - - return n1 + (diff * perc); - } - for (let i = 0, j = 0; i <= n; ++i) { j = i / n; diff --git a/src/core/index.js b/src/core/index.js index f7a817c..4f414e8 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -30,6 +30,7 @@ export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; export { default as CanvasGraphicsRenderer } from './graphics/canvas/CanvasGraphicsRenderer'; +export { default as Spritesheet } from './textures/Spritesheet'; export { default as Texture } from './textures/Texture'; export { default as BaseTexture } from './textures/BaseTexture'; export { default as RenderTexture } from './textures/RenderTexture'; diff --git a/src/core/math/Matrix.js b/src/core/math/Matrix.js index 0b77477..3679860 100644 --- a/src/core/math/Matrix.js +++ b/src/core/math/Matrix.js @@ -13,45 +13,50 @@ export default class Matrix { /** - * + * @param {number} [a=1] - x scale + * @param {number} [b=0] - y skew + * @param {number} [c=0] - x skew + * @param {number} [d=1] - y scale + * @param {number} [tx=0] - x translation + * @param {number} [ty=0] - y translation */ - constructor() + constructor(a = 1, b = 0, c = 0, d = 1, tx = 0, ty = 0) { /** * @member {number} * @default 1 */ - this.a = 1; + this.a = a; /** * @member {number} * @default 0 */ - this.b = 0; + this.b = b; /** * @member {number} * @default 0 */ - this.c = 0; + this.c = c; /** * @member {number} * @default 1 */ - this.d = 1; + this.d = d; /** * @member {number} * @default 0 */ - this.tx = 0; + this.tx = tx; /** * @member {number} * @default 0 */ - this.ty = 0; + this.ty = ty; this.array = null; } diff --git a/src/core/renderers/SystemRenderer.js b/src/core/renderers/SystemRenderer.js index fa116a6..a24c775 100644 --- a/src/core/renderers/SystemRenderer.js +++ b/src/core/renderers/SystemRenderer.js @@ -1,5 +1,5 @@ import { sayHello, hex2string, hex2rgb } from '../utils'; -import { Matrix } from '../math'; +import { Matrix, Rectangle } from '../math'; import { RENDERER_TYPE } from '../const'; import settings from '../settings'; import Container from '../display/Container'; @@ -21,8 +21,8 @@ { /** * @param {string} system - The name of the system this renderer is for. - * @param {number} [width=800] - the width of the canvas view - * @param {number} [height=600] - the height of the canvas view + * @param {number} [screenWidth=800] - the width of the screen + * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false @@ -37,7 +37,7 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(system, width, height, options) + constructor(system, screenWidth, screenHeight, options) { super(); @@ -69,20 +69,13 @@ this.type = RENDERER_TYPE.UNKNOWN; /** - * The width of the canvas view + * Measurements of the screen. (0, 0, screenWidth, screenHeight) * - * @member {number} - * @default 800 - */ - this.width = width || 800; - - /** - * The height of the canvas view + * Its safe to use as filterArea or hitArea for whole stage * - * @member {number} - * @default 600 + * @member {PIXI.Rectangle} */ - this.height = height || 600; + this.screen = new Rectangle(0, 0, screenWidth || 800, screenHeight || 600); /** * The canvas element that everything is drawn to @@ -107,7 +100,7 @@ this.transparent = options.transparent; /** - * Whether the render view should be resized automatically + * Whether css dimensions of canvas view should be resized to screen dimensions automatically * * @member {boolean} */ @@ -192,23 +185,48 @@ } /** - * Resizes the canvas view to the specified width and height + * Same as view.width, actual number of pixels in the canvas by horizontal * - * @param {number} width - the new width of the canvas view - * @param {number} height - the new height of the canvas view + * @member {number} + * @readonly + * @default 800 */ - resize(width, height) + get width() { - this.width = width * this.resolution; - this.height = height * this.resolution; + return this.view.width; + } - this.view.width = this.width; - this.view.height = this.height; + /** + * Same as view.height, actual number of pixels in the canvas by vertical + * + * @member {number} + * @readonly + * @default 600 + */ + get height() + { + return this.view.height; + } + + /** + * Resizes the screen and canvas to the specified width and height + * Canvas dimensions are multiplied by resolution + * + * @param {number} screenWidth - the new width of the screen + * @param {number} screenHeight - the new height of the screen + */ + resize(screenWidth, screenHeight) + { + this.screen.width = screenWidth; + this.screen.height = screenHeight; + + this.view.width = screenWidth * this.resolution; + this.view.height = screenHeight * this.resolution; if (this.autoResize) { - this.view.style.width = `${this.width / this.resolution}px`; - this.view.style.height = `${this.height / this.resolution}px`; + this.view.style.width = `${screenWidth}px`; + this.view.style.height = `${screenHeight}px`; } } @@ -249,11 +267,10 @@ this.type = RENDERER_TYPE.UNKNOWN; - this.width = 0; - this.height = 0; - this.view = null; + this.screen = null; + this.resolution = 0; this.transparent = false; diff --git a/src/core/renderers/canvas/CanvasRenderer.js b/src/core/renderers/canvas/CanvasRenderer.js index 1bd2d5e..e84e49c 100644 --- a/src/core/renderers/canvas/CanvasRenderer.js +++ b/src/core/renderers/canvas/CanvasRenderer.js @@ -18,8 +18,8 @@ export default class CanvasRenderer extends SystemRenderer { /** - * @param {number} [width=800] - the width of the canvas view - * @param {number} [height=600] - the height of the canvas view + * @param {number} [screenWidth=800] - the width of the screen + * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false @@ -34,9 +34,9 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(width, height, options = {}) + constructor(screenWidth, screenHeight, options = {}) { - super('Canvas', width, height, options); + super('Canvas', screenWidth, screenHeight, options); this.type = RENDERER_TYPE.CANVAS; @@ -96,7 +96,7 @@ this.context = null; this.renderingToScreen = false; - this.resize(width, height); + this.resize(screenWidth, screenHeight); } /** @@ -283,12 +283,12 @@ * * @extends PIXI.SystemRenderer#resize * - * @param {number} width - The new width of the canvas view - * @param {number} height - The new height of the canvas view + * @param {number} screenWidth - the new width of the screen + * @param {number} screenHeight - the new height of the screen */ - resize(width, height) + resize(screenWidth, screenHeight) { - super.resize(width, height); + super.resize(screenWidth, screenHeight); // reset the scale mode.. oddly this seems to be reset when the canvas is resized. // surely a browser bug?? Let pixi fix that for you.. diff --git a/src/core/renderers/webgl/TextureManager.js b/src/core/renderers/webgl/TextureManager.js index b685c1a..8227289 100644 --- a/src/core/renderers/webgl/TextureManager.js +++ b/src/core/renderers/webgl/TextureManager.js @@ -61,7 +61,7 @@ * Updates and/or Creates a WebGL texture for the renderer's context. * * @param {PIXI.BaseTexture|PIXI.Texture} texture - the texture to update - * @param {Number} location - the location the texture will be bound to. + * @param {number} location - the location the texture will be bound to. * @return {GLTexture} The gl texture. */ updateTexture(texture, location) diff --git a/src/core/renderers/webgl/WebGLRenderer.js b/src/core/renderers/webgl/WebGLRenderer.js index e4cd8a0..830b382 100644 --- a/src/core/renderers/webgl/WebGLRenderer.js +++ b/src/core/renderers/webgl/WebGLRenderer.js @@ -31,8 +31,8 @@ { /** * - * @param {number} [width=0] - the width of the canvas view - * @param {number} [height=0] - the height of the canvas view + * @param {number} [screenWidth=800] - the width of the screen + * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false @@ -53,9 +53,9 @@ * @param {boolean} [options.legacy=false] - If true Pixi will aim to ensure compatibility * with older / less advanced devices. If you experiance unexplained flickering try setting this to true. */ - constructor(width, height, options = {}) + constructor(screenWidth, screenHeight, options = {}) { - super('WebGL', width, height, options); + super('WebGL', screenWidth, screenHeight, options); this.legacy = !!options.legacy; @@ -245,7 +245,7 @@ glCore._testingContext = gl; // setup the width/height properties and gl viewport - this.resize(this.width, this.height); + this.resize(this.screen.width, this.screen.height); } /** @@ -343,16 +343,16 @@ /** * Resizes the webGL view to the specified width and height. * - * @param {number} width - the new width of the webGL view - * @param {number} height - the new height of the webGL view + * @param {number} screenWidth - the new width of the screen + * @param {number} screenHeight - the new height of the screen */ - resize(width, height) + resize(screenWidth, screenHeight) { // if(width * this.resolution === this.width && height * this.resolution === this.height)return; - SystemRenderer.prototype.resize.call(this, width, height); + SystemRenderer.prototype.resize.call(this, screenWidth, screenHeight); - this.rootRenderTarget.resize(width, height); + this.rootRenderTarget.resize(screenWidth, screenHeight); if (this._activeRenderTarget === this.rootRenderTarget) { @@ -396,6 +396,26 @@ } /** + * Erases the render texture and fills the drawing area with a colour + * + * @param {PIXI.RenderTexture} renderTexture - The render texture to clear + * @param {number} [clearColor] - The colour + * @return {PIXI.WebGLRenderer} Returns itself. + */ + clearRenderTexture(renderTexture, clearColor) + { + const baseTexture = renderTexture.baseTexture; + const renderTarget = baseTexture._glRenderTargets[this.CONTEXT_UID]; + + if (renderTarget) + { + renderTarget.clear(clearColor); + } + + return this; + } + + /** * Binds a render texture for rendering * * @param {PIXI.RenderTexture} renderTexture - The render texture to render @@ -474,9 +494,10 @@ * Changes the current GLShader to the one given in parameter * * @param {PIXI.glCore.Shader} shader - the new glShader + * @param {boolean} [autoProject=true] - Whether automatically set the projection matrix * @return {PIXI.WebGLRenderer} Returns itself. */ - _bindGLShader(shader) + _bindGLShader(shader, autoProject) { // TODO cache if (this._activeShader !== shader) @@ -484,8 +505,14 @@ this._activeShader = shader; shader.bind(); - // automatically set the projection matrix - shader.uniforms.projectionMatrix = this._activeRenderTarget.projectionMatrix.toArray(true); + // `autoProject` normally would be a default parameter set to true + // but because of how Babel transpiles default parameters + // it hinders the performance of this method. + if (autoProject !== false) + { + // automatically set the projection matrix + shader.uniforms.projectionMatrix = this._activeRenderTarget.projectionMatrix.toArray(true); + } } return this; diff --git a/src/core/renderers/webgl/WebGLState.js b/src/core/renderers/webgl/WebGLState.js index ba6ecc5..6c846d5 100755 --- a/src/core/renderers/webgl/WebGLState.js +++ b/src/core/renderers/webgl/WebGLState.js @@ -82,18 +82,20 @@ push() { // next state.. - let state = this.stack[++this.stackIndex]; + let state = this.stack[this.stackIndex]; if (!state) { state = this.stack[this.stackIndex] = new Uint8Array(16); } + ++this.stackIndex; + // copy state.. // set active state so we can force overrides of gl state for (let i = 0; i < this.activeState.length; i++) { - this.activeState[i] = state[i]; + state[i] = this.activeState[i]; } } diff --git a/src/core/renderers/webgl/filters/Filter.js b/src/core/renderers/webgl/filters/Filter.js index 2c3eaad..4eb6128 100644 --- a/src/core/renderers/webgl/filters/Filter.js +++ b/src/core/renderers/webgl/filters/Filter.js @@ -9,7 +9,7 @@ * @memberof PIXI * @extends PIXI.Shader */ -class Filter extends Shader +export default class Filter extends Shader { /** * @param {string} [vertexSrc] - The source of the vertex shader. @@ -56,15 +56,16 @@ * @param {PIXI.RenderTarget} input - The input render target. * @param {PIXI.RenderTarget} output - The target to output to. * @param {boolean} clear - Should the output be cleared before rendering to it + * @param {object} [currentState] - It's current state of filter. + * There are some useful properties in the currentState : + * target, filters, sourceFrame, destinationFrame, renderTarget, resolution */ - apply(filterManager, input, output, clear) + apply(filterManager, input, output, clear, currentState) // eslint-disable-line no-unused-vars { // do as you please! - filterManager.applyFilter(this, input, output, clear); + filterManager.applyFilter(this, input, output, clear, currentState); // or just do a regular render.. } } - -export default Filter; diff --git a/src/core/renderers/webgl/filters/extractUniformsFromSrc.js b/src/core/renderers/webgl/filters/extractUniformsFromSrc.js index d78a0b1..4893214 100644 --- a/src/core/renderers/webgl/filters/extractUniformsFromSrc.js +++ b/src/core/renderers/webgl/filters/extractUniformsFromSrc.js @@ -12,7 +12,7 @@ function extractUniformsFromString(string) { - const maskRegex = new RegExp('^(projectionMatrix|uSampler|filterArea)$'); + const maskRegex = new RegExp('^(projectionMatrix|uSampler|filterArea|filterClamp)$'); const uniforms = {}; let nameSplit; diff --git a/src/core/renderers/webgl/managers/FilterManager.js b/src/core/renderers/webgl/managers/FilterManager.js index 43fc869..6c7422d 100644 --- a/src/core/renderers/webgl/managers/FilterManager.js +++ b/src/core/renderers/webgl/managers/FilterManager.js @@ -2,6 +2,7 @@ import RenderTarget from '../utils/RenderTarget'; import Quad from '../utils/Quad'; import { Rectangle } from '../../../math'; +import Shader from '../../../Shader'; import * as filterTransforms from '../filters/filterTransforms'; import bitTwiddle from 'bit-twiddle'; @@ -22,8 +23,6 @@ this.filters = []; this.target = null; this.resolution = 1; - - this.firstRun = true; } } @@ -155,7 +154,7 @@ if (filters.length === 1) { - filters[0].apply(this, currentState.renderTarget, lastState.renderTarget, false); + filters[0].apply(this, currentState.renderTarget, lastState.renderTarget, false, currentState); this.freePotRenderTarget(currentState.renderTarget); } else @@ -177,7 +176,7 @@ for (i = 0; i < filters.length - 1; ++i) { - filters[i].apply(this, flip, flop, true); + filters[i].apply(this, flip, flop, true, currentState); const t = flip; @@ -185,7 +184,7 @@ flop = t; } - filters[i].apply(this, flip, lastState.renderTarget, false); + filters[i].apply(this, flip, lastState.renderTarget, false, currentState); this.freePotRenderTarget(flip); this.freePotRenderTarget(flop); @@ -212,16 +211,28 @@ const renderer = this.renderer; const gl = renderer.gl; - renderer.bindShader(filter, true); + let shader = filter.glShaders[renderer.CONTEXT_UID]; - // TODO theres a better way! - // quad can be a mesh and we then should be able to pull this off without accessing the shader directly - const shader = renderer.shaderManager.getGLShader(); - - // TODO change quad to mesh.. - if (!this.firstRun) + // cacheing.. + if (!shader) { - this.firstRun = true; + if (filter.glShaderKey) + { + shader = this.shaderCache[filter.glShaderKey]; + + if (!shader) + { + shader = new Shader(this.gl, filter.vertexSrc, filter.fragmentSrc); + + filter.glShaders[renderer.CONTEXT_UID] = this.shaderCache[filter.glShaderKey] = shader; + } + } + else + { + shader = filter.glShaders[renderer.CONTEXT_UID] = new Shader(this.gl, filter.vertexSrc, filter.fragmentSrc); + } + + // TODO - this only needs to be done once? renderer.bindVao(null); this.quad.initVao(shader); @@ -244,58 +255,146 @@ renderer.maskManager.pushScissorMask(null, renderer.maskManager.scissorData); } - // renderer._bindGLShader(shader); + renderer.bindShader(shader); - let currentState; - const uniforms = filter.uniforms; + // free unit 0 for us, doesn't matter what was there + // don't try to restore it, because syncUniforms can upload it to another slot + // and it'll be a problem + const tex = this.renderer.emptyTextures[0]; - if (uniforms.filterArea) - { - currentState = this.filterData.stack[this.filterData.index]; - - const filterArea = uniforms.filterArea; - - filterArea[0] = currentState.renderTarget.size.width; - filterArea[1] = currentState.renderTarget.size.height; - filterArea[2] = currentState.sourceFrame.x; - filterArea[3] = currentState.sourceFrame.y; - - filter.uniforms.filterArea = filterArea; - } - - // use this to clamp displaced texture coords so they belong to filterArea - // see displacementFilter fragment shader for an example - if (uniforms.filterClamp) - { - currentState = this.filterData.stack[this.filterData.index]; - - const filterClamp = uniforms.filterClamp; - - filterClamp[0] = 0; - filterClamp[1] = 0; - filterClamp[2] = (currentState.sourceFrame.width - 1) / currentState.renderTarget.size.width; - filterClamp[3] = (currentState.sourceFrame.height - 1) / currentState.renderTarget.size.height; - - filter.uniforms.filterClamp = filterClamp; - } - - renderer.shaderManager.setUniforms(filter.uniforms); + this.renderer.boundTextures[0] = tex; + // this syncs the pixi filters uniforms with glsl uniforms + this.syncUniforms(shader, filter); renderer.state.setBlendMode(filter.blendMode); - // temporary bypass cache.. - const tex = this.renderer.boundTextures[0]; - gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, input.texture.texture); this.quad.vao.draw(this.renderer.gl.TRIANGLES, 6, 0); - // restore cache. gl.bindTexture(gl.TEXTURE_2D, tex._glTextures[this.renderer.CONTEXT_UID].texture); } /** + * Uploads the uniforms of the filter. + * + * @param {GLShader} shader - The underlying gl shader. + * @param {PIXI.Filter} filter - The filter we are synchronizing. + */ + syncUniforms(shader, filter) + { + const uniformData = filter.uniformData; + const uniforms = filter.uniforms; + + // 0 is reserved for the pixi texture so we start at 1! + let textureCount = 1; + let currentState; + + // filterArea and filterClamp that are handled by FilterManager directly + // they must not appear in uniformData + + if (shader.uniforms.filterArea) + { + currentState = this.filterData.stack[this.filterData.index]; + + const filterArea = shader.uniforms.filterArea; + + filterArea[0] = currentState.renderTarget.size.width; + filterArea[1] = currentState.renderTarget.size.height; + filterArea[2] = currentState.sourceFrame.x; + filterArea[3] = currentState.sourceFrame.y; + + shader.uniforms.filterArea = filterArea; + } + + // use this to clamp displaced texture coords so they belong to filterArea + // see displacementFilter fragment shader for an example + if (shader.uniforms.filterClamp) + { + currentState = currentState || this.filterData.stack[this.filterData.index]; + + const filterClamp = shader.uniforms.filterClamp; + + filterClamp[0] = 0; + filterClamp[1] = 0; + filterClamp[2] = (currentState.sourceFrame.width - 1) / currentState.renderTarget.size.width; + filterClamp[3] = (currentState.sourceFrame.height - 1) / currentState.renderTarget.size.height; + + shader.uniforms.filterClamp = filterClamp; + } + + // TODO Cacheing layer.. + for (const i in uniformData) + { + if (uniformData[i].type === 'sampler2D' && uniforms[i] !== 0) + { + if (uniforms[i].baseTexture) + { + shader.uniforms[i] = this.renderer.bindTexture(uniforms[i].baseTexture, textureCount); + } + else + { + shader.uniforms[i] = textureCount; + + // TODO + // this is helpful as renderTargets can also be set. + // Although thinking about it, we could probably + // make the filter texture cache return a RenderTexture + // rather than a renderTarget + const gl = this.renderer.gl; + + this.renderer.boundTextures[textureCount] = this.renderer.emptyTextures[textureCount]; + gl.activeTexture(gl.TEXTURE0 + textureCount); + + uniforms[i].texture.bind(); + } + + textureCount++; + } + else if (uniformData[i].type === 'mat3') + { + // check if its pixi matrix.. + if (uniforms[i].a !== undefined) + { + shader.uniforms[i] = uniforms[i].toArray(true); + } + else + { + shader.uniforms[i] = uniforms[i]; + } + } + else if (uniformData[i].type === 'vec2') + { + // check if its a point.. + if (uniforms[i].x !== undefined) + { + const val = shader.uniforms[i] || new Float32Array(2); + + val[0] = uniforms[i].x; + val[1] = uniforms[i].y; + shader.uniforms[i] = val; + } + else + { + shader.uniforms[i] = uniforms[i]; + } + } + else if (uniformData[i].type === 'float') + { + if (shader.uniforms.data[i].value !== uniformData[i]) + { + shader.uniforms[i] = uniforms[i]; + } + } + else + { + shader.uniforms[i] = uniforms[i]; + } + } + } + + /** * Gets a render target from the pool, or creates a new one. * * @param {boolean} clear - Should we clear the render texture when we get it? @@ -442,7 +541,6 @@ renderTarget.resolution = resolution; renderTarget.defaultFrame.width = renderTarget.size.width = minWidth / resolution; renderTarget.defaultFrame.height = renderTarget.size.height = minHeight / resolution; - renderTarget.attachStencilBuffer(); return renderTarget; } diff --git a/src/core/settings.js b/src/core/settings.js index bb08cb0..f4dd087 100644 --- a/src/core/settings.js +++ b/src/core/settings.js @@ -77,11 +77,11 @@ * * @static * @memberof PIXI.settings - * @type {RegExp|string} + * @type {RegExp} * @example `@2x` - * @default /@(.+)x/ + * @default /@([0-9\.]+)x/ */ - RETINA_PREFIX: /@(.+)x/, + RETINA_PREFIX: /@([0-9\.]+)x/, /** * The default render options if none are supplied to {@link PIXI.WebGLRenderer} @@ -175,14 +175,24 @@ SCALE_MODE: 0, /** - * Default specify float precision in shaders. + * Default specify float precision in vertex shader. + * + * @static + * @memberof PIXI.settings + * @type {PIXI.PRECISION} + * @default PIXI.PRECISION.HIGH + */ + PRECISION_VERTEX: 'highp', + + /** + * Default specify float precision in fragment shader. * * @static * @memberof PIXI.settings * @type {PIXI.PRECISION} * @default PIXI.PRECISION.MEDIUM */ - PRECISION: 'mediump', + PRECISION_FRAGMENT: 'mediump', /** * Can we upload the same buffer in a single frame? diff --git a/src/core/sprites/Sprite.js b/src/core/sprites/Sprite.js index 28b0b18..121bf58 100644 --- a/src/core/sprites/Sprite.js +++ b/src/core/sprites/Sprite.js @@ -212,11 +212,11 @@ } else { - w0 = orig.width * (1 - anchor._x); - w1 = orig.width * -anchor._x; + w1 = -anchor._x * orig.width; + w0 = w1 + orig.width; - h0 = orig.height * (1 - anchor._y); - h1 = orig.height * -anchor._y; + h1 = -anchor._y * orig.height; + h0 = h1 + orig.height; } // xy @@ -269,11 +269,11 @@ const tx = wt.tx; const ty = wt.ty; - const w0 = (orig.width) * (1 - anchor._x); - const w1 = (orig.width) * -anchor._x; + const w1 = -anchor._x * orig.width; + const w0 = w1 + orig.width; - const h0 = orig.height * (1 - anchor._y); - const h1 = orig.height * -anchor._y; + const h1 = -anchor._y * orig.height; + const h0 = h1 + orig.height; // xy vertexData[0] = (a * w1) + (c * h1) + tx; @@ -440,7 +440,7 @@ * * @static * @param {number|string|PIXI.BaseTexture|HTMLCanvasElement|HTMLVideoElement} source Source to create texture from - * @return {PIXI.Sprite} The newly created texture + * @return {PIXI.Sprite} The newly created sprite */ static from(source) { @@ -538,8 +538,8 @@ } /** - * The tint applied to the sprite. This is a hex value. A value of - * 0xFFFFFF will remove any tint effect. + * The tint applied to the sprite. This is a hex value. + * A value of 0xFFFFFF will remove any tint effect. * * @member {number} * @default 0xFFFFFF diff --git a/src/core/sprites/canvas/CanvasTinter.js b/src/core/sprites/canvas/CanvasTinter.js index 1cf316b..27e5e0c 100644 --- a/src/core/sprites/canvas/CanvasTinter.js +++ b/src/core/sprites/canvas/CanvasTinter.js @@ -75,8 +75,8 @@ crop.width *= resolution; crop.height *= resolution; - canvas.width = crop.width; - canvas.height = crop.height; + canvas.width = Math.ceil(crop.width); + canvas.height = Math.ceil(crop.height); context.fillStyle = `#${(`00000${(color | 0).toString(16)}`).substr(-6)}`; @@ -130,8 +130,8 @@ crop.width *= resolution; crop.height *= resolution; - canvas.width = crop.width; - canvas.height = crop.height; + canvas.width = Math.ceil(crop.width); + canvas.height = Math.ceil(crop.height); context.globalCompositeOperation = 'copy'; context.fillStyle = `#${(`00000${(color | 0).toString(16)}`).substr(-6)}`; @@ -172,8 +172,8 @@ crop.width *= resolution; crop.height *= resolution; - canvas.width = crop.width; - canvas.height = crop.height; + canvas.width = Math.ceil(crop.width); + canvas.height = Math.ceil(crop.height); context.globalCompositeOperation = 'copy'; context.drawImage( diff --git a/src/core/sprites/webgl/SpriteRenderer.js b/src/core/sprites/webgl/SpriteRenderer.js index 2affcd5..7c352e2 100644 --- a/src/core/sprites/webgl/SpriteRenderer.js +++ b/src/core/sprites/webgl/SpriteRenderer.js @@ -30,7 +30,7 @@ /** * Number of values sent in the vertex buffer. - * positionX, positionY, colorR, colorG, colorB = 5 + * aVertexPosition(2), aTextureCoord(1), aColor(1), aTextureId(1) = 5 * * @member {number} */ @@ -75,7 +75,6 @@ this.shader = null; this.currentIndex = 0; - TICK = 0; this.groups = []; for (let k = 0; k < this.size; k++) diff --git a/src/core/textures/BaseTexture.js b/src/core/textures/BaseTexture.js index 853de7e..aeccf39 100644 --- a/src/core/textures/BaseTexture.js +++ b/src/core/textures/BaseTexture.js @@ -232,16 +232,24 @@ this.realWidth = this.source.naturalWidth || this.source.videoWidth || this.source.width; this.realHeight = this.source.naturalHeight || this.source.videoHeight || this.source.height; - this.width = this.realWidth / this.resolution; - this.height = this.realHeight / this.resolution; - - this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); + this._updateDimensions(); } this.emit('update', this); } /** + * Update dimensions from real values + */ + _updateDimensions() + { + this.width = this.realWidth / this.resolution; + this.height = this.realHeight / this.resolution; + + this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); + } + + /** * Load a source. * * If the source is not-immediately-available, such as an image that needs to be @@ -525,11 +533,7 @@ this.realWidth = Math.round(svgWidth * this.sourceScale); this.realHeight = Math.round(svgHeight * this.sourceScale); - this.width = this.realWidth / this.resolution; - this.height = this.realHeight / this.resolution; - - // Check pow2 after scale - this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); + this._updateDimensions(); // Create a canvas element const canvas = document.createElement('canvas'); @@ -690,4 +694,52 @@ return baseTexture; } + + /** + * Helper function that creates a base texture based on the source you provide. + * The source can be - image url, image element, canvas element. + * + * @static + * @param {string|HTMLImageElement|HTMLCanvasElement} source - The source to create base texture from. + * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values + * @param {number} [sourceScale=(auto)] - Scale for the original image, used with Svg images. + * @return {PIXI.BaseTexture} The new base texture. + */ + static from(source, scaleMode, sourceScale) + { + if (typeof source === 'string') + { + return BaseTexture.fromImage(source, undefined, scaleMode, sourceScale); + } + else if (source instanceof HTMLImageElement) + { + const imageUrl = source.src; + let baseTexture = BaseTextureCache[imageUrl]; + + if (!baseTexture) + { + baseTexture = new BaseTexture(source, scaleMode); + baseTexture.imageUrl = imageUrl; + + if (sourceScale) + { + baseTexture.sourceScale = sourceScale; + } + + // if there is an @2x at the end of the url we are going to assume its a highres image + baseTexture.resolution = getResolutionOfUrl(imageUrl); + + BaseTextureCache[imageUrl] = baseTexture; + } + + return baseTexture; + } + else if (source instanceof HTMLCanvasElement) + { + return BaseTexture.fromCanvas(source, scaleMode); + } + + // lets assume its a base texture! + return source; + } } diff --git a/src/core/textures/Spritesheet.js b/src/core/textures/Spritesheet.js new file mode 100644 index 0000000..d469c62 --- /dev/null +++ b/src/core/textures/Spritesheet.js @@ -0,0 +1,274 @@ +import { Rectangle, Texture } from '../'; +import { getResolutionOfUrl, TextureCache } from '../utils'; + +/** + * Utility class for maintaining reference to a collection + * of Textures on a single Spritesheet. + * + * @class + * @memberof PIXI + */ +export default class Spritesheet +{ + /** + * The maximum number of Textures to build per process. + * + * @type {number} + * @default 1000 + */ + static get BATCH_SIZE() + { + return 1000; + } + + /** + * @param {PIXI.BaseTexture} baseTexture Reference to the source BaseTexture object. + * @param {Object} data - Spritesheet image data. + * @param {string} [resolutionFilename] - The filename to consider when determining + * the resolution of the spritesheet. If not provided, the imageUrl will + * be used on the BaseTexture. + */ + constructor(baseTexture, data, resolutionFilename = null) + { + /** + * Reference to ths source texture + * @type {PIXI.BaseTexture} + */ + this.baseTexture = baseTexture; + + /** + * Map of spritesheet textures. + * @type {Object} + */ + this.textures = {}; + + /** + * Reference to the original JSON data. + * @type {Object} + */ + this.data = data; + + /** + * The resolution of the spritesheet. + * @type {number} + */ + this.resolution = this._updateResolution( + resolutionFilename || this.baseTexture.imageUrl + ); + + /** + * Map of spritesheet frames. + * @type {Object} + * @private + */ + this._frames = this.data.frames; + + /** + * Collection of frame names. + * @type {string[]} + * @private + */ + this._frameKeys = Object.keys(this._frames); + + /** + * Current batch index being processed. + * @type {number} + * @private + */ + this._batchIndex = 0; + + /** + * Callback when parse is completed. + * @type {Function} + * @private + */ + this._callback = null; + } + + /** + * Generate the resolution from the filename or fallback + * to the meta.scale field of the JSON data. + * + * @private + * @param {string} resolutionFilename - The filename to use for resolving + * the default resolution. + * @return {number} Resolution to use for spritesheet. + */ + _updateResolution(resolutionFilename) + { + const scale = this.data.meta.scale; + + // Use a defaultValue of `null` to check if a url-based resolution is set + let resolution = getResolutionOfUrl(resolutionFilename, null); + + // No resolution found via URL + if (resolution === null) + { + // Use the scale value or default to 1 + resolution = scale !== undefined ? parseFloat(scale) : 1; + } + + // For non-1 resolutions, update baseTexture + if (resolution !== 1) + { + this.baseTexture.resolution = resolution; + this.baseTexture.update(); + } + + return resolution; + } + + /** + * Parser spritesheet from loaded data. This is done asynchronously + * to prevent creating too many Texture within a single process. + * + * @param {Function} callback - Callback when complete returns + * a map of the Textures for this spritesheet. + */ + parse(callback) + { + this._batchIndex = 0; + this._callback = callback; + + if (this._frameKeys.length <= Spritesheet.BATCH_SIZE) + { + this._processFrames(0); + this._parseComplete(); + } + else + { + this._nextBatch(); + } + } + + /** + * Process a batch of frames + * + * @private + * @param {number} initialFrameIndex - The index of frame to start. + */ + _processFrames(initialFrameIndex) + { + let frameIndex = initialFrameIndex; + const maxFrames = Spritesheet.BATCH_SIZE; + + while (frameIndex - initialFrameIndex < maxFrames && frameIndex < this._frameKeys.length) + { + const i = this._frameKeys[frameIndex]; + const rect = this._frames[i].frame; + + if (rect) + { + let frame = null; + let trim = null; + const orig = new Rectangle( + 0, + 0, + this._frames[i].sourceSize.w / this.resolution, + this._frames[i].sourceSize.h / this.resolution + ); + + if (this._frames[i].rotated) + { + frame = new Rectangle( + rect.x / this.resolution, + rect.y / this.resolution, + rect.h / this.resolution, + rect.w / this.resolution + ); + } + else + { + frame = new Rectangle( + rect.x / this.resolution, + rect.y / this.resolution, + rect.w / this.resolution, + rect.h / this.resolution + ); + } + + // Check to see if the sprite is trimmed + if (this._frames[i].trimmed) + { + trim = new Rectangle( + this._frames[i].spriteSourceSize.x / this.resolution, + this._frames[i].spriteSourceSize.y / this.resolution, + rect.w / this.resolution, + rect.h / this.resolution + ); + } + + this.textures[i] = new Texture( + this.baseTexture, + frame, + orig, + trim, + this._frames[i].rotated ? 2 : 0 + ); + + // lets also add the frame to pixi's global cache for fromFrame and fromImage functions + TextureCache[i] = this.textures[i]; + } + + frameIndex++; + } + } + + /** + * The parse has completed. + * + * @private + */ + _parseComplete() + { + const callback = this._callback; + + this._callback = null; + this._batchIndex = 0; + callback.call(this, this.textures); + } + + /** + * Begin the next batch of textures. + * + * @private + */ + _nextBatch() + { + this._processFrames(this._batchIndex * Spritesheet.BATCH_SIZE); + this._batchIndex++; + setTimeout(() => + { + if (this._batchIndex * Spritesheet.BATCH_SIZE < this._frameKeys.length) + { + this._nextBatch(); + } + else + { + this._parseComplete(); + } + }, 0); + } + + /** + * Destroy Spritesheet and don't use after this. + * + * @param {boolean} [destroyBase=false] Whether to destroy the base texture as well + */ + destroy(destroyBase = false) + { + for (const i in this.textures) + { + this.textures[i].destroy(); + } + this._frames = null; + this._frameKeys = null; + this.data = null; + this.textures = null; + if (destroyBase) + { + this.baseTexture.destroy(); + } + this.baseTexture = null; + } +} diff --git a/src/core/textures/Texture.js b/src/core/textures/Texture.js index 6403d4f..afe4254 100644 --- a/src/core/textures/Texture.js +++ b/src/core/textures/Texture.js @@ -3,7 +3,7 @@ import TextureUvs from './TextureUvs'; import EventEmitter from 'eventemitter3'; import { Rectangle } from '../math'; -import { TextureCache, BaseTextureCache } from '../utils'; +import { TextureCache, BaseTextureCache, getResolutionOfUrl } from '../utils'; /** * A texture stores the information that represents an image or part of an image. It cannot be added @@ -365,7 +365,8 @@ * The source can be - frame id, image url, video url, canvas element, video element, base texture * * @static - * @param {number|string|PIXI.BaseTexture|HTMLCanvasElement|HTMLVideoElement} source - Source to create texture from + * @param {number|string|HTMLImageElement|HTMLCanvasElement|HTMLVideoElement|PIXI.BaseTexture} + * source - Source to create texture from * @return {PIXI.Texture} The newly created texture */ static from(source) @@ -393,7 +394,7 @@ } else if (source instanceof HTMLImageElement) { - return new Texture(new BaseTexture(source)); + return new Texture(BaseTexture.from(source)); } else if (source instanceof HTMLCanvasElement) { @@ -413,6 +414,43 @@ } /** + * Create a texture from a source and add to the cache. + * + * @static + * @param {HTMLImageElement|HTMLCanvasElement} source - The input source. + * @param {String} imageUrl - File name of texture, for cache and resolving resolution. + * @param {String} [name] - Human readible name for the texture cache. If no name is + * specified, only `imageUrl` will be used as the cache ID. + * @return {PIXI.Texture} Output texture + */ + static fromLoader(source, imageUrl, name) + { + const baseTexture = new BaseTexture(source, null, getResolutionOfUrl(imageUrl)); + const texture = new Texture(baseTexture); + + baseTexture.imageUrl = imageUrl; + + // No name, use imageUrl instead + if (!name) + { + name = imageUrl; + } + + // lets also add the frame to pixi's global cache for fromFrame and fromImage fucntions + BaseTextureCache[name] = baseTexture; + TextureCache[name] = texture; + + // also add references by url if they are different. + if (name !== imageUrl) + { + BaseTextureCache[imageUrl] = baseTexture; + TextureCache[imageUrl] = texture; + } + + return texture; + } + + /** * Adds a texture to the global TextureCache. This cache is shared across the whole PIXI object. * * @static @@ -522,6 +560,29 @@ } } +function createWhiteTexture() +{ + const canvas = document.createElement('canvas'); + + canvas.width = 10; + canvas.height = 10; + + const context = canvas.getContext('2d'); + + context.fillStyle = 'white'; + context.fillRect(0, 0, 10, 10); + + return new Texture(new BaseTexture(canvas)); +} + +function removeAllHandlers(tex) +{ + tex.destroy = function _emptyDestroy() { /* empty */ }; + tex.on = function _emptyOn() { /* empty */ }; + tex.once = function _emptyOnce() { /* empty */ }; + tex.emit = function _emptyEmit() { /* empty */ }; +} + /** * An empty texture, used often to not have to create multiple empty textures. * Can not be destroyed. @@ -530,7 +591,14 @@ * @constant */ Texture.EMPTY = new Texture(new BaseTexture()); -Texture.EMPTY.destroy = function _emptyDestroy() { /* empty */ }; -Texture.EMPTY.on = function _emptyOn() { /* empty */ }; -Texture.EMPTY.once = function _emptyOnce() { /* empty */ }; -Texture.EMPTY.emit = function _emptyEmit() { /* empty */ }; +removeAllHandlers(Texture.EMPTY); + +/** + * A white texture of 10x10 size, used for graphics and other things + * Can not be destroyed. + * + * @static + * @constant + */ +Texture.WHITE = createWhiteTexture(); +removeAllHandlers(Texture.WHITE); diff --git a/src/core/ticker/Ticker.js b/src/core/ticker/Ticker.js index d98a1f0..17a1517 100644 --- a/src/core/ticker/Ticker.js +++ b/src/core/ticker/Ticker.js @@ -67,11 +67,12 @@ * is based, this value is neither capped nor scaled. * If the platform supports DOMHighResTimeStamp, * this value will have a precision of 1 µs. + * Defaults to target frame time * * @member {number} - * @default 1 / TARGET_FPMS + * @default 16.66 */ - this.elapsedMS = 1 / settings.TARGET_FPMS; // default to target frame time + this.elapsedMS = 1 / settings.TARGET_FPMS; /** * The last time {@link PIXI.ticker.Ticker#update} was invoked. diff --git a/src/core/utils/index.js b/src/core/utils/index.js index 6ff609f..be44750 100644 --- a/src/core/utils/index.js +++ b/src/core/utils/index.js @@ -2,6 +2,7 @@ import settings from '../settings'; import EventEmitter from 'eventemitter3'; import pluginTarget from './pluginTarget'; +import * as mixins from './mixin'; import * as isMobile from 'ismobilejs'; let nextUid = 0; @@ -33,6 +34,7 @@ * @type {mixin} */ pluginTarget, + mixins, }; /** @@ -93,7 +95,7 @@ */ export function rgb2hex(rgb) { - return (((rgb[0] * 255) << 16) + ((rgb[1] * 255) << 8) + (rgb[2] * 255)); + return (((rgb[0] * 255) << 16) + ((rgb[1] * 255) << 8) + (rgb[2] * 255 | 0)); } /** @@ -368,3 +370,43 @@ * @private */ export const ProgramCache = {}; + +/** + * Destroys all texture in the cache + * + * @memberof PIXI.utils + * @function destroyTextureCache + */ +export function destroyTextureCache() +{ + let key; + + for (key in TextureCache) + { + TextureCache[key].destroy(); + } + for (key in BaseTextureCache) + { + BaseTextureCache[key].destroy(); + } +} + +/** + * Removes all textures from cache, but does not destroy them + * + * @memberof PIXI.utils + * @function clearTextureCache + */ +export function clearTextureCache() +{ + let key; + + for (key in TextureCache) + { + delete TextureCache[key]; + } + for (key in BaseTextureCache) + { + delete BaseTextureCache[key]; + } +} diff --git a/src/core/utils/mixin.js b/src/core/utils/mixin.js new file mode 100644 index 0000000..114de2a --- /dev/null +++ b/src/core/utils/mixin.js @@ -0,0 +1,59 @@ +/** + * Mixes all enumerable properties and methods from a source object to a target object. + * + * @memberof PIXI.utils.mixins + * @function mixin + * @param {object} target The prototype or instance that properties and methods should be added to. + * @param {object} source The source of properties and methods to mix in. + */ +export function mixin(target, source) +{ + if (!target || !source) return; + // in ES8/ES2017, this would be really easy: + // Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); + + // get all the enumerable property keys + const keys = Object.keys(source); + + // loop through properties + for (let i = 0; i < keys.length; ++i) + { + const propertyName = keys[i]; + + // Set the property using the property descriptor - this works for accessors and normal value properties + Object.defineProperty(target, propertyName, Object.getOwnPropertyDescriptor(source, propertyName)); + } +} + +const mixins = []; + +/** + * Queues a mixin to be handled towards the end of the initialization of PIXI, so that deprecation + * can take effect. + * + * @memberof PIXI.utils.mixins + * @function delayMixin + * @private + * @param {object} target The prototype or instance that properties and methods should be added to. + * @param {object} source The source of properties and methods to mix in. + */ +export function delayMixin(target, source) +{ + mixins.push(target, source); +} + +/** + * Handles all mixins queued via delayMixin(). + * + * @memberof PIXI.utils.mixins + * @function performMixins + * @private + */ +export function performMixins() +{ + for (let i = 0; i < mixins.length; i += 2) + { + mixin(mixins[i], mixins[i + 1]); + } + mixins.length = 0; +} diff --git a/src/deprecation.js b/src/deprecation.js index 045a19f..b9c73b3 100644 --- a/src/deprecation.js +++ b/src/deprecation.js @@ -5,6 +5,7 @@ import * as filters from './filters'; import * as prepare from './prepare'; import * as loaders from './loaders'; +import * as interaction from './interaction'; // provide method to give a stack track for warnings // useful for tracking-down where deprecated methods/properties/classes @@ -534,7 +535,7 @@ { parent: 'GC_MODES', target: 'GC_MODE' }, { parent: 'WRAP_MODES', target: 'WRAP_MODE' }, { parent: 'SCALE_MODES', target: 'SCALE_MODE' }, - { parent: 'PRECISION', target: 'PRECISION' }, + { parent: 'PRECISION', target: 'PRECISION_FRAGMENT' }, ]; for (let i = 0; i < defaults.length; i++) @@ -558,6 +559,32 @@ }); } +Object.defineProperties(core.settings, { + + /** + * @static + * @name PRECISION + * @memberof PIXI.settings + * @see PIXI.PRECISION + * @deprecated since version 4.4.0 + */ + PRECISION: { + enumerable: true, + get() + { + warn('PIXI.settings.PRECISION has been deprecated, please use PIXI.settings.PRECISION_FRAGMENT'); + + return core.settings.PRECISION_FRAGMENT; + }, + set(value) + { + warn('PIXI.settings.PRECISION has been deprecated, please use PIXI.settings.PRECISION_FRAGMENT'); + + core.settings.PRECISION_FRAGMENT = value; + }, + }, +}); + Object.defineProperties(extras, { /** @@ -694,7 +721,7 @@ warn('determineFontProperties is now deprecated, please use the static calculateFontProperties method, ' + 'e.g : Text.calculateFontProperties(fontStyle);'); - return Text.calculateFontProperties(fontStyle); + return core.Text.calculateFontProperties(fontStyle); }; Object.defineProperties(core.TextStyle.prototype, { @@ -1003,3 +1030,69 @@ }, }, }); + +/** + * @name PIXI.interaction.interactiveTarget#defaultCursor + * @static + * @type {number} + * @see PIXI.interaction.interactiveTarget#cursor + * @deprecated since 4.3.0 + */ +Object.defineProperty(interaction.interactiveTarget, 'defaultCursor', { + set(value) + { + warn('Property defaultCursor has been replaced with \'cursor\'. '); + this.cursor = value; + }, + get() + { + warn('Property defaultCursor has been replaced with \'cursor\'. '); + + return this.cursor; + }, + enumerable: true, +}); + +/** + * @name PIXI.interaction.InteractionManager#defaultCursorStyle + * @static + * @type {string} + * @see PIXI.interaction.InteractionManager#cursorStyles + * @deprecated since 4.3.0 + */ +Object.defineProperty(interaction.InteractionManager, 'defaultCursorStyle', { + set(value) + { + warn('Property defaultCursorStyle has been replaced with \'cursorStyles.default\'. '); + this.cursorStyles.default = value; + }, + get() + { + warn('Property defaultCursorStyle has been replaced with \'cursorStyles.default\'. '); + + return this.cursorStyles.default; + }, +}); + +/** + * @name PIXI.interaction.InteractionManager#currentCursorStyle + * @static + * @type {string} + * @see PIXI.interaction.InteractionManager#cursorStyles + * @deprecated since 4.3.0 + */ +Object.defineProperty(interaction.InteractionManager, 'currentCursorStyle', { + set(value) + { + warn('Property currentCursorStyle has been removed.' + + 'See the currentCursorMode property, which works differently.'); + this.currentCursorMode = value; + }, + get() + { + warn('Property currentCursorStyle has been removed.' + + 'See the currentCursorMode property, which works differently.'); + + return this.currentCursorMode; + }, +}); diff --git a/src/extras/BitmapText.js b/src/extras/BitmapText.js index fb6c343..0a0e150 100644 --- a/src/extras/BitmapText.js +++ b/src/extras/BitmapText.js @@ -455,6 +455,73 @@ return this._textHeight; } + + /** + * Register a bitmap font with data and a texture. + * + * @static + * @param {XMLDocument} xml - The XML document data. + * @param {PIXI.Texture} texture - Texture with all symbols. + * @return {Object} Result font object with font, size, lineHeight and char fields. + */ + static registerFont(xml, texture) + { + const data = {}; + const info = xml.getElementsByTagName('info')[0]; + const common = xml.getElementsByTagName('common')[0]; + + data.font = info.getAttribute('face'); + data.size = parseInt(info.getAttribute('size'), 10); + data.lineHeight = parseInt(common.getAttribute('lineHeight'), 10); + data.chars = {}; + + // parse letters + const letters = xml.getElementsByTagName('char'); + + for (let i = 0; i < letters.length; i++) + { + const letter = letters[i]; + const charCode = parseInt(letter.getAttribute('id'), 10); + + const textureRect = new core.Rectangle( + parseInt(letter.getAttribute('x'), 10) + texture.frame.x, + parseInt(letter.getAttribute('y'), 10) + texture.frame.y, + parseInt(letter.getAttribute('width'), 10), + parseInt(letter.getAttribute('height'), 10) + ); + + data.chars[charCode] = { + xOffset: parseInt(letter.getAttribute('xoffset'), 10), + yOffset: parseInt(letter.getAttribute('yoffset'), 10), + xAdvance: parseInt(letter.getAttribute('xadvance'), 10), + kerning: {}, + texture: new core.Texture(texture.baseTexture, textureRect), + + }; + } + + // parse kernings + const kernings = xml.getElementsByTagName('kerning'); + + for (let i = 0; i < kernings.length; i++) + { + const kerning = kernings[i]; + const first = parseInt(kerning.getAttribute('first'), 10); + const second = parseInt(kerning.getAttribute('second'), 10); + const amount = parseInt(kerning.getAttribute('amount'), 10); + + if (data.chars[second]) + { + data.chars[second].kerning[first] = amount; + } + } + + // I'm leaving this as a temporary fix so we can test the bitmap fonts in v3 + // but it's very likely to change + BitmapText.fonts[data.font] = data; + + return data; + } } BitmapText.fonts = {}; diff --git a/src/index.js b/src/index.js index 59356cf..c553427 100644 --- a/src/index.js +++ b/src/index.js @@ -16,6 +16,10 @@ import * as particles from './particles'; import * as prepare from './prepare'; +// handle mixins now, after all code has been added, including deprecation +import { utils } from './core'; +utils.mixins.performMixins(); + export { accessibility, extract, diff --git a/src/interaction/InteractionManager.js b/src/interaction/InteractionManager.js index 7d51134..8baf13a 100644 --- a/src/interaction/InteractionManager.js +++ b/src/interaction/InteractionManager.js @@ -1,18 +1,20 @@ 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'; -// Mix interactiveTarget into core.DisplayObject.prototype -Object.assign( +// Mix interactiveTarget into core.DisplayObject.prototype, after deprecation has been handled +core.utils.mixins.delayMixin( core.DisplayObject.prototype, 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 +70,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 +101,6 @@ this.eventData = new InteractionEvent(); /** - * Tiny little interactiveData pool ! - * - * @member {PIXI.interaction.InteractionData[]} - */ - this.interactiveDataPool = []; - - /** * The DOM element to bind to. * * @private @@ -153,66 +155,12 @@ */ this.supportsPointerEvents = !!window.PointerEvent; - /** - * Are touch events being 'normalized' and converted into pointer events if pointer events are not supported - * For example, on a touch screen mobile device, a touchstart would also be emitted as a pointerdown - * - * @private - * @readonly - * @member {boolean} - */ - this.normalizeTouchEvents = !this.supportsPointerEvents && this.supportsTouchEvents; - - /** - * Are mouse events being 'normalized' and converted into pointer events if pointer events are not supported - * For example, on a desktop pc, a mousedown would also be emitted as a pointerdown - * - * @private - * @readonly - * @member {boolean} - */ - this.normalizeMouseEvents = !this.supportsPointerEvents && !MobileDevice.any; - // this will make it so that you don't have to call bind all the time /** * @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 +168,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,41 +199,32 @@ this.onPointerOver = this.onPointerOver.bind(this); /** - * @private - * @member {Function} + * Dictionary of how different cursor modes are handled. Strings are handled as CSS cursor + * values, objects are handled as dictionaries of CSS values for interactionDOMElement, + * and functions are called instead of changing the CSS. + * Default CSS cursor values are provided for 'default' and 'pointer' modes. + * @member {Object.)>} */ - this.onTouchStart = this.onTouchStart.bind(this); - this.processTouchStart = this.processTouchStart.bind(this); + this.cursorStyles = { + default: 'inherit', + pointer: 'pointer', + }; /** - * @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. - * - * @member {string} - * @default 'inherit' - */ - this.defaultCursorStyle = 'inherit'; - - /** - * The css style of the cursor that is being used. + * The mode of the cursor that is being used. + * The value of this is a key from the cursorStyles dictionary. * * @member {string} */ - this.currentCursorStyle = 'inherit'; + this.currentCursorMode = null; + + /** + * Internal cached let. + * + * @private + * @member {string} + */ + this.cursor = null; /** * Internal cached let. @@ -303,6 +249,7 @@ * object. * * @event mousedown + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -311,6 +258,7 @@ * on the display object. * * @event rightdown + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -319,6 +267,7 @@ * object. * * @event mouseup + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -327,6 +276,7 @@ * over the display object. * * @event rightup + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -335,6 +285,7 @@ * the display object. * * @event click + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -343,6 +294,7 @@ * and released on the display object. * * @event rightclick + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -352,6 +304,7 @@ * [mousedown]{@link PIXI.interaction.InteractionManager#event:mousedown}. * * @event mouseupoutside + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -361,6 +314,7 @@ * [rightdown]{@link PIXI.interaction.InteractionManager#event:rightdown}. * * @event rightupoutside + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -368,6 +322,7 @@ * Fired when a pointer device (usually a mouse) is moved while over the display object * * @event mousemove + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -375,6 +330,7 @@ * Fired when a pointer device (usually a mouse) is moved onto the display object * * @event mouseover + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -382,6 +338,7 @@ * Fired when a pointer device (usually a mouse) is moved off the display object * * @event mouseout + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -389,6 +346,7 @@ * Fired when a pointer device button is pressed on the display object. * * @event pointerdown + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -396,6 +354,14 @@ * Fired when a pointer device button is released over the display object. * * @event pointerup + * @type {PIXI.interaction.InteractionData} + * @memberof PIXI.interaction.InteractionManager# + */ + + /** + * Fired when the operating system cancels a pointer event + * + * @event pointercancel * @memberof PIXI.interaction.InteractionManager# */ @@ -403,6 +369,7 @@ * Fired when a pointer device button is pressed and released on the display object. * * @event pointertap + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -411,6 +378,7 @@ * registered a [pointerdown]{@link PIXI.interaction.InteractionManager#event:pointerdown}. * * @event pointerupoutside + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -418,6 +386,7 @@ * Fired when a pointer device is moved while over the display object * * @event pointermove + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -425,6 +394,7 @@ * Fired when a pointer device is moved onto the display object * * @event pointerover + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -432,6 +402,7 @@ * Fired when a pointer device is moved off the display object * * @event pointerout + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -439,6 +410,7 @@ * Fired when a touch point is placed on the display object. * * @event touchstart + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -446,6 +418,14 @@ * Fired when a touch point is removed from the display object. * * @event touchend + * @type {PIXI.interaction.InteractionData} + * @memberof PIXI.interaction.InteractionManager# + */ + + /** + * Fired when the operating system cancels a touch + * + * @event touchcancel * @memberof PIXI.interaction.InteractionManager# */ @@ -453,6 +433,7 @@ * Fired when a touch point is placed and removed from the display object. * * @event tap + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -461,6 +442,7 @@ * registered a [touchstart]{@link PIXI.interaction.InteractionManager#event:touchstart}. * * @event touchendoutside + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -468,6 +450,7 @@ * Fired when a touch point is moved along the display object. * * @event touchmove + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ } @@ -524,45 +507,30 @@ { window.document.addEventListener('pointermove', this.onPointerMove, true); this.interactionDOMElement.addEventListener('pointerdown', this.onPointerDown, true); - this.interactionDOMElement.addEventListener('pointerout', this.onPointerOut, true); + // pointerout is fired in addition to pointerup (for touch events) and pointercancel + // we already handle those, so for the purposes of what we do in onPointerOut, we only + // care about the pointerleave event + this.interactionDOMElement.addEventListener('pointerleave', 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; @@ -596,45 +564,26 @@ { window.document.removeEventListener('pointermove', this.onPointerMove, true); this.interactionDOMElement.removeEventListener('pointerdown', this.onPointerDown, true); - this.interactionDOMElement.removeEventListener('pointerout', this.onPointerOut, true); + this.interactionDOMElement.removeEventListener('pointerleave', 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) + 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.onPointerDown, true); + this.interactionDOMElement.removeEventListener('touchcancel', this.onPointerCancel, 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); - - 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 = null; @@ -664,7 +613,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; @@ -672,25 +621,80 @@ return; } - this.cursor = this.defaultCursorStyle; + this.cursor = null; // 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(); - - this.processInteractive(this.mouse.global, this.renderer._lastObjectRendered, this.processMouseOverOut, true); - - if (this.currentCursorStyle !== this.cursor) + for (const k in this.activeInteractionData) { - this.currentCursorStyle = this.cursor; - this.interactionDOMElement.style.cursor = this.cursor; + // eslint-disable-next-line no-prototype-builtins + if (this.activeInteractionData.hasOwnProperty(k)) + { + const interactionData = this.activeInteractionData[k]; + + if (interactionData.originalEvent && interactionData.pointerType !== 'touch') + { + const interactionEvent = this.configureInteractionEventForDOMEvent( + this.eventData, + interactionData.originalEvent, + interactionData + ); + + this.processInteractive( + interactionEvent, + this.renderer._lastObjectRendered, + this.processPointerOverOut, + true + ); + } + } } + this.setCursorMode(this.cursor); + // TODO } /** + * Sets the current cursor mode, handling any callbacks or CSS style changes. + * + * @param {string} mode - cursor mode, a key from the cursorStyles dictionary + */ + setCursorMode(mode) + { + mode = mode || 'default'; + // if the mode didn't actually change, bail early + if (this.currentCursorMode === mode) + { + return; + } + this.currentCursorMode = mode; + const style = this.cursorStyles[mode]; + + // only do things if there is a cursor style for it + if (style) + { + switch (typeof style) + { + case 'string': + // string styles are handled as cursor CSS + this.interactionDOMElement.style.cursor = style; + break; + case 'function': + // functions are just called, and passed the cursor mode + style(mode); + break; + case 'object': + // if it is an object, assume that it is a dictionary of CSS styles, + // apply it to the interactionDOMElement + Object.assign(this.interactionDOMElement.style, style); + break; + } + } + } + + /** * Dispatches an event on the display object that was interacted with * * @param {PIXI.Container|PIXI.Sprite|PIXI.extras.TilingSprite} displayObject - the display object in question @@ -748,22 +752,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 @@ -787,9 +795,8 @@ { interactiveParent = false; } - - // it has a mask! Then lets hit test that before continuing.. - if (hitTest && displayObject._mask) + // it has a mask! Then lets hit test that before continuing + else if (hitTest && displayObject._mask) { if (!displayObject._mask.containsPoint(point)) { @@ -797,14 +804,7 @@ } } - // it has a filterArea! Same as mask but easier, its a rectangle - if (hitTest && displayObject.filterArea) - { - if (!displayObject.filterArea.contains(point.x, point.y)) - { - hitTest = false; - } - } + let keepHitTestingAfterChildren = hitTest; // ** FREE TIP **! If an object is not interactive or has no buttons in it // (such as a game scene!) set interactiveChildren to false for that displayObject. @@ -818,7 +818,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 @@ -838,15 +838,20 @@ // This means we no longer need to hit test anything else. We still need to run // through all objects, but we don't need to perform any hit tests. - // { - hitTest = false; - // } + keepHitTestingAfterChildren = false; + + if (child.interactive) + { + hitTest = false; + } // we can break now as we have hit an object. } } } + hitTest = keepHitTestingAfterChildren; + // no point running this if the item is not interactive or does not have an interactive parent. if (interactive) { @@ -868,14 +873,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 +886,175 @@ } /** - * 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].isNormalized) { - 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); + if (!displayObject.trackedPointers[id]) + { + displayObject.trackedPointers[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.trackedPointers[id].rightDown = true; + } + else + { + displayObject.trackedPointers[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.trackedPointers[id] !== undefined) + { + delete displayObject.trackedPointers[id]; + this.dispatchEvent(displayObject, 'pointercancel', interactionEvent); + + if (e.type === 'touchcancel' || e.pointerType === 'touch') + { + this.dispatchEvent(displayObject, 'touchcancel', interactionEvent); + } } } @@ -1159,42 +1066,94 @@ */ 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) { - if (hit) - { - this.dispatchEvent(displayObject, 'pointerup', this.eventData); + const e = interactionEvent.data.originalEvent; - if (displayObject._pointerDown) + const id = interactionEvent.data.identifier; + + const trackingData = displayObject.trackedPointers[id]; + + const isTouch = (e.type === 'touchend' || e.pointerType === 'touch'); + + const isMouse = (e.type.indexOf('mouse') === 0 || e.pointerType === 'mouse'); + + // 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) { - displayObject._pointerDown = false; - this.dispatchEvent(displayObject, 'pointertap', this.eventData); + 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); + } + // update the down state of the tracking data + if (trackingData) + { + if (isRightButton) + { + trackingData.rightDown = hit; + } + else + { + trackingData.leftDown = hit; + } } } - else if (displayObject._pointerDown) + + // Pointers and Touches, and Mouse + if (hit) { - displayObject._pointerDown = false; - this.dispatchEvent(displayObject, 'pointerupoutside', this.eventData); + this.dispatchEvent(displayObject, 'pointerup', interactionEvent); + if (isTouch) this.dispatchEvent(displayObject, 'touchend', interactionEvent); + + if (trackingData) + { + this.dispatchEvent(displayObject, 'pointertap', interactionEvent); + if (isTouch) + { + this.dispatchEvent(displayObject, 'tap', interactionEvent); + // touches are no longer over (if they ever were) when we get the touchend + // so we should ensure that we don't keep pretending that they are + trackingData.over = false; + } + } + } + else if (trackingData) + { + this.dispatchEvent(displayObject, 'pointerupoutside', interactionEvent); + if (isTouch) this.dispatchEvent(displayObject, 'touchendoutside', interactionEvent); + } + // Only remove the tracking data if there is no over/down state still associated with it + if (trackingData && trackingData.none) + { + delete displayObject.trackedPointers[id]; } } @@ -1202,39 +1161,78 @@ * 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 = null; + } - 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') + { + this.setCursorMode(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 (isMouse) { - 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 +1240,94 @@ * 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.setCursorMode(null); + } - 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'); + + let trackingData = displayObject.trackedPointers[id]; + + // if we just moused over the display object, then we need to track that state + if (hit && !trackingData) + { + trackingData = displayObject.trackedPointers[id] = new InteractionTrackingData(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); + } + } + + // only change the cursor if it has not already been changed (by something deeper in the + // display tree) + if (isMouse && this.cursor === null) + { + this.cursor = displayObject.cursor; } } - 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); + } + // if there is no mouse down information for the pointer, then it is safe to delete + if (trackingData.none) + { + delete displayObject.trackedPointers[id]; + } } } @@ -1287,253 +1335,154 @@ * 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 && pointerEvent.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; + + // mark the touch as normalized, just so that we know we did it + touch.isNormalized = true; + + normalizedEvents.push(touch); + } } - else if (this.normalizeMouseEvents) + // apparently PointerEvent subclasses MouseEvent, so yay + else if (event instanceof MouseEvent && (!this.supportsPointerEvents || !(event instanceof window.PointerEvent))) { if (typeof event.isPrimary === 'undefined') event.isPrimary = true; if (typeof event.width === 'undefined') event.width = 1; @@ -1541,10 +1490,21 @@ 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; + + // mark the mouse event as normalized, just so that we know we did it + event.isNormalized = true; + + normalizedEvents.push(event); } + else + { + normalizedEvents.push(event); + } + + return normalizedEvents; } /** @@ -1563,30 +1523,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 +1542,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..6a1f69e --- /dev/null +++ b/src/interaction/InteractionTrackingData.js @@ -0,0 +1,147 @@ +/** + * 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 inactive (not over or down)? + * + * @member {number} + * @memberof PIXI.interaction.InteractionTrackingData# + */ + get none() + { + return this._flags === this.constructor.FLAGS.NONE; + } + + /** + * 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..d252fba 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 * ); */ @@ -38,68 +38,56 @@ hitArea: null, /** - * If enabled, the mouse cursor will change when hovered over the displayObject if it is interactive + * If enabled, the mouse cursor use the pointer behavior when hovered over the displayObject if it is interactive + * Setting this changes the 'cursor' property to `'pointer'`. * - * @inner {boolean} + * @member {boolean} + * @memberof PIXI.interaction.interactiveTarget# */ - buttonMode: false, + get buttonMode() + { + return this.cursor === 'pointer'; + }, + set buttonMode(value) + { + if (value) + { + this.cursor = 'pointer'; + } + else if (this.cursor === 'pointer') + { + this.cursor = null; + } + }, /** - * If buttonMode is enabled, this defines what CSS cursor property is used when the mouse cursor - * is hovered over the displayObject + * This defines what cursor mode is used when the mouse cursor + * is hovered over the displayObject. * * @see https://developer.mozilla.org/en/docs/Web/CSS/cursor * * @inner {string} */ - defaultCursor: 'pointer', - - // some internal checks.. - /** - * Internal check to detect if the mouse cursor is hovered over the displayObject - * - * @inner {boolean} - * @private - */ - _over: false, + cursor: null, /** - * Internal check to detect if the left mouse button is pressed on the displayObject + * Internal set of all active pointers, by identifier * - * @inner {boolean} + * @member {Map} + * @memberof PIXI.interaction.interactiveTarget# * @private */ - _isLeftDown: false, + get trackedPointers() + { + if (this._trackedPointers === undefined) this._trackedPointers = {}; + + return this._trackedPointers; + }, /** - * Internal check to detect if the right mouse button is pressed on the displayObject + * Map of all tracked pointers, by identifier. Use trackedPointers to access. * - * @inner {boolean} - * @private + * @private {Map} */ - _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, + _trackedPointers: undefined, }; diff --git a/src/loaders/bitmapFontParser.js b/src/loaders/bitmapFontParser.js index cc2618c..f4cf996 100644 --- a/src/loaders/bitmapFontParser.js +++ b/src/loaders/bitmapFontParser.js @@ -1,63 +1,19 @@ import * as path from 'path'; -import { Rectangle, Texture, utils } from '../core'; +import { utils } from '../core'; import { Resource } from 'resource-loader'; import { BitmapText } from '../extras'; +/** + * Register a BitmapText font from loader resource. + * + * @function parseBitmapFontData + * @memberof PIXI.loaders + * @param {PIXI.loaders.Resource} resource - Loader resource. + * @param {PIXI.Texture} texture - Reference to texture. + */ export function parse(resource, texture) { - const data = {}; - const info = resource.data.getElementsByTagName('info')[0]; - const common = resource.data.getElementsByTagName('common')[0]; - - data.font = info.getAttribute('face'); - data.size = parseInt(info.getAttribute('size'), 10); - data.lineHeight = parseInt(common.getAttribute('lineHeight'), 10); - data.chars = {}; - - // parse letters - const letters = resource.data.getElementsByTagName('char'); - - for (let i = 0; i < letters.length; i++) - { - const charCode = parseInt(letters[i].getAttribute('id'), 10); - - const textureRect = new Rectangle( - parseInt(letters[i].getAttribute('x'), 10) + texture.frame.x, - parseInt(letters[i].getAttribute('y'), 10) + texture.frame.y, - parseInt(letters[i].getAttribute('width'), 10), - parseInt(letters[i].getAttribute('height'), 10) - ); - - data.chars[charCode] = { - xOffset: parseInt(letters[i].getAttribute('xoffset'), 10), - yOffset: parseInt(letters[i].getAttribute('yoffset'), 10), - xAdvance: parseInt(letters[i].getAttribute('xadvance'), 10), - kerning: {}, - texture: new Texture(texture.baseTexture, textureRect), - - }; - } - - // parse kernings - const kernings = resource.data.getElementsByTagName('kerning'); - - for (let i = 0; i < kernings.length; i++) - { - const first = parseInt(kernings[i].getAttribute('first'), 10); - const second = parseInt(kernings[i].getAttribute('second'), 10); - const amount = parseInt(kernings[i].getAttribute('amount'), 10); - - if (data.chars[second]) - { - data.chars[second].kerning[first] = amount; - } - } - - resource.bitmapFont = data; - - // I'm leaving this as a temporary fix so we can test the bitmap fonts in v3 - // but it's very likely to change - BitmapText.fonts[data.font] = data; + resource.bitmapFont = BitmapText.registerFont(resource.data, texture); } export default function () diff --git a/package.json b/package.json index 6a366ba..71ade15 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pixi.js", - "version": "4.3.4", + "version": "4.3.5", "description": "Pixi.js is a fast lightweight 2D library that works across all devices.", "author": "Mat Groves", "contributors": [ @@ -61,7 +61,7 @@ "ismobilejs": "^0.4.0", "object-assign": "^4.0.1", "pixi-gl-core": "^1.0.3", - "resource-loader": "^2.0.4" + "resource-loader": "^2.0.6" }, "devDependencies": { "babel-cli": "^6.18.0", @@ -70,9 +70,9 @@ "babel-preset-es2015": "^6.14.0", "babelify": "^7.3.0", "del": "^2.2.0", - "electron-prebuilt": "^1.3.2", + "electron": "^1.4.15", "eslint": "^3.5.0", - "floss": "^1.2.0", + "floss": "^2.0.1", "gh-pages": "^0.11.0", "jaguarjs-jsdoc": "^1.0.1", "js-md5": "^0.4.1", diff --git a/src/accessibility/AccessibilityManager.js b/src/accessibility/AccessibilityManager.js index df2716a..f16bcb1 100644 --- a/src/accessibility/AccessibilityManager.js +++ b/src/accessibility/AccessibilityManager.js @@ -3,7 +3,7 @@ import accessibleTarget from './accessibleTarget'; // add some extra variables to the container.. -Object.assign( +core.utils.mixins.delayMixin( core.DisplayObject.prototype, accessibleTarget ); diff --git a/src/core/Application.js b/src/core/Application.js index ea0f058..5340578 100644 --- a/src/core/Application.js +++ b/src/core/Application.js @@ -1,6 +1,6 @@ import { autoDetectRenderer } from './autoDetectRenderer'; import Container from './display/Container'; -import Ticker from './ticker/Ticker'; +import { shared, Ticker } from './ticker'; /** * Convenience class to create a new PIXI application. @@ -35,9 +35,9 @@ * @param {boolean} [noWebGL=false] - prevents selection of WebGL renderer, even if such is present * @param {boolean} [options.legacy=false] - If true Pixi will aim to ensure compatibility * with older / less advanced devices. If you experience unexplained flickering try setting this to true. - + * @param {boolean} [useSharedTicker=false] - `true` to use PIXI.ticker.shared, `false` to create new ticker. */ - constructor(width, height, options, noWebGL) + constructor(width, height, options, noWebGL, useSharedTicker = false) { /** * WebGL renderer if available, otherwise CanvasRenderer @@ -46,23 +46,46 @@ this.renderer = autoDetectRenderer(width, height, options, noWebGL); /** - * The root display container that's renderered. + * The root display container that's rendered. * @member {PIXI.Container} */ this.stage = new Container(); /** + * Internal reference to the ticker + * @member {PIXI.ticker.Ticker} + * @private + */ + this._ticker = null; + + /** * Ticker for doing render updates. * @member {PIXI.ticker.Ticker} + * @default PIXI.ticker.shared */ - this.ticker = new Ticker(); - - this.ticker.add(this.render, this); + this.ticker = useSharedTicker ? shared : new Ticker(); // Start the rendering this.start(); } + set ticker(ticker) // eslint-disable-line require-jsdoc + { + if (this._ticker) + { + this._ticker.remove(this.render, this); + } + this._ticker = ticker; + if (ticker) + { + ticker.add(this.render, this); + } + } + get ticker() // eslint-disable-line require-jsdoc + { + return this._ticker; + } + /** * Render the current stage. */ @@ -76,7 +99,7 @@ */ stop() { - this.ticker.stop(); + this._ticker.stop(); } /** @@ -84,7 +107,7 @@ */ start() { - this.ticker.start(); + this._ticker.start(); } /** @@ -98,13 +121,22 @@ } /** + * Reference to the renderer's screen rectangle. Its safe to use as filterArea or hitArea for whole screen + * @member {PIXI.Rectangle} + * @readonly + */ + get screen() + { + return this.renderer.screen; + } + + /** * Destroy and don't use after this. * @param {Boolean} [removeView=false] Automatically remove canvas from DOM. */ destroy(removeView) { this.stop(); - this.ticker.remove(this.render, this); this.ticker = null; this.stage.destroy(); diff --git a/src/core/Shader.js b/src/core/Shader.js new file mode 100644 index 0000000..78b17e6 --- /dev/null +++ b/src/core/Shader.js @@ -0,0 +1,46 @@ +import { GLShader } from 'pixi-gl-core'; +import settings from './settings'; + +function checkPrecision(src, def) +{ + if (src instanceof Array) + { + if (src[0].substring(0, 9) !== 'precision') + { + const copy = src.slice(0); + + copy.unshift(`precision ${def} float;`); + + return copy; + } + } + else if (src.substring(0, 9) !== 'precision') + { + return `precision ${def} float;\n${src}`; + } + + return src; +} + +/** + * Wrapper class, webGL Shader for Pixi. + * Adds precision string if vertexSrc or fragmentSrc have no mention of it. + * + * @class + * @extends GLShader + * @memberof PIXI + */ +export default class Shader extends GLShader +{ + /** + * + * @param {WebGLRenderingContext} gl - The current WebGL rendering context + * @param {string|string[]} vertexSrc - The vertex shader source as an array of strings. + * @param {string|string[]} fragmentSrc - The fragment shader source as an array of strings. + */ + constructor(gl, vertexSrc, fragmentSrc) + { + super(gl, checkPrecision(vertexSrc, settings.PRECISION_VERTEX), + checkPrecision(fragmentSrc, settings.PRECISION_FRAGMENT)); + } +} diff --git a/src/core/const.js b/src/core/const.js index ef6f662..3de0efe 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -188,7 +188,7 @@ * The gc modes that are supported by pixi. * * The {@link PIXI.settings.GC_MODE} Garbage Collection mode for pixi textures is AUTO - * If set to GC_MODE, the renderer will occasianally check textures usage. If they are not + * If set to GC_MODE, the renderer will occasionally check textures usage. If they are not * used for a specified period of time they will be removed from the GPU. They will of course * be uploaded again when they are required. This is a silent behind the scenes process that * should ensure that the GPU does not get filled up. diff --git a/src/core/display/Container.js b/src/core/display/Container.js index ed8cd5f..17fde8d 100644 --- a/src/core/display/Container.js +++ b/src/core/display/Container.js @@ -73,11 +73,12 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.push(child); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -108,11 +109,12 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.splice(index, 0, child); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -225,10 +227,11 @@ if (index === -1) return null; child.parent = null; + // ensure child transform will be recalculated + child.transform._parentID = -1; removeItems(this.children, index, 1); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -249,11 +252,12 @@ { const child = this.getChildAt(index); + // ensure child transform will be recalculated.. child.parent = null; + child.transform._parentID = -1; removeItems(this.children, index, 1); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -284,13 +288,12 @@ for (let i = 0; i < removed.length; ++i) { removed[i].parent = null; + if (removed[i].transform) + { + removed[i].transform._parentID = -1; + } } - // ensure a transform will be recalculated.. - if (this.transform) - { - this.transform._parentID = -1; - } this._boundsID++; this.onChildrenChange(beginIndex); diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index f37f2f5..be4c4fb 100644 --- a/src/core/graphics/Graphics.js +++ b/src/core/graphics/Graphics.js @@ -28,8 +28,9 @@ { /** * + * @param {boolean} [nativeLines=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP */ - constructor() + constructor(nativeLines = false) { super(); @@ -50,6 +51,13 @@ this.lineWidth = 0; /** + * If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * + * @member {boolean} + */ + this.nativeLines = nativeLines; + + /** * The color of any lines drawn. * * @member {string} @@ -757,28 +765,14 @@ if (!this._spriteRect) { - if (!Graphics._SPRITE_TEXTURE) - { - Graphics._SPRITE_TEXTURE = RenderTexture.create(10, 10); - - const canvas = document.createElement('canvas'); - - canvas.width = 10; - canvas.height = 10; - - const context = canvas.getContext('2d'); - - context.fillStyle = 'white'; - context.fillRect(0, 0, 10, 10); - - Graphics._SPRITE_TEXTURE = Texture.fromCanvas(canvas); - } - - this._spriteRect = new Sprite(Graphics._SPRITE_TEXTURE); + this._spriteRect = new Sprite(new Texture(Texture.WHITE)); } + + const sprite = this._spriteRect; + if (this.tint === 0xffffff) { - this._spriteRect.tint = this.graphicsData[0].fillColor; + sprite.tint = this.graphicsData[0].fillColor; } else { @@ -792,20 +786,21 @@ t1[1] *= t2[1]; t1[2] *= t2[2]; - this._spriteRect.tint = rgb2hex(t1); + sprite.tint = rgb2hex(t1); } - this._spriteRect.alpha = this.graphicsData[0].fillAlpha; - this._spriteRect.worldAlpha = this.worldAlpha * this._spriteRect.alpha; + sprite.alpha = this.graphicsData[0].fillAlpha; + sprite.worldAlpha = this.worldAlpha * sprite.alpha; + sprite.blendMode = this.blendMode; - Graphics._SPRITE_TEXTURE._frame.width = rect.width; - Graphics._SPRITE_TEXTURE._frame.height = rect.height; + sprite.texture._frame.width = rect.width; + sprite.texture._frame.height = rect.height; - this._spriteRect.transform.worldTransform = this.transform.worldTransform; + sprite.transform.worldTransform = this.transform.worldTransform; - this._spriteRect.anchor.set(-rect.x / rect.width, -rect.y / rect.height); - this._spriteRect._onAnchorUpdate(); + sprite.anchor.set(-rect.x / rect.width, -rect.y / rect.height); + sprite._onAnchorUpdate(); - this._spriteRect._renderWebGL(renderer); + sprite._renderWebGL(renderer); } /** @@ -1031,6 +1026,7 @@ this.fillColor, this.fillAlpha, this.filling, + this.nativeLines, shape ); diff --git a/src/core/graphics/GraphicsData.js b/src/core/graphics/GraphicsData.js index f5a87ae..b6f03c0 100644 --- a/src/core/graphics/GraphicsData.js +++ b/src/core/graphics/GraphicsData.js @@ -14,14 +14,19 @@ * @param {number} fillColor - the color of the fill * @param {number} fillAlpha - the alpha of the fill * @param {boolean} fill - whether or not the shape is filled with a colour + * @param {boolean} nativeLines - the method for drawing lines * @param {PIXI.Circle|PIXI.Rectangle|PIXI.Ellipse|PIXI.Polygon} shape - The shape object to draw. */ - constructor(lineWidth, lineColor, lineAlpha, fillColor, fillAlpha, fill, shape) + constructor(lineWidth, lineColor, lineAlpha, fillColor, fillAlpha, fill, nativeLines, shape) { /** * @member {number} the width of the line to draw */ this.lineWidth = lineWidth; + /** + * @member {boolean} if true the liens will be draw using LINES instead of TRIANGLE_STRIP + */ + this.nativeLines = nativeLines; /** * @member {number} the color of the line to draw @@ -85,6 +90,7 @@ this.fillColor, this.fillAlpha, this.fill, + this.nativeLines, this.shape ); } diff --git a/src/core/graphics/webgl/GraphicsRenderer.js b/src/core/graphics/webgl/GraphicsRenderer.js index 14083fe..67f5e92 100644 --- a/src/core/graphics/webgl/GraphicsRenderer.js +++ b/src/core/graphics/webgl/GraphicsRenderer.js @@ -102,7 +102,15 @@ shaderTemp.uniforms.alpha = graphics.worldAlpha; renderer.bindVao(webGLData.vao); - webGLData.vao.draw(gl.TRIANGLE_STRIP, webGLData.indices.length); + + if (graphics.nativeLines) + { + gl.drawArrays(gl.LINES, 0, webGLData.points.length / 6); + } + else + { + webGLData.vao.draw(gl.TRIANGLE_STRIP, webGLData.indices.length); + } } } diff --git a/src/core/graphics/webgl/utils/buildLine.js b/src/core/graphics/webgl/utils/buildLine.js index ecf43f7..4d6862e 100644 --- a/src/core/graphics/webgl/utils/buildLine.js +++ b/src/core/graphics/webgl/utils/buildLine.js @@ -11,7 +11,29 @@ * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties * @param {object} webGLData - an object containing all the webGL-specific information to create this shape */ -export default function buildLine(graphicsData, webGLData) +export default function (graphicsData, webGLData) +{ + if (graphicsData.nativeLines) + { + buildNativeLine(graphicsData, webGLData); + } + else + { + buildLine(graphicsData, webGLData); + } +} + +/** + * Builds a line to draw using the poligon method. + * + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @private + * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties + * @param {object} webGLData - an object containing all the webGL-specific information to create this shape + */ +function buildLine(graphicsData, webGLData) { // TODO OPTIMISE! let points = graphicsData.points; @@ -224,3 +246,46 @@ indices.push(indexStart - 1); } + +/** + * Builds a line to draw using the gl.drawArrays(gl.LINES) method + * + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @private + * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties + * @param {object} webGLData - an object containing all the webGL-specific information to create this shape + */ +function buildNativeLine(graphicsData, webGLData) +{ + let i = 0; + const points = graphicsData.points; + + if (points.length === 0) return; + + const verts = webGLData.points; + const length = points.length / 2; + + // sort color + const color = hex2rgb(graphicsData.lineColor); + const alpha = graphicsData.lineAlpha; + const r = color[0] * alpha; + const g = color[1] * alpha; + const b = color[2] * alpha; + + for (i = 1; i < length; i++) + { + const p1x = points[(i - 1) * 2]; + const p1y = points[((i - 1) * 2) + 1]; + + const p2x = points[i * 2]; + const p2y = points[(i * 2) + 1]; + + verts.push(p1x, p1y); + verts.push(r, g, b, alpha); + + verts.push(p2x, p2y); + verts.push(r, g, b, alpha); + } +} diff --git a/src/core/graphics/webgl/utils/buildRoundedRectangle.js b/src/core/graphics/webgl/utils/buildRoundedRectangle.js index 9abc807..a6c8461 100644 --- a/src/core/graphics/webgl/utils/buildRoundedRectangle.js +++ b/src/core/graphics/webgl/utils/buildRoundedRectangle.js @@ -77,6 +77,26 @@ } /** + * Calculate a single point for a quadratic bezier curve. + * Utility function used by quadraticBezierCurve. + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @private + * @param {number} n1 - first number + * @param {number} n2 - second number + * @param {number} perc - percentage + * @return {number} the result + * + */ +function getPt(n1, n2, perc) +{ + const diff = n2 - n1; + + return n1 + (diff * perc); +} + +/** * Calculate the points for a quadratic bezier curve. (helper function..) * Based on: https://stackoverflow.com/questions/785097/how-do-i-implement-a-bezier-curve-in-c * @@ -105,13 +125,6 @@ let x = 0; let y = 0; - function getPt(n1, n2, perc) - { - const diff = n2 - n1; - - return n1 + (diff * perc); - } - for (let i = 0, j = 0; i <= n; ++i) { j = i / n; diff --git a/src/core/index.js b/src/core/index.js index f7a817c..4f414e8 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -30,6 +30,7 @@ export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; export { default as CanvasGraphicsRenderer } from './graphics/canvas/CanvasGraphicsRenderer'; +export { default as Spritesheet } from './textures/Spritesheet'; export { default as Texture } from './textures/Texture'; export { default as BaseTexture } from './textures/BaseTexture'; export { default as RenderTexture } from './textures/RenderTexture'; diff --git a/src/core/math/Matrix.js b/src/core/math/Matrix.js index 0b77477..3679860 100644 --- a/src/core/math/Matrix.js +++ b/src/core/math/Matrix.js @@ -13,45 +13,50 @@ export default class Matrix { /** - * + * @param {number} [a=1] - x scale + * @param {number} [b=0] - y skew + * @param {number} [c=0] - x skew + * @param {number} [d=1] - y scale + * @param {number} [tx=0] - x translation + * @param {number} [ty=0] - y translation */ - constructor() + constructor(a = 1, b = 0, c = 0, d = 1, tx = 0, ty = 0) { /** * @member {number} * @default 1 */ - this.a = 1; + this.a = a; /** * @member {number} * @default 0 */ - this.b = 0; + this.b = b; /** * @member {number} * @default 0 */ - this.c = 0; + this.c = c; /** * @member {number} * @default 1 */ - this.d = 1; + this.d = d; /** * @member {number} * @default 0 */ - this.tx = 0; + this.tx = tx; /** * @member {number} * @default 0 */ - this.ty = 0; + this.ty = ty; this.array = null; } diff --git a/src/core/renderers/SystemRenderer.js b/src/core/renderers/SystemRenderer.js index fa116a6..a24c775 100644 --- a/src/core/renderers/SystemRenderer.js +++ b/src/core/renderers/SystemRenderer.js @@ -1,5 +1,5 @@ import { sayHello, hex2string, hex2rgb } from '../utils'; -import { Matrix } from '../math'; +import { Matrix, Rectangle } from '../math'; import { RENDERER_TYPE } from '../const'; import settings from '../settings'; import Container from '../display/Container'; @@ -21,8 +21,8 @@ { /** * @param {string} system - The name of the system this renderer is for. - * @param {number} [width=800] - the width of the canvas view - * @param {number} [height=600] - the height of the canvas view + * @param {number} [screenWidth=800] - the width of the screen + * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false @@ -37,7 +37,7 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(system, width, height, options) + constructor(system, screenWidth, screenHeight, options) { super(); @@ -69,20 +69,13 @@ this.type = RENDERER_TYPE.UNKNOWN; /** - * The width of the canvas view + * Measurements of the screen. (0, 0, screenWidth, screenHeight) * - * @member {number} - * @default 800 - */ - this.width = width || 800; - - /** - * The height of the canvas view + * Its safe to use as filterArea or hitArea for whole stage * - * @member {number} - * @default 600 + * @member {PIXI.Rectangle} */ - this.height = height || 600; + this.screen = new Rectangle(0, 0, screenWidth || 800, screenHeight || 600); /** * The canvas element that everything is drawn to @@ -107,7 +100,7 @@ this.transparent = options.transparent; /** - * Whether the render view should be resized automatically + * Whether css dimensions of canvas view should be resized to screen dimensions automatically * * @member {boolean} */ @@ -192,23 +185,48 @@ } /** - * Resizes the canvas view to the specified width and height + * Same as view.width, actual number of pixels in the canvas by horizontal * - * @param {number} width - the new width of the canvas view - * @param {number} height - the new height of the canvas view + * @member {number} + * @readonly + * @default 800 */ - resize(width, height) + get width() { - this.width = width * this.resolution; - this.height = height * this.resolution; + return this.view.width; + } - this.view.width = this.width; - this.view.height = this.height; + /** + * Same as view.height, actual number of pixels in the canvas by vertical + * + * @member {number} + * @readonly + * @default 600 + */ + get height() + { + return this.view.height; + } + + /** + * Resizes the screen and canvas to the specified width and height + * Canvas dimensions are multiplied by resolution + * + * @param {number} screenWidth - the new width of the screen + * @param {number} screenHeight - the new height of the screen + */ + resize(screenWidth, screenHeight) + { + this.screen.width = screenWidth; + this.screen.height = screenHeight; + + this.view.width = screenWidth * this.resolution; + this.view.height = screenHeight * this.resolution; if (this.autoResize) { - this.view.style.width = `${this.width / this.resolution}px`; - this.view.style.height = `${this.height / this.resolution}px`; + this.view.style.width = `${screenWidth}px`; + this.view.style.height = `${screenHeight}px`; } } @@ -249,11 +267,10 @@ this.type = RENDERER_TYPE.UNKNOWN; - this.width = 0; - this.height = 0; - this.view = null; + this.screen = null; + this.resolution = 0; this.transparent = false; diff --git a/src/core/renderers/canvas/CanvasRenderer.js b/src/core/renderers/canvas/CanvasRenderer.js index 1bd2d5e..e84e49c 100644 --- a/src/core/renderers/canvas/CanvasRenderer.js +++ b/src/core/renderers/canvas/CanvasRenderer.js @@ -18,8 +18,8 @@ export default class CanvasRenderer extends SystemRenderer { /** - * @param {number} [width=800] - the width of the canvas view - * @param {number} [height=600] - the height of the canvas view + * @param {number} [screenWidth=800] - the width of the screen + * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false @@ -34,9 +34,9 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(width, height, options = {}) + constructor(screenWidth, screenHeight, options = {}) { - super('Canvas', width, height, options); + super('Canvas', screenWidth, screenHeight, options); this.type = RENDERER_TYPE.CANVAS; @@ -96,7 +96,7 @@ this.context = null; this.renderingToScreen = false; - this.resize(width, height); + this.resize(screenWidth, screenHeight); } /** @@ -283,12 +283,12 @@ * * @extends PIXI.SystemRenderer#resize * - * @param {number} width - The new width of the canvas view - * @param {number} height - The new height of the canvas view + * @param {number} screenWidth - the new width of the screen + * @param {number} screenHeight - the new height of the screen */ - resize(width, height) + resize(screenWidth, screenHeight) { - super.resize(width, height); + super.resize(screenWidth, screenHeight); // reset the scale mode.. oddly this seems to be reset when the canvas is resized. // surely a browser bug?? Let pixi fix that for you.. diff --git a/src/core/renderers/webgl/TextureManager.js b/src/core/renderers/webgl/TextureManager.js index b685c1a..8227289 100644 --- a/src/core/renderers/webgl/TextureManager.js +++ b/src/core/renderers/webgl/TextureManager.js @@ -61,7 +61,7 @@ * Updates and/or Creates a WebGL texture for the renderer's context. * * @param {PIXI.BaseTexture|PIXI.Texture} texture - the texture to update - * @param {Number} location - the location the texture will be bound to. + * @param {number} location - the location the texture will be bound to. * @return {GLTexture} The gl texture. */ updateTexture(texture, location) diff --git a/src/core/renderers/webgl/WebGLRenderer.js b/src/core/renderers/webgl/WebGLRenderer.js index e4cd8a0..830b382 100644 --- a/src/core/renderers/webgl/WebGLRenderer.js +++ b/src/core/renderers/webgl/WebGLRenderer.js @@ -31,8 +31,8 @@ { /** * - * @param {number} [width=0] - the width of the canvas view - * @param {number} [height=0] - the height of the canvas view + * @param {number} [screenWidth=800] - the width of the screen + * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false @@ -53,9 +53,9 @@ * @param {boolean} [options.legacy=false] - If true Pixi will aim to ensure compatibility * with older / less advanced devices. If you experiance unexplained flickering try setting this to true. */ - constructor(width, height, options = {}) + constructor(screenWidth, screenHeight, options = {}) { - super('WebGL', width, height, options); + super('WebGL', screenWidth, screenHeight, options); this.legacy = !!options.legacy; @@ -245,7 +245,7 @@ glCore._testingContext = gl; // setup the width/height properties and gl viewport - this.resize(this.width, this.height); + this.resize(this.screen.width, this.screen.height); } /** @@ -343,16 +343,16 @@ /** * Resizes the webGL view to the specified width and height. * - * @param {number} width - the new width of the webGL view - * @param {number} height - the new height of the webGL view + * @param {number} screenWidth - the new width of the screen + * @param {number} screenHeight - the new height of the screen */ - resize(width, height) + resize(screenWidth, screenHeight) { // if(width * this.resolution === this.width && height * this.resolution === this.height)return; - SystemRenderer.prototype.resize.call(this, width, height); + SystemRenderer.prototype.resize.call(this, screenWidth, screenHeight); - this.rootRenderTarget.resize(width, height); + this.rootRenderTarget.resize(screenWidth, screenHeight); if (this._activeRenderTarget === this.rootRenderTarget) { @@ -396,6 +396,26 @@ } /** + * Erases the render texture and fills the drawing area with a colour + * + * @param {PIXI.RenderTexture} renderTexture - The render texture to clear + * @param {number} [clearColor] - The colour + * @return {PIXI.WebGLRenderer} Returns itself. + */ + clearRenderTexture(renderTexture, clearColor) + { + const baseTexture = renderTexture.baseTexture; + const renderTarget = baseTexture._glRenderTargets[this.CONTEXT_UID]; + + if (renderTarget) + { + renderTarget.clear(clearColor); + } + + return this; + } + + /** * Binds a render texture for rendering * * @param {PIXI.RenderTexture} renderTexture - The render texture to render @@ -474,9 +494,10 @@ * Changes the current GLShader to the one given in parameter * * @param {PIXI.glCore.Shader} shader - the new glShader + * @param {boolean} [autoProject=true] - Whether automatically set the projection matrix * @return {PIXI.WebGLRenderer} Returns itself. */ - _bindGLShader(shader) + _bindGLShader(shader, autoProject) { // TODO cache if (this._activeShader !== shader) @@ -484,8 +505,14 @@ this._activeShader = shader; shader.bind(); - // automatically set the projection matrix - shader.uniforms.projectionMatrix = this._activeRenderTarget.projectionMatrix.toArray(true); + // `autoProject` normally would be a default parameter set to true + // but because of how Babel transpiles default parameters + // it hinders the performance of this method. + if (autoProject !== false) + { + // automatically set the projection matrix + shader.uniforms.projectionMatrix = this._activeRenderTarget.projectionMatrix.toArray(true); + } } return this; diff --git a/src/core/renderers/webgl/WebGLState.js b/src/core/renderers/webgl/WebGLState.js index ba6ecc5..6c846d5 100755 --- a/src/core/renderers/webgl/WebGLState.js +++ b/src/core/renderers/webgl/WebGLState.js @@ -82,18 +82,20 @@ push() { // next state.. - let state = this.stack[++this.stackIndex]; + let state = this.stack[this.stackIndex]; if (!state) { state = this.stack[this.stackIndex] = new Uint8Array(16); } + ++this.stackIndex; + // copy state.. // set active state so we can force overrides of gl state for (let i = 0; i < this.activeState.length; i++) { - this.activeState[i] = state[i]; + state[i] = this.activeState[i]; } } diff --git a/src/core/renderers/webgl/filters/Filter.js b/src/core/renderers/webgl/filters/Filter.js index 2c3eaad..4eb6128 100644 --- a/src/core/renderers/webgl/filters/Filter.js +++ b/src/core/renderers/webgl/filters/Filter.js @@ -9,7 +9,7 @@ * @memberof PIXI * @extends PIXI.Shader */ -class Filter extends Shader +export default class Filter extends Shader { /** * @param {string} [vertexSrc] - The source of the vertex shader. @@ -56,15 +56,16 @@ * @param {PIXI.RenderTarget} input - The input render target. * @param {PIXI.RenderTarget} output - The target to output to. * @param {boolean} clear - Should the output be cleared before rendering to it + * @param {object} [currentState] - It's current state of filter. + * There are some useful properties in the currentState : + * target, filters, sourceFrame, destinationFrame, renderTarget, resolution */ - apply(filterManager, input, output, clear) + apply(filterManager, input, output, clear, currentState) // eslint-disable-line no-unused-vars { // do as you please! - filterManager.applyFilter(this, input, output, clear); + filterManager.applyFilter(this, input, output, clear, currentState); // or just do a regular render.. } } - -export default Filter; diff --git a/src/core/renderers/webgl/filters/extractUniformsFromSrc.js b/src/core/renderers/webgl/filters/extractUniformsFromSrc.js index d78a0b1..4893214 100644 --- a/src/core/renderers/webgl/filters/extractUniformsFromSrc.js +++ b/src/core/renderers/webgl/filters/extractUniformsFromSrc.js @@ -12,7 +12,7 @@ function extractUniformsFromString(string) { - const maskRegex = new RegExp('^(projectionMatrix|uSampler|filterArea)$'); + const maskRegex = new RegExp('^(projectionMatrix|uSampler|filterArea|filterClamp)$'); const uniforms = {}; let nameSplit; diff --git a/src/core/renderers/webgl/managers/FilterManager.js b/src/core/renderers/webgl/managers/FilterManager.js index 43fc869..6c7422d 100644 --- a/src/core/renderers/webgl/managers/FilterManager.js +++ b/src/core/renderers/webgl/managers/FilterManager.js @@ -2,6 +2,7 @@ import RenderTarget from '../utils/RenderTarget'; import Quad from '../utils/Quad'; import { Rectangle } from '../../../math'; +import Shader from '../../../Shader'; import * as filterTransforms from '../filters/filterTransforms'; import bitTwiddle from 'bit-twiddle'; @@ -22,8 +23,6 @@ this.filters = []; this.target = null; this.resolution = 1; - - this.firstRun = true; } } @@ -155,7 +154,7 @@ if (filters.length === 1) { - filters[0].apply(this, currentState.renderTarget, lastState.renderTarget, false); + filters[0].apply(this, currentState.renderTarget, lastState.renderTarget, false, currentState); this.freePotRenderTarget(currentState.renderTarget); } else @@ -177,7 +176,7 @@ for (i = 0; i < filters.length - 1; ++i) { - filters[i].apply(this, flip, flop, true); + filters[i].apply(this, flip, flop, true, currentState); const t = flip; @@ -185,7 +184,7 @@ flop = t; } - filters[i].apply(this, flip, lastState.renderTarget, false); + filters[i].apply(this, flip, lastState.renderTarget, false, currentState); this.freePotRenderTarget(flip); this.freePotRenderTarget(flop); @@ -212,16 +211,28 @@ const renderer = this.renderer; const gl = renderer.gl; - renderer.bindShader(filter, true); + let shader = filter.glShaders[renderer.CONTEXT_UID]; - // TODO theres a better way! - // quad can be a mesh and we then should be able to pull this off without accessing the shader directly - const shader = renderer.shaderManager.getGLShader(); - - // TODO change quad to mesh.. - if (!this.firstRun) + // cacheing.. + if (!shader) { - this.firstRun = true; + if (filter.glShaderKey) + { + shader = this.shaderCache[filter.glShaderKey]; + + if (!shader) + { + shader = new Shader(this.gl, filter.vertexSrc, filter.fragmentSrc); + + filter.glShaders[renderer.CONTEXT_UID] = this.shaderCache[filter.glShaderKey] = shader; + } + } + else + { + shader = filter.glShaders[renderer.CONTEXT_UID] = new Shader(this.gl, filter.vertexSrc, filter.fragmentSrc); + } + + // TODO - this only needs to be done once? renderer.bindVao(null); this.quad.initVao(shader); @@ -244,58 +255,146 @@ renderer.maskManager.pushScissorMask(null, renderer.maskManager.scissorData); } - // renderer._bindGLShader(shader); + renderer.bindShader(shader); - let currentState; - const uniforms = filter.uniforms; + // free unit 0 for us, doesn't matter what was there + // don't try to restore it, because syncUniforms can upload it to another slot + // and it'll be a problem + const tex = this.renderer.emptyTextures[0]; - if (uniforms.filterArea) - { - currentState = this.filterData.stack[this.filterData.index]; - - const filterArea = uniforms.filterArea; - - filterArea[0] = currentState.renderTarget.size.width; - filterArea[1] = currentState.renderTarget.size.height; - filterArea[2] = currentState.sourceFrame.x; - filterArea[3] = currentState.sourceFrame.y; - - filter.uniforms.filterArea = filterArea; - } - - // use this to clamp displaced texture coords so they belong to filterArea - // see displacementFilter fragment shader for an example - if (uniforms.filterClamp) - { - currentState = this.filterData.stack[this.filterData.index]; - - const filterClamp = uniforms.filterClamp; - - filterClamp[0] = 0; - filterClamp[1] = 0; - filterClamp[2] = (currentState.sourceFrame.width - 1) / currentState.renderTarget.size.width; - filterClamp[3] = (currentState.sourceFrame.height - 1) / currentState.renderTarget.size.height; - - filter.uniforms.filterClamp = filterClamp; - } - - renderer.shaderManager.setUniforms(filter.uniforms); + this.renderer.boundTextures[0] = tex; + // this syncs the pixi filters uniforms with glsl uniforms + this.syncUniforms(shader, filter); renderer.state.setBlendMode(filter.blendMode); - // temporary bypass cache.. - const tex = this.renderer.boundTextures[0]; - gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, input.texture.texture); this.quad.vao.draw(this.renderer.gl.TRIANGLES, 6, 0); - // restore cache. gl.bindTexture(gl.TEXTURE_2D, tex._glTextures[this.renderer.CONTEXT_UID].texture); } /** + * Uploads the uniforms of the filter. + * + * @param {GLShader} shader - The underlying gl shader. + * @param {PIXI.Filter} filter - The filter we are synchronizing. + */ + syncUniforms(shader, filter) + { + const uniformData = filter.uniformData; + const uniforms = filter.uniforms; + + // 0 is reserved for the pixi texture so we start at 1! + let textureCount = 1; + let currentState; + + // filterArea and filterClamp that are handled by FilterManager directly + // they must not appear in uniformData + + if (shader.uniforms.filterArea) + { + currentState = this.filterData.stack[this.filterData.index]; + + const filterArea = shader.uniforms.filterArea; + + filterArea[0] = currentState.renderTarget.size.width; + filterArea[1] = currentState.renderTarget.size.height; + filterArea[2] = currentState.sourceFrame.x; + filterArea[3] = currentState.sourceFrame.y; + + shader.uniforms.filterArea = filterArea; + } + + // use this to clamp displaced texture coords so they belong to filterArea + // see displacementFilter fragment shader for an example + if (shader.uniforms.filterClamp) + { + currentState = currentState || this.filterData.stack[this.filterData.index]; + + const filterClamp = shader.uniforms.filterClamp; + + filterClamp[0] = 0; + filterClamp[1] = 0; + filterClamp[2] = (currentState.sourceFrame.width - 1) / currentState.renderTarget.size.width; + filterClamp[3] = (currentState.sourceFrame.height - 1) / currentState.renderTarget.size.height; + + shader.uniforms.filterClamp = filterClamp; + } + + // TODO Cacheing layer.. + for (const i in uniformData) + { + if (uniformData[i].type === 'sampler2D' && uniforms[i] !== 0) + { + if (uniforms[i].baseTexture) + { + shader.uniforms[i] = this.renderer.bindTexture(uniforms[i].baseTexture, textureCount); + } + else + { + shader.uniforms[i] = textureCount; + + // TODO + // this is helpful as renderTargets can also be set. + // Although thinking about it, we could probably + // make the filter texture cache return a RenderTexture + // rather than a renderTarget + const gl = this.renderer.gl; + + this.renderer.boundTextures[textureCount] = this.renderer.emptyTextures[textureCount]; + gl.activeTexture(gl.TEXTURE0 + textureCount); + + uniforms[i].texture.bind(); + } + + textureCount++; + } + else if (uniformData[i].type === 'mat3') + { + // check if its pixi matrix.. + if (uniforms[i].a !== undefined) + { + shader.uniforms[i] = uniforms[i].toArray(true); + } + else + { + shader.uniforms[i] = uniforms[i]; + } + } + else if (uniformData[i].type === 'vec2') + { + // check if its a point.. + if (uniforms[i].x !== undefined) + { + const val = shader.uniforms[i] || new Float32Array(2); + + val[0] = uniforms[i].x; + val[1] = uniforms[i].y; + shader.uniforms[i] = val; + } + else + { + shader.uniforms[i] = uniforms[i]; + } + } + else if (uniformData[i].type === 'float') + { + if (shader.uniforms.data[i].value !== uniformData[i]) + { + shader.uniforms[i] = uniforms[i]; + } + } + else + { + shader.uniforms[i] = uniforms[i]; + } + } + } + + /** * Gets a render target from the pool, or creates a new one. * * @param {boolean} clear - Should we clear the render texture when we get it? @@ -442,7 +541,6 @@ renderTarget.resolution = resolution; renderTarget.defaultFrame.width = renderTarget.size.width = minWidth / resolution; renderTarget.defaultFrame.height = renderTarget.size.height = minHeight / resolution; - renderTarget.attachStencilBuffer(); return renderTarget; } diff --git a/src/core/settings.js b/src/core/settings.js index bb08cb0..f4dd087 100644 --- a/src/core/settings.js +++ b/src/core/settings.js @@ -77,11 +77,11 @@ * * @static * @memberof PIXI.settings - * @type {RegExp|string} + * @type {RegExp} * @example `@2x` - * @default /@(.+)x/ + * @default /@([0-9\.]+)x/ */ - RETINA_PREFIX: /@(.+)x/, + RETINA_PREFIX: /@([0-9\.]+)x/, /** * The default render options if none are supplied to {@link PIXI.WebGLRenderer} @@ -175,14 +175,24 @@ SCALE_MODE: 0, /** - * Default specify float precision in shaders. + * Default specify float precision in vertex shader. + * + * @static + * @memberof PIXI.settings + * @type {PIXI.PRECISION} + * @default PIXI.PRECISION.HIGH + */ + PRECISION_VERTEX: 'highp', + + /** + * Default specify float precision in fragment shader. * * @static * @memberof PIXI.settings * @type {PIXI.PRECISION} * @default PIXI.PRECISION.MEDIUM */ - PRECISION: 'mediump', + PRECISION_FRAGMENT: 'mediump', /** * Can we upload the same buffer in a single frame? diff --git a/src/core/sprites/Sprite.js b/src/core/sprites/Sprite.js index 28b0b18..121bf58 100644 --- a/src/core/sprites/Sprite.js +++ b/src/core/sprites/Sprite.js @@ -212,11 +212,11 @@ } else { - w0 = orig.width * (1 - anchor._x); - w1 = orig.width * -anchor._x; + w1 = -anchor._x * orig.width; + w0 = w1 + orig.width; - h0 = orig.height * (1 - anchor._y); - h1 = orig.height * -anchor._y; + h1 = -anchor._y * orig.height; + h0 = h1 + orig.height; } // xy @@ -269,11 +269,11 @@ const tx = wt.tx; const ty = wt.ty; - const w0 = (orig.width) * (1 - anchor._x); - const w1 = (orig.width) * -anchor._x; + const w1 = -anchor._x * orig.width; + const w0 = w1 + orig.width; - const h0 = orig.height * (1 - anchor._y); - const h1 = orig.height * -anchor._y; + const h1 = -anchor._y * orig.height; + const h0 = h1 + orig.height; // xy vertexData[0] = (a * w1) + (c * h1) + tx; @@ -440,7 +440,7 @@ * * @static * @param {number|string|PIXI.BaseTexture|HTMLCanvasElement|HTMLVideoElement} source Source to create texture from - * @return {PIXI.Sprite} The newly created texture + * @return {PIXI.Sprite} The newly created sprite */ static from(source) { @@ -538,8 +538,8 @@ } /** - * The tint applied to the sprite. This is a hex value. A value of - * 0xFFFFFF will remove any tint effect. + * The tint applied to the sprite. This is a hex value. + * A value of 0xFFFFFF will remove any tint effect. * * @member {number} * @default 0xFFFFFF diff --git a/src/core/sprites/canvas/CanvasTinter.js b/src/core/sprites/canvas/CanvasTinter.js index 1cf316b..27e5e0c 100644 --- a/src/core/sprites/canvas/CanvasTinter.js +++ b/src/core/sprites/canvas/CanvasTinter.js @@ -75,8 +75,8 @@ crop.width *= resolution; crop.height *= resolution; - canvas.width = crop.width; - canvas.height = crop.height; + canvas.width = Math.ceil(crop.width); + canvas.height = Math.ceil(crop.height); context.fillStyle = `#${(`00000${(color | 0).toString(16)}`).substr(-6)}`; @@ -130,8 +130,8 @@ crop.width *= resolution; crop.height *= resolution; - canvas.width = crop.width; - canvas.height = crop.height; + canvas.width = Math.ceil(crop.width); + canvas.height = Math.ceil(crop.height); context.globalCompositeOperation = 'copy'; context.fillStyle = `#${(`00000${(color | 0).toString(16)}`).substr(-6)}`; @@ -172,8 +172,8 @@ crop.width *= resolution; crop.height *= resolution; - canvas.width = crop.width; - canvas.height = crop.height; + canvas.width = Math.ceil(crop.width); + canvas.height = Math.ceil(crop.height); context.globalCompositeOperation = 'copy'; context.drawImage( diff --git a/src/core/sprites/webgl/SpriteRenderer.js b/src/core/sprites/webgl/SpriteRenderer.js index 2affcd5..7c352e2 100644 --- a/src/core/sprites/webgl/SpriteRenderer.js +++ b/src/core/sprites/webgl/SpriteRenderer.js @@ -30,7 +30,7 @@ /** * Number of values sent in the vertex buffer. - * positionX, positionY, colorR, colorG, colorB = 5 + * aVertexPosition(2), aTextureCoord(1), aColor(1), aTextureId(1) = 5 * * @member {number} */ @@ -75,7 +75,6 @@ this.shader = null; this.currentIndex = 0; - TICK = 0; this.groups = []; for (let k = 0; k < this.size; k++) diff --git a/src/core/textures/BaseTexture.js b/src/core/textures/BaseTexture.js index 853de7e..aeccf39 100644 --- a/src/core/textures/BaseTexture.js +++ b/src/core/textures/BaseTexture.js @@ -232,16 +232,24 @@ this.realWidth = this.source.naturalWidth || this.source.videoWidth || this.source.width; this.realHeight = this.source.naturalHeight || this.source.videoHeight || this.source.height; - this.width = this.realWidth / this.resolution; - this.height = this.realHeight / this.resolution; - - this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); + this._updateDimensions(); } this.emit('update', this); } /** + * Update dimensions from real values + */ + _updateDimensions() + { + this.width = this.realWidth / this.resolution; + this.height = this.realHeight / this.resolution; + + this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); + } + + /** * Load a source. * * If the source is not-immediately-available, such as an image that needs to be @@ -525,11 +533,7 @@ this.realWidth = Math.round(svgWidth * this.sourceScale); this.realHeight = Math.round(svgHeight * this.sourceScale); - this.width = this.realWidth / this.resolution; - this.height = this.realHeight / this.resolution; - - // Check pow2 after scale - this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); + this._updateDimensions(); // Create a canvas element const canvas = document.createElement('canvas'); @@ -690,4 +694,52 @@ return baseTexture; } + + /** + * Helper function that creates a base texture based on the source you provide. + * The source can be - image url, image element, canvas element. + * + * @static + * @param {string|HTMLImageElement|HTMLCanvasElement} source - The source to create base texture from. + * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values + * @param {number} [sourceScale=(auto)] - Scale for the original image, used with Svg images. + * @return {PIXI.BaseTexture} The new base texture. + */ + static from(source, scaleMode, sourceScale) + { + if (typeof source === 'string') + { + return BaseTexture.fromImage(source, undefined, scaleMode, sourceScale); + } + else if (source instanceof HTMLImageElement) + { + const imageUrl = source.src; + let baseTexture = BaseTextureCache[imageUrl]; + + if (!baseTexture) + { + baseTexture = new BaseTexture(source, scaleMode); + baseTexture.imageUrl = imageUrl; + + if (sourceScale) + { + baseTexture.sourceScale = sourceScale; + } + + // if there is an @2x at the end of the url we are going to assume its a highres image + baseTexture.resolution = getResolutionOfUrl(imageUrl); + + BaseTextureCache[imageUrl] = baseTexture; + } + + return baseTexture; + } + else if (source instanceof HTMLCanvasElement) + { + return BaseTexture.fromCanvas(source, scaleMode); + } + + // lets assume its a base texture! + return source; + } } diff --git a/src/core/textures/Spritesheet.js b/src/core/textures/Spritesheet.js new file mode 100644 index 0000000..d469c62 --- /dev/null +++ b/src/core/textures/Spritesheet.js @@ -0,0 +1,274 @@ +import { Rectangle, Texture } from '../'; +import { getResolutionOfUrl, TextureCache } from '../utils'; + +/** + * Utility class for maintaining reference to a collection + * of Textures on a single Spritesheet. + * + * @class + * @memberof PIXI + */ +export default class Spritesheet +{ + /** + * The maximum number of Textures to build per process. + * + * @type {number} + * @default 1000 + */ + static get BATCH_SIZE() + { + return 1000; + } + + /** + * @param {PIXI.BaseTexture} baseTexture Reference to the source BaseTexture object. + * @param {Object} data - Spritesheet image data. + * @param {string} [resolutionFilename] - The filename to consider when determining + * the resolution of the spritesheet. If not provided, the imageUrl will + * be used on the BaseTexture. + */ + constructor(baseTexture, data, resolutionFilename = null) + { + /** + * Reference to ths source texture + * @type {PIXI.BaseTexture} + */ + this.baseTexture = baseTexture; + + /** + * Map of spritesheet textures. + * @type {Object} + */ + this.textures = {}; + + /** + * Reference to the original JSON data. + * @type {Object} + */ + this.data = data; + + /** + * The resolution of the spritesheet. + * @type {number} + */ + this.resolution = this._updateResolution( + resolutionFilename || this.baseTexture.imageUrl + ); + + /** + * Map of spritesheet frames. + * @type {Object} + * @private + */ + this._frames = this.data.frames; + + /** + * Collection of frame names. + * @type {string[]} + * @private + */ + this._frameKeys = Object.keys(this._frames); + + /** + * Current batch index being processed. + * @type {number} + * @private + */ + this._batchIndex = 0; + + /** + * Callback when parse is completed. + * @type {Function} + * @private + */ + this._callback = null; + } + + /** + * Generate the resolution from the filename or fallback + * to the meta.scale field of the JSON data. + * + * @private + * @param {string} resolutionFilename - The filename to use for resolving + * the default resolution. + * @return {number} Resolution to use for spritesheet. + */ + _updateResolution(resolutionFilename) + { + const scale = this.data.meta.scale; + + // Use a defaultValue of `null` to check if a url-based resolution is set + let resolution = getResolutionOfUrl(resolutionFilename, null); + + // No resolution found via URL + if (resolution === null) + { + // Use the scale value or default to 1 + resolution = scale !== undefined ? parseFloat(scale) : 1; + } + + // For non-1 resolutions, update baseTexture + if (resolution !== 1) + { + this.baseTexture.resolution = resolution; + this.baseTexture.update(); + } + + return resolution; + } + + /** + * Parser spritesheet from loaded data. This is done asynchronously + * to prevent creating too many Texture within a single process. + * + * @param {Function} callback - Callback when complete returns + * a map of the Textures for this spritesheet. + */ + parse(callback) + { + this._batchIndex = 0; + this._callback = callback; + + if (this._frameKeys.length <= Spritesheet.BATCH_SIZE) + { + this._processFrames(0); + this._parseComplete(); + } + else + { + this._nextBatch(); + } + } + + /** + * Process a batch of frames + * + * @private + * @param {number} initialFrameIndex - The index of frame to start. + */ + _processFrames(initialFrameIndex) + { + let frameIndex = initialFrameIndex; + const maxFrames = Spritesheet.BATCH_SIZE; + + while (frameIndex - initialFrameIndex < maxFrames && frameIndex < this._frameKeys.length) + { + const i = this._frameKeys[frameIndex]; + const rect = this._frames[i].frame; + + if (rect) + { + let frame = null; + let trim = null; + const orig = new Rectangle( + 0, + 0, + this._frames[i].sourceSize.w / this.resolution, + this._frames[i].sourceSize.h / this.resolution + ); + + if (this._frames[i].rotated) + { + frame = new Rectangle( + rect.x / this.resolution, + rect.y / this.resolution, + rect.h / this.resolution, + rect.w / this.resolution + ); + } + else + { + frame = new Rectangle( + rect.x / this.resolution, + rect.y / this.resolution, + rect.w / this.resolution, + rect.h / this.resolution + ); + } + + // Check to see if the sprite is trimmed + if (this._frames[i].trimmed) + { + trim = new Rectangle( + this._frames[i].spriteSourceSize.x / this.resolution, + this._frames[i].spriteSourceSize.y / this.resolution, + rect.w / this.resolution, + rect.h / this.resolution + ); + } + + this.textures[i] = new Texture( + this.baseTexture, + frame, + orig, + trim, + this._frames[i].rotated ? 2 : 0 + ); + + // lets also add the frame to pixi's global cache for fromFrame and fromImage functions + TextureCache[i] = this.textures[i]; + } + + frameIndex++; + } + } + + /** + * The parse has completed. + * + * @private + */ + _parseComplete() + { + const callback = this._callback; + + this._callback = null; + this._batchIndex = 0; + callback.call(this, this.textures); + } + + /** + * Begin the next batch of textures. + * + * @private + */ + _nextBatch() + { + this._processFrames(this._batchIndex * Spritesheet.BATCH_SIZE); + this._batchIndex++; + setTimeout(() => + { + if (this._batchIndex * Spritesheet.BATCH_SIZE < this._frameKeys.length) + { + this._nextBatch(); + } + else + { + this._parseComplete(); + } + }, 0); + } + + /** + * Destroy Spritesheet and don't use after this. + * + * @param {boolean} [destroyBase=false] Whether to destroy the base texture as well + */ + destroy(destroyBase = false) + { + for (const i in this.textures) + { + this.textures[i].destroy(); + } + this._frames = null; + this._frameKeys = null; + this.data = null; + this.textures = null; + if (destroyBase) + { + this.baseTexture.destroy(); + } + this.baseTexture = null; + } +} diff --git a/src/core/textures/Texture.js b/src/core/textures/Texture.js index 6403d4f..afe4254 100644 --- a/src/core/textures/Texture.js +++ b/src/core/textures/Texture.js @@ -3,7 +3,7 @@ import TextureUvs from './TextureUvs'; import EventEmitter from 'eventemitter3'; import { Rectangle } from '../math'; -import { TextureCache, BaseTextureCache } from '../utils'; +import { TextureCache, BaseTextureCache, getResolutionOfUrl } from '../utils'; /** * A texture stores the information that represents an image or part of an image. It cannot be added @@ -365,7 +365,8 @@ * The source can be - frame id, image url, video url, canvas element, video element, base texture * * @static - * @param {number|string|PIXI.BaseTexture|HTMLCanvasElement|HTMLVideoElement} source - Source to create texture from + * @param {number|string|HTMLImageElement|HTMLCanvasElement|HTMLVideoElement|PIXI.BaseTexture} + * source - Source to create texture from * @return {PIXI.Texture} The newly created texture */ static from(source) @@ -393,7 +394,7 @@ } else if (source instanceof HTMLImageElement) { - return new Texture(new BaseTexture(source)); + return new Texture(BaseTexture.from(source)); } else if (source instanceof HTMLCanvasElement) { @@ -413,6 +414,43 @@ } /** + * Create a texture from a source and add to the cache. + * + * @static + * @param {HTMLImageElement|HTMLCanvasElement} source - The input source. + * @param {String} imageUrl - File name of texture, for cache and resolving resolution. + * @param {String} [name] - Human readible name for the texture cache. If no name is + * specified, only `imageUrl` will be used as the cache ID. + * @return {PIXI.Texture} Output texture + */ + static fromLoader(source, imageUrl, name) + { + const baseTexture = new BaseTexture(source, null, getResolutionOfUrl(imageUrl)); + const texture = new Texture(baseTexture); + + baseTexture.imageUrl = imageUrl; + + // No name, use imageUrl instead + if (!name) + { + name = imageUrl; + } + + // lets also add the frame to pixi's global cache for fromFrame and fromImage fucntions + BaseTextureCache[name] = baseTexture; + TextureCache[name] = texture; + + // also add references by url if they are different. + if (name !== imageUrl) + { + BaseTextureCache[imageUrl] = baseTexture; + TextureCache[imageUrl] = texture; + } + + return texture; + } + + /** * Adds a texture to the global TextureCache. This cache is shared across the whole PIXI object. * * @static @@ -522,6 +560,29 @@ } } +function createWhiteTexture() +{ + const canvas = document.createElement('canvas'); + + canvas.width = 10; + canvas.height = 10; + + const context = canvas.getContext('2d'); + + context.fillStyle = 'white'; + context.fillRect(0, 0, 10, 10); + + return new Texture(new BaseTexture(canvas)); +} + +function removeAllHandlers(tex) +{ + tex.destroy = function _emptyDestroy() { /* empty */ }; + tex.on = function _emptyOn() { /* empty */ }; + tex.once = function _emptyOnce() { /* empty */ }; + tex.emit = function _emptyEmit() { /* empty */ }; +} + /** * An empty texture, used often to not have to create multiple empty textures. * Can not be destroyed. @@ -530,7 +591,14 @@ * @constant */ Texture.EMPTY = new Texture(new BaseTexture()); -Texture.EMPTY.destroy = function _emptyDestroy() { /* empty */ }; -Texture.EMPTY.on = function _emptyOn() { /* empty */ }; -Texture.EMPTY.once = function _emptyOnce() { /* empty */ }; -Texture.EMPTY.emit = function _emptyEmit() { /* empty */ }; +removeAllHandlers(Texture.EMPTY); + +/** + * A white texture of 10x10 size, used for graphics and other things + * Can not be destroyed. + * + * @static + * @constant + */ +Texture.WHITE = createWhiteTexture(); +removeAllHandlers(Texture.WHITE); diff --git a/src/core/ticker/Ticker.js b/src/core/ticker/Ticker.js index d98a1f0..17a1517 100644 --- a/src/core/ticker/Ticker.js +++ b/src/core/ticker/Ticker.js @@ -67,11 +67,12 @@ * is based, this value is neither capped nor scaled. * If the platform supports DOMHighResTimeStamp, * this value will have a precision of 1 µs. + * Defaults to target frame time * * @member {number} - * @default 1 / TARGET_FPMS + * @default 16.66 */ - this.elapsedMS = 1 / settings.TARGET_FPMS; // default to target frame time + this.elapsedMS = 1 / settings.TARGET_FPMS; /** * The last time {@link PIXI.ticker.Ticker#update} was invoked. diff --git a/src/core/utils/index.js b/src/core/utils/index.js index 6ff609f..be44750 100644 --- a/src/core/utils/index.js +++ b/src/core/utils/index.js @@ -2,6 +2,7 @@ import settings from '../settings'; import EventEmitter from 'eventemitter3'; import pluginTarget from './pluginTarget'; +import * as mixins from './mixin'; import * as isMobile from 'ismobilejs'; let nextUid = 0; @@ -33,6 +34,7 @@ * @type {mixin} */ pluginTarget, + mixins, }; /** @@ -93,7 +95,7 @@ */ export function rgb2hex(rgb) { - return (((rgb[0] * 255) << 16) + ((rgb[1] * 255) << 8) + (rgb[2] * 255)); + return (((rgb[0] * 255) << 16) + ((rgb[1] * 255) << 8) + (rgb[2] * 255 | 0)); } /** @@ -368,3 +370,43 @@ * @private */ export const ProgramCache = {}; + +/** + * Destroys all texture in the cache + * + * @memberof PIXI.utils + * @function destroyTextureCache + */ +export function destroyTextureCache() +{ + let key; + + for (key in TextureCache) + { + TextureCache[key].destroy(); + } + for (key in BaseTextureCache) + { + BaseTextureCache[key].destroy(); + } +} + +/** + * Removes all textures from cache, but does not destroy them + * + * @memberof PIXI.utils + * @function clearTextureCache + */ +export function clearTextureCache() +{ + let key; + + for (key in TextureCache) + { + delete TextureCache[key]; + } + for (key in BaseTextureCache) + { + delete BaseTextureCache[key]; + } +} diff --git a/src/core/utils/mixin.js b/src/core/utils/mixin.js new file mode 100644 index 0000000..114de2a --- /dev/null +++ b/src/core/utils/mixin.js @@ -0,0 +1,59 @@ +/** + * Mixes all enumerable properties and methods from a source object to a target object. + * + * @memberof PIXI.utils.mixins + * @function mixin + * @param {object} target The prototype or instance that properties and methods should be added to. + * @param {object} source The source of properties and methods to mix in. + */ +export function mixin(target, source) +{ + if (!target || !source) return; + // in ES8/ES2017, this would be really easy: + // Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); + + // get all the enumerable property keys + const keys = Object.keys(source); + + // loop through properties + for (let i = 0; i < keys.length; ++i) + { + const propertyName = keys[i]; + + // Set the property using the property descriptor - this works for accessors and normal value properties + Object.defineProperty(target, propertyName, Object.getOwnPropertyDescriptor(source, propertyName)); + } +} + +const mixins = []; + +/** + * Queues a mixin to be handled towards the end of the initialization of PIXI, so that deprecation + * can take effect. + * + * @memberof PIXI.utils.mixins + * @function delayMixin + * @private + * @param {object} target The prototype or instance that properties and methods should be added to. + * @param {object} source The source of properties and methods to mix in. + */ +export function delayMixin(target, source) +{ + mixins.push(target, source); +} + +/** + * Handles all mixins queued via delayMixin(). + * + * @memberof PIXI.utils.mixins + * @function performMixins + * @private + */ +export function performMixins() +{ + for (let i = 0; i < mixins.length; i += 2) + { + mixin(mixins[i], mixins[i + 1]); + } + mixins.length = 0; +} diff --git a/src/deprecation.js b/src/deprecation.js index 045a19f..b9c73b3 100644 --- a/src/deprecation.js +++ b/src/deprecation.js @@ -5,6 +5,7 @@ import * as filters from './filters'; import * as prepare from './prepare'; import * as loaders from './loaders'; +import * as interaction from './interaction'; // provide method to give a stack track for warnings // useful for tracking-down where deprecated methods/properties/classes @@ -534,7 +535,7 @@ { parent: 'GC_MODES', target: 'GC_MODE' }, { parent: 'WRAP_MODES', target: 'WRAP_MODE' }, { parent: 'SCALE_MODES', target: 'SCALE_MODE' }, - { parent: 'PRECISION', target: 'PRECISION' }, + { parent: 'PRECISION', target: 'PRECISION_FRAGMENT' }, ]; for (let i = 0; i < defaults.length; i++) @@ -558,6 +559,32 @@ }); } +Object.defineProperties(core.settings, { + + /** + * @static + * @name PRECISION + * @memberof PIXI.settings + * @see PIXI.PRECISION + * @deprecated since version 4.4.0 + */ + PRECISION: { + enumerable: true, + get() + { + warn('PIXI.settings.PRECISION has been deprecated, please use PIXI.settings.PRECISION_FRAGMENT'); + + return core.settings.PRECISION_FRAGMENT; + }, + set(value) + { + warn('PIXI.settings.PRECISION has been deprecated, please use PIXI.settings.PRECISION_FRAGMENT'); + + core.settings.PRECISION_FRAGMENT = value; + }, + }, +}); + Object.defineProperties(extras, { /** @@ -694,7 +721,7 @@ warn('determineFontProperties is now deprecated, please use the static calculateFontProperties method, ' + 'e.g : Text.calculateFontProperties(fontStyle);'); - return Text.calculateFontProperties(fontStyle); + return core.Text.calculateFontProperties(fontStyle); }; Object.defineProperties(core.TextStyle.prototype, { @@ -1003,3 +1030,69 @@ }, }, }); + +/** + * @name PIXI.interaction.interactiveTarget#defaultCursor + * @static + * @type {number} + * @see PIXI.interaction.interactiveTarget#cursor + * @deprecated since 4.3.0 + */ +Object.defineProperty(interaction.interactiveTarget, 'defaultCursor', { + set(value) + { + warn('Property defaultCursor has been replaced with \'cursor\'. '); + this.cursor = value; + }, + get() + { + warn('Property defaultCursor has been replaced with \'cursor\'. '); + + return this.cursor; + }, + enumerable: true, +}); + +/** + * @name PIXI.interaction.InteractionManager#defaultCursorStyle + * @static + * @type {string} + * @see PIXI.interaction.InteractionManager#cursorStyles + * @deprecated since 4.3.0 + */ +Object.defineProperty(interaction.InteractionManager, 'defaultCursorStyle', { + set(value) + { + warn('Property defaultCursorStyle has been replaced with \'cursorStyles.default\'. '); + this.cursorStyles.default = value; + }, + get() + { + warn('Property defaultCursorStyle has been replaced with \'cursorStyles.default\'. '); + + return this.cursorStyles.default; + }, +}); + +/** + * @name PIXI.interaction.InteractionManager#currentCursorStyle + * @static + * @type {string} + * @see PIXI.interaction.InteractionManager#cursorStyles + * @deprecated since 4.3.0 + */ +Object.defineProperty(interaction.InteractionManager, 'currentCursorStyle', { + set(value) + { + warn('Property currentCursorStyle has been removed.' + + 'See the currentCursorMode property, which works differently.'); + this.currentCursorMode = value; + }, + get() + { + warn('Property currentCursorStyle has been removed.' + + 'See the currentCursorMode property, which works differently.'); + + return this.currentCursorMode; + }, +}); diff --git a/src/extras/BitmapText.js b/src/extras/BitmapText.js index fb6c343..0a0e150 100644 --- a/src/extras/BitmapText.js +++ b/src/extras/BitmapText.js @@ -455,6 +455,73 @@ return this._textHeight; } + + /** + * Register a bitmap font with data and a texture. + * + * @static + * @param {XMLDocument} xml - The XML document data. + * @param {PIXI.Texture} texture - Texture with all symbols. + * @return {Object} Result font object with font, size, lineHeight and char fields. + */ + static registerFont(xml, texture) + { + const data = {}; + const info = xml.getElementsByTagName('info')[0]; + const common = xml.getElementsByTagName('common')[0]; + + data.font = info.getAttribute('face'); + data.size = parseInt(info.getAttribute('size'), 10); + data.lineHeight = parseInt(common.getAttribute('lineHeight'), 10); + data.chars = {}; + + // parse letters + const letters = xml.getElementsByTagName('char'); + + for (let i = 0; i < letters.length; i++) + { + const letter = letters[i]; + const charCode = parseInt(letter.getAttribute('id'), 10); + + const textureRect = new core.Rectangle( + parseInt(letter.getAttribute('x'), 10) + texture.frame.x, + parseInt(letter.getAttribute('y'), 10) + texture.frame.y, + parseInt(letter.getAttribute('width'), 10), + parseInt(letter.getAttribute('height'), 10) + ); + + data.chars[charCode] = { + xOffset: parseInt(letter.getAttribute('xoffset'), 10), + yOffset: parseInt(letter.getAttribute('yoffset'), 10), + xAdvance: parseInt(letter.getAttribute('xadvance'), 10), + kerning: {}, + texture: new core.Texture(texture.baseTexture, textureRect), + + }; + } + + // parse kernings + const kernings = xml.getElementsByTagName('kerning'); + + for (let i = 0; i < kernings.length; i++) + { + const kerning = kernings[i]; + const first = parseInt(kerning.getAttribute('first'), 10); + const second = parseInt(kerning.getAttribute('second'), 10); + const amount = parseInt(kerning.getAttribute('amount'), 10); + + if (data.chars[second]) + { + data.chars[second].kerning[first] = amount; + } + } + + // I'm leaving this as a temporary fix so we can test the bitmap fonts in v3 + // but it's very likely to change + BitmapText.fonts[data.font] = data; + + return data; + } } BitmapText.fonts = {}; diff --git a/src/index.js b/src/index.js index 59356cf..c553427 100644 --- a/src/index.js +++ b/src/index.js @@ -16,6 +16,10 @@ import * as particles from './particles'; import * as prepare from './prepare'; +// handle mixins now, after all code has been added, including deprecation +import { utils } from './core'; +utils.mixins.performMixins(); + export { accessibility, extract, diff --git a/src/interaction/InteractionManager.js b/src/interaction/InteractionManager.js index 7d51134..8baf13a 100644 --- a/src/interaction/InteractionManager.js +++ b/src/interaction/InteractionManager.js @@ -1,18 +1,20 @@ 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'; -// Mix interactiveTarget into core.DisplayObject.prototype -Object.assign( +// Mix interactiveTarget into core.DisplayObject.prototype, after deprecation has been handled +core.utils.mixins.delayMixin( core.DisplayObject.prototype, 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 +70,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 +101,6 @@ this.eventData = new InteractionEvent(); /** - * Tiny little interactiveData pool ! - * - * @member {PIXI.interaction.InteractionData[]} - */ - this.interactiveDataPool = []; - - /** * The DOM element to bind to. * * @private @@ -153,66 +155,12 @@ */ this.supportsPointerEvents = !!window.PointerEvent; - /** - * Are touch events being 'normalized' and converted into pointer events if pointer events are not supported - * For example, on a touch screen mobile device, a touchstart would also be emitted as a pointerdown - * - * @private - * @readonly - * @member {boolean} - */ - this.normalizeTouchEvents = !this.supportsPointerEvents && this.supportsTouchEvents; - - /** - * Are mouse events being 'normalized' and converted into pointer events if pointer events are not supported - * For example, on a desktop pc, a mousedown would also be emitted as a pointerdown - * - * @private - * @readonly - * @member {boolean} - */ - this.normalizeMouseEvents = !this.supportsPointerEvents && !MobileDevice.any; - // this will make it so that you don't have to call bind all the time /** * @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 +168,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,41 +199,32 @@ this.onPointerOver = this.onPointerOver.bind(this); /** - * @private - * @member {Function} + * Dictionary of how different cursor modes are handled. Strings are handled as CSS cursor + * values, objects are handled as dictionaries of CSS values for interactionDOMElement, + * and functions are called instead of changing the CSS. + * Default CSS cursor values are provided for 'default' and 'pointer' modes. + * @member {Object.)>} */ - this.onTouchStart = this.onTouchStart.bind(this); - this.processTouchStart = this.processTouchStart.bind(this); + this.cursorStyles = { + default: 'inherit', + pointer: 'pointer', + }; /** - * @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. - * - * @member {string} - * @default 'inherit' - */ - this.defaultCursorStyle = 'inherit'; - - /** - * The css style of the cursor that is being used. + * The mode of the cursor that is being used. + * The value of this is a key from the cursorStyles dictionary. * * @member {string} */ - this.currentCursorStyle = 'inherit'; + this.currentCursorMode = null; + + /** + * Internal cached let. + * + * @private + * @member {string} + */ + this.cursor = null; /** * Internal cached let. @@ -303,6 +249,7 @@ * object. * * @event mousedown + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -311,6 +258,7 @@ * on the display object. * * @event rightdown + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -319,6 +267,7 @@ * object. * * @event mouseup + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -327,6 +276,7 @@ * over the display object. * * @event rightup + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -335,6 +285,7 @@ * the display object. * * @event click + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -343,6 +294,7 @@ * and released on the display object. * * @event rightclick + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -352,6 +304,7 @@ * [mousedown]{@link PIXI.interaction.InteractionManager#event:mousedown}. * * @event mouseupoutside + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -361,6 +314,7 @@ * [rightdown]{@link PIXI.interaction.InteractionManager#event:rightdown}. * * @event rightupoutside + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -368,6 +322,7 @@ * Fired when a pointer device (usually a mouse) is moved while over the display object * * @event mousemove + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -375,6 +330,7 @@ * Fired when a pointer device (usually a mouse) is moved onto the display object * * @event mouseover + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -382,6 +338,7 @@ * Fired when a pointer device (usually a mouse) is moved off the display object * * @event mouseout + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -389,6 +346,7 @@ * Fired when a pointer device button is pressed on the display object. * * @event pointerdown + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -396,6 +354,14 @@ * Fired when a pointer device button is released over the display object. * * @event pointerup + * @type {PIXI.interaction.InteractionData} + * @memberof PIXI.interaction.InteractionManager# + */ + + /** + * Fired when the operating system cancels a pointer event + * + * @event pointercancel * @memberof PIXI.interaction.InteractionManager# */ @@ -403,6 +369,7 @@ * Fired when a pointer device button is pressed and released on the display object. * * @event pointertap + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -411,6 +378,7 @@ * registered a [pointerdown]{@link PIXI.interaction.InteractionManager#event:pointerdown}. * * @event pointerupoutside + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -418,6 +386,7 @@ * Fired when a pointer device is moved while over the display object * * @event pointermove + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -425,6 +394,7 @@ * Fired when a pointer device is moved onto the display object * * @event pointerover + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -432,6 +402,7 @@ * Fired when a pointer device is moved off the display object * * @event pointerout + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -439,6 +410,7 @@ * Fired when a touch point is placed on the display object. * * @event touchstart + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -446,6 +418,14 @@ * Fired when a touch point is removed from the display object. * * @event touchend + * @type {PIXI.interaction.InteractionData} + * @memberof PIXI.interaction.InteractionManager# + */ + + /** + * Fired when the operating system cancels a touch + * + * @event touchcancel * @memberof PIXI.interaction.InteractionManager# */ @@ -453,6 +433,7 @@ * Fired when a touch point is placed and removed from the display object. * * @event tap + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -461,6 +442,7 @@ * registered a [touchstart]{@link PIXI.interaction.InteractionManager#event:touchstart}. * * @event touchendoutside + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -468,6 +450,7 @@ * Fired when a touch point is moved along the display object. * * @event touchmove + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ } @@ -524,45 +507,30 @@ { window.document.addEventListener('pointermove', this.onPointerMove, true); this.interactionDOMElement.addEventListener('pointerdown', this.onPointerDown, true); - this.interactionDOMElement.addEventListener('pointerout', this.onPointerOut, true); + // pointerout is fired in addition to pointerup (for touch events) and pointercancel + // we already handle those, so for the purposes of what we do in onPointerOut, we only + // care about the pointerleave event + this.interactionDOMElement.addEventListener('pointerleave', 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; @@ -596,45 +564,26 @@ { window.document.removeEventListener('pointermove', this.onPointerMove, true); this.interactionDOMElement.removeEventListener('pointerdown', this.onPointerDown, true); - this.interactionDOMElement.removeEventListener('pointerout', this.onPointerOut, true); + this.interactionDOMElement.removeEventListener('pointerleave', 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) + 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.onPointerDown, true); + this.interactionDOMElement.removeEventListener('touchcancel', this.onPointerCancel, 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); - - 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 = null; @@ -664,7 +613,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; @@ -672,25 +621,80 @@ return; } - this.cursor = this.defaultCursorStyle; + this.cursor = null; // 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(); - - this.processInteractive(this.mouse.global, this.renderer._lastObjectRendered, this.processMouseOverOut, true); - - if (this.currentCursorStyle !== this.cursor) + for (const k in this.activeInteractionData) { - this.currentCursorStyle = this.cursor; - this.interactionDOMElement.style.cursor = this.cursor; + // eslint-disable-next-line no-prototype-builtins + if (this.activeInteractionData.hasOwnProperty(k)) + { + const interactionData = this.activeInteractionData[k]; + + if (interactionData.originalEvent && interactionData.pointerType !== 'touch') + { + const interactionEvent = this.configureInteractionEventForDOMEvent( + this.eventData, + interactionData.originalEvent, + interactionData + ); + + this.processInteractive( + interactionEvent, + this.renderer._lastObjectRendered, + this.processPointerOverOut, + true + ); + } + } } + this.setCursorMode(this.cursor); + // TODO } /** + * Sets the current cursor mode, handling any callbacks or CSS style changes. + * + * @param {string} mode - cursor mode, a key from the cursorStyles dictionary + */ + setCursorMode(mode) + { + mode = mode || 'default'; + // if the mode didn't actually change, bail early + if (this.currentCursorMode === mode) + { + return; + } + this.currentCursorMode = mode; + const style = this.cursorStyles[mode]; + + // only do things if there is a cursor style for it + if (style) + { + switch (typeof style) + { + case 'string': + // string styles are handled as cursor CSS + this.interactionDOMElement.style.cursor = style; + break; + case 'function': + // functions are just called, and passed the cursor mode + style(mode); + break; + case 'object': + // if it is an object, assume that it is a dictionary of CSS styles, + // apply it to the interactionDOMElement + Object.assign(this.interactionDOMElement.style, style); + break; + } + } + } + + /** * Dispatches an event on the display object that was interacted with * * @param {PIXI.Container|PIXI.Sprite|PIXI.extras.TilingSprite} displayObject - the display object in question @@ -748,22 +752,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 @@ -787,9 +795,8 @@ { interactiveParent = false; } - - // it has a mask! Then lets hit test that before continuing.. - if (hitTest && displayObject._mask) + // it has a mask! Then lets hit test that before continuing + else if (hitTest && displayObject._mask) { if (!displayObject._mask.containsPoint(point)) { @@ -797,14 +804,7 @@ } } - // it has a filterArea! Same as mask but easier, its a rectangle - if (hitTest && displayObject.filterArea) - { - if (!displayObject.filterArea.contains(point.x, point.y)) - { - hitTest = false; - } - } + let keepHitTestingAfterChildren = hitTest; // ** FREE TIP **! If an object is not interactive or has no buttons in it // (such as a game scene!) set interactiveChildren to false for that displayObject. @@ -818,7 +818,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 @@ -838,15 +838,20 @@ // This means we no longer need to hit test anything else. We still need to run // through all objects, but we don't need to perform any hit tests. - // { - hitTest = false; - // } + keepHitTestingAfterChildren = false; + + if (child.interactive) + { + hitTest = false; + } // we can break now as we have hit an object. } } } + hitTest = keepHitTestingAfterChildren; + // no point running this if the item is not interactive or does not have an interactive parent. if (interactive) { @@ -868,14 +873,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 +886,175 @@ } /** - * 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].isNormalized) { - 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); + if (!displayObject.trackedPointers[id]) + { + displayObject.trackedPointers[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.trackedPointers[id].rightDown = true; + } + else + { + displayObject.trackedPointers[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.trackedPointers[id] !== undefined) + { + delete displayObject.trackedPointers[id]; + this.dispatchEvent(displayObject, 'pointercancel', interactionEvent); + + if (e.type === 'touchcancel' || e.pointerType === 'touch') + { + this.dispatchEvent(displayObject, 'touchcancel', interactionEvent); + } } } @@ -1159,42 +1066,94 @@ */ 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) { - if (hit) - { - this.dispatchEvent(displayObject, 'pointerup', this.eventData); + const e = interactionEvent.data.originalEvent; - if (displayObject._pointerDown) + const id = interactionEvent.data.identifier; + + const trackingData = displayObject.trackedPointers[id]; + + const isTouch = (e.type === 'touchend' || e.pointerType === 'touch'); + + const isMouse = (e.type.indexOf('mouse') === 0 || e.pointerType === 'mouse'); + + // 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) { - displayObject._pointerDown = false; - this.dispatchEvent(displayObject, 'pointertap', this.eventData); + 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); + } + // update the down state of the tracking data + if (trackingData) + { + if (isRightButton) + { + trackingData.rightDown = hit; + } + else + { + trackingData.leftDown = hit; + } } } - else if (displayObject._pointerDown) + + // Pointers and Touches, and Mouse + if (hit) { - displayObject._pointerDown = false; - this.dispatchEvent(displayObject, 'pointerupoutside', this.eventData); + this.dispatchEvent(displayObject, 'pointerup', interactionEvent); + if (isTouch) this.dispatchEvent(displayObject, 'touchend', interactionEvent); + + if (trackingData) + { + this.dispatchEvent(displayObject, 'pointertap', interactionEvent); + if (isTouch) + { + this.dispatchEvent(displayObject, 'tap', interactionEvent); + // touches are no longer over (if they ever were) when we get the touchend + // so we should ensure that we don't keep pretending that they are + trackingData.over = false; + } + } + } + else if (trackingData) + { + this.dispatchEvent(displayObject, 'pointerupoutside', interactionEvent); + if (isTouch) this.dispatchEvent(displayObject, 'touchendoutside', interactionEvent); + } + // Only remove the tracking data if there is no over/down state still associated with it + if (trackingData && trackingData.none) + { + delete displayObject.trackedPointers[id]; } } @@ -1202,39 +1161,78 @@ * 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 = null; + } - 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') + { + this.setCursorMode(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 (isMouse) { - 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 +1240,94 @@ * 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.setCursorMode(null); + } - 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'); + + let trackingData = displayObject.trackedPointers[id]; + + // if we just moused over the display object, then we need to track that state + if (hit && !trackingData) + { + trackingData = displayObject.trackedPointers[id] = new InteractionTrackingData(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); + } + } + + // only change the cursor if it has not already been changed (by something deeper in the + // display tree) + if (isMouse && this.cursor === null) + { + this.cursor = displayObject.cursor; } } - 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); + } + // if there is no mouse down information for the pointer, then it is safe to delete + if (trackingData.none) + { + delete displayObject.trackedPointers[id]; + } } } @@ -1287,253 +1335,154 @@ * 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 && pointerEvent.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; + + // mark the touch as normalized, just so that we know we did it + touch.isNormalized = true; + + normalizedEvents.push(touch); + } } - else if (this.normalizeMouseEvents) + // apparently PointerEvent subclasses MouseEvent, so yay + else if (event instanceof MouseEvent && (!this.supportsPointerEvents || !(event instanceof window.PointerEvent))) { if (typeof event.isPrimary === 'undefined') event.isPrimary = true; if (typeof event.width === 'undefined') event.width = 1; @@ -1541,10 +1490,21 @@ 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; + + // mark the mouse event as normalized, just so that we know we did it + event.isNormalized = true; + + normalizedEvents.push(event); } + else + { + normalizedEvents.push(event); + } + + return normalizedEvents; } /** @@ -1563,30 +1523,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 +1542,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..6a1f69e --- /dev/null +++ b/src/interaction/InteractionTrackingData.js @@ -0,0 +1,147 @@ +/** + * 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 inactive (not over or down)? + * + * @member {number} + * @memberof PIXI.interaction.InteractionTrackingData# + */ + get none() + { + return this._flags === this.constructor.FLAGS.NONE; + } + + /** + * 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..d252fba 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 * ); */ @@ -38,68 +38,56 @@ hitArea: null, /** - * If enabled, the mouse cursor will change when hovered over the displayObject if it is interactive + * If enabled, the mouse cursor use the pointer behavior when hovered over the displayObject if it is interactive + * Setting this changes the 'cursor' property to `'pointer'`. * - * @inner {boolean} + * @member {boolean} + * @memberof PIXI.interaction.interactiveTarget# */ - buttonMode: false, + get buttonMode() + { + return this.cursor === 'pointer'; + }, + set buttonMode(value) + { + if (value) + { + this.cursor = 'pointer'; + } + else if (this.cursor === 'pointer') + { + this.cursor = null; + } + }, /** - * If buttonMode is enabled, this defines what CSS cursor property is used when the mouse cursor - * is hovered over the displayObject + * This defines what cursor mode is used when the mouse cursor + * is hovered over the displayObject. * * @see https://developer.mozilla.org/en/docs/Web/CSS/cursor * * @inner {string} */ - defaultCursor: 'pointer', - - // some internal checks.. - /** - * Internal check to detect if the mouse cursor is hovered over the displayObject - * - * @inner {boolean} - * @private - */ - _over: false, + cursor: null, /** - * Internal check to detect if the left mouse button is pressed on the displayObject + * Internal set of all active pointers, by identifier * - * @inner {boolean} + * @member {Map} + * @memberof PIXI.interaction.interactiveTarget# * @private */ - _isLeftDown: false, + get trackedPointers() + { + if (this._trackedPointers === undefined) this._trackedPointers = {}; + + return this._trackedPointers; + }, /** - * Internal check to detect if the right mouse button is pressed on the displayObject + * Map of all tracked pointers, by identifier. Use trackedPointers to access. * - * @inner {boolean} - * @private + * @private {Map} */ - _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, + _trackedPointers: undefined, }; diff --git a/src/loaders/bitmapFontParser.js b/src/loaders/bitmapFontParser.js index cc2618c..f4cf996 100644 --- a/src/loaders/bitmapFontParser.js +++ b/src/loaders/bitmapFontParser.js @@ -1,63 +1,19 @@ import * as path from 'path'; -import { Rectangle, Texture, utils } from '../core'; +import { utils } from '../core'; import { Resource } from 'resource-loader'; import { BitmapText } from '../extras'; +/** + * Register a BitmapText font from loader resource. + * + * @function parseBitmapFontData + * @memberof PIXI.loaders + * @param {PIXI.loaders.Resource} resource - Loader resource. + * @param {PIXI.Texture} texture - Reference to texture. + */ export function parse(resource, texture) { - const data = {}; - const info = resource.data.getElementsByTagName('info')[0]; - const common = resource.data.getElementsByTagName('common')[0]; - - data.font = info.getAttribute('face'); - data.size = parseInt(info.getAttribute('size'), 10); - data.lineHeight = parseInt(common.getAttribute('lineHeight'), 10); - data.chars = {}; - - // parse letters - const letters = resource.data.getElementsByTagName('char'); - - for (let i = 0; i < letters.length; i++) - { - const charCode = parseInt(letters[i].getAttribute('id'), 10); - - const textureRect = new Rectangle( - parseInt(letters[i].getAttribute('x'), 10) + texture.frame.x, - parseInt(letters[i].getAttribute('y'), 10) + texture.frame.y, - parseInt(letters[i].getAttribute('width'), 10), - parseInt(letters[i].getAttribute('height'), 10) - ); - - data.chars[charCode] = { - xOffset: parseInt(letters[i].getAttribute('xoffset'), 10), - yOffset: parseInt(letters[i].getAttribute('yoffset'), 10), - xAdvance: parseInt(letters[i].getAttribute('xadvance'), 10), - kerning: {}, - texture: new Texture(texture.baseTexture, textureRect), - - }; - } - - // parse kernings - const kernings = resource.data.getElementsByTagName('kerning'); - - for (let i = 0; i < kernings.length; i++) - { - const first = parseInt(kernings[i].getAttribute('first'), 10); - const second = parseInt(kernings[i].getAttribute('second'), 10); - const amount = parseInt(kernings[i].getAttribute('amount'), 10); - - if (data.chars[second]) - { - data.chars[second].kerning[first] = amount; - } - } - - resource.bitmapFont = data; - - // I'm leaving this as a temporary fix so we can test the bitmap fonts in v3 - // but it's very likely to change - BitmapText.fonts[data.font] = data; + resource.bitmapFont = BitmapText.registerFont(resource.data, texture); } export default function () diff --git a/src/loaders/index.js b/src/loaders/index.js index 8942436..5189793 100644 --- a/src/loaders/index.js +++ b/src/loaders/index.js @@ -5,4 +5,11 @@ export { default as bitmapFontParser, parse as parseBitmapFontData } from './bitmapFontParser'; export { default as spritesheetParser } from './spritesheetParser'; export { default as textureParser } from './textureParser'; + +/** + * Reference to **resource-loader**'s Resource class. + * See https://github.com/englercj/resource-loader + * @class Resource + * @memberof PIXI.loaders + */ export { Resource } from 'resource-loader'; diff --git a/package.json b/package.json index 6a366ba..71ade15 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pixi.js", - "version": "4.3.4", + "version": "4.3.5", "description": "Pixi.js is a fast lightweight 2D library that works across all devices.", "author": "Mat Groves", "contributors": [ @@ -61,7 +61,7 @@ "ismobilejs": "^0.4.0", "object-assign": "^4.0.1", "pixi-gl-core": "^1.0.3", - "resource-loader": "^2.0.4" + "resource-loader": "^2.0.6" }, "devDependencies": { "babel-cli": "^6.18.0", @@ -70,9 +70,9 @@ "babel-preset-es2015": "^6.14.0", "babelify": "^7.3.0", "del": "^2.2.0", - "electron-prebuilt": "^1.3.2", + "electron": "^1.4.15", "eslint": "^3.5.0", - "floss": "^1.2.0", + "floss": "^2.0.1", "gh-pages": "^0.11.0", "jaguarjs-jsdoc": "^1.0.1", "js-md5": "^0.4.1", diff --git a/src/accessibility/AccessibilityManager.js b/src/accessibility/AccessibilityManager.js index df2716a..f16bcb1 100644 --- a/src/accessibility/AccessibilityManager.js +++ b/src/accessibility/AccessibilityManager.js @@ -3,7 +3,7 @@ import accessibleTarget from './accessibleTarget'; // add some extra variables to the container.. -Object.assign( +core.utils.mixins.delayMixin( core.DisplayObject.prototype, accessibleTarget ); diff --git a/src/core/Application.js b/src/core/Application.js index ea0f058..5340578 100644 --- a/src/core/Application.js +++ b/src/core/Application.js @@ -1,6 +1,6 @@ import { autoDetectRenderer } from './autoDetectRenderer'; import Container from './display/Container'; -import Ticker from './ticker/Ticker'; +import { shared, Ticker } from './ticker'; /** * Convenience class to create a new PIXI application. @@ -35,9 +35,9 @@ * @param {boolean} [noWebGL=false] - prevents selection of WebGL renderer, even if such is present * @param {boolean} [options.legacy=false] - If true Pixi will aim to ensure compatibility * with older / less advanced devices. If you experience unexplained flickering try setting this to true. - + * @param {boolean} [useSharedTicker=false] - `true` to use PIXI.ticker.shared, `false` to create new ticker. */ - constructor(width, height, options, noWebGL) + constructor(width, height, options, noWebGL, useSharedTicker = false) { /** * WebGL renderer if available, otherwise CanvasRenderer @@ -46,23 +46,46 @@ this.renderer = autoDetectRenderer(width, height, options, noWebGL); /** - * The root display container that's renderered. + * The root display container that's rendered. * @member {PIXI.Container} */ this.stage = new Container(); /** + * Internal reference to the ticker + * @member {PIXI.ticker.Ticker} + * @private + */ + this._ticker = null; + + /** * Ticker for doing render updates. * @member {PIXI.ticker.Ticker} + * @default PIXI.ticker.shared */ - this.ticker = new Ticker(); - - this.ticker.add(this.render, this); + this.ticker = useSharedTicker ? shared : new Ticker(); // Start the rendering this.start(); } + set ticker(ticker) // eslint-disable-line require-jsdoc + { + if (this._ticker) + { + this._ticker.remove(this.render, this); + } + this._ticker = ticker; + if (ticker) + { + ticker.add(this.render, this); + } + } + get ticker() // eslint-disable-line require-jsdoc + { + return this._ticker; + } + /** * Render the current stage. */ @@ -76,7 +99,7 @@ */ stop() { - this.ticker.stop(); + this._ticker.stop(); } /** @@ -84,7 +107,7 @@ */ start() { - this.ticker.start(); + this._ticker.start(); } /** @@ -98,13 +121,22 @@ } /** + * Reference to the renderer's screen rectangle. Its safe to use as filterArea or hitArea for whole screen + * @member {PIXI.Rectangle} + * @readonly + */ + get screen() + { + return this.renderer.screen; + } + + /** * Destroy and don't use after this. * @param {Boolean} [removeView=false] Automatically remove canvas from DOM. */ destroy(removeView) { this.stop(); - this.ticker.remove(this.render, this); this.ticker = null; this.stage.destroy(); diff --git a/src/core/Shader.js b/src/core/Shader.js new file mode 100644 index 0000000..78b17e6 --- /dev/null +++ b/src/core/Shader.js @@ -0,0 +1,46 @@ +import { GLShader } from 'pixi-gl-core'; +import settings from './settings'; + +function checkPrecision(src, def) +{ + if (src instanceof Array) + { + if (src[0].substring(0, 9) !== 'precision') + { + const copy = src.slice(0); + + copy.unshift(`precision ${def} float;`); + + return copy; + } + } + else if (src.substring(0, 9) !== 'precision') + { + return `precision ${def} float;\n${src}`; + } + + return src; +} + +/** + * Wrapper class, webGL Shader for Pixi. + * Adds precision string if vertexSrc or fragmentSrc have no mention of it. + * + * @class + * @extends GLShader + * @memberof PIXI + */ +export default class Shader extends GLShader +{ + /** + * + * @param {WebGLRenderingContext} gl - The current WebGL rendering context + * @param {string|string[]} vertexSrc - The vertex shader source as an array of strings. + * @param {string|string[]} fragmentSrc - The fragment shader source as an array of strings. + */ + constructor(gl, vertexSrc, fragmentSrc) + { + super(gl, checkPrecision(vertexSrc, settings.PRECISION_VERTEX), + checkPrecision(fragmentSrc, settings.PRECISION_FRAGMENT)); + } +} diff --git a/src/core/const.js b/src/core/const.js index ef6f662..3de0efe 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -188,7 +188,7 @@ * The gc modes that are supported by pixi. * * The {@link PIXI.settings.GC_MODE} Garbage Collection mode for pixi textures is AUTO - * If set to GC_MODE, the renderer will occasianally check textures usage. If they are not + * If set to GC_MODE, the renderer will occasionally check textures usage. If they are not * used for a specified period of time they will be removed from the GPU. They will of course * be uploaded again when they are required. This is a silent behind the scenes process that * should ensure that the GPU does not get filled up. diff --git a/src/core/display/Container.js b/src/core/display/Container.js index ed8cd5f..17fde8d 100644 --- a/src/core/display/Container.js +++ b/src/core/display/Container.js @@ -73,11 +73,12 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.push(child); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -108,11 +109,12 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.splice(index, 0, child); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -225,10 +227,11 @@ if (index === -1) return null; child.parent = null; + // ensure child transform will be recalculated + child.transform._parentID = -1; removeItems(this.children, index, 1); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -249,11 +252,12 @@ { const child = this.getChildAt(index); + // ensure child transform will be recalculated.. child.parent = null; + child.transform._parentID = -1; removeItems(this.children, index, 1); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -284,13 +288,12 @@ for (let i = 0; i < removed.length; ++i) { removed[i].parent = null; + if (removed[i].transform) + { + removed[i].transform._parentID = -1; + } } - // ensure a transform will be recalculated.. - if (this.transform) - { - this.transform._parentID = -1; - } this._boundsID++; this.onChildrenChange(beginIndex); diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index f37f2f5..be4c4fb 100644 --- a/src/core/graphics/Graphics.js +++ b/src/core/graphics/Graphics.js @@ -28,8 +28,9 @@ { /** * + * @param {boolean} [nativeLines=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP */ - constructor() + constructor(nativeLines = false) { super(); @@ -50,6 +51,13 @@ this.lineWidth = 0; /** + * If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * + * @member {boolean} + */ + this.nativeLines = nativeLines; + + /** * The color of any lines drawn. * * @member {string} @@ -757,28 +765,14 @@ if (!this._spriteRect) { - if (!Graphics._SPRITE_TEXTURE) - { - Graphics._SPRITE_TEXTURE = RenderTexture.create(10, 10); - - const canvas = document.createElement('canvas'); - - canvas.width = 10; - canvas.height = 10; - - const context = canvas.getContext('2d'); - - context.fillStyle = 'white'; - context.fillRect(0, 0, 10, 10); - - Graphics._SPRITE_TEXTURE = Texture.fromCanvas(canvas); - } - - this._spriteRect = new Sprite(Graphics._SPRITE_TEXTURE); + this._spriteRect = new Sprite(new Texture(Texture.WHITE)); } + + const sprite = this._spriteRect; + if (this.tint === 0xffffff) { - this._spriteRect.tint = this.graphicsData[0].fillColor; + sprite.tint = this.graphicsData[0].fillColor; } else { @@ -792,20 +786,21 @@ t1[1] *= t2[1]; t1[2] *= t2[2]; - this._spriteRect.tint = rgb2hex(t1); + sprite.tint = rgb2hex(t1); } - this._spriteRect.alpha = this.graphicsData[0].fillAlpha; - this._spriteRect.worldAlpha = this.worldAlpha * this._spriteRect.alpha; + sprite.alpha = this.graphicsData[0].fillAlpha; + sprite.worldAlpha = this.worldAlpha * sprite.alpha; + sprite.blendMode = this.blendMode; - Graphics._SPRITE_TEXTURE._frame.width = rect.width; - Graphics._SPRITE_TEXTURE._frame.height = rect.height; + sprite.texture._frame.width = rect.width; + sprite.texture._frame.height = rect.height; - this._spriteRect.transform.worldTransform = this.transform.worldTransform; + sprite.transform.worldTransform = this.transform.worldTransform; - this._spriteRect.anchor.set(-rect.x / rect.width, -rect.y / rect.height); - this._spriteRect._onAnchorUpdate(); + sprite.anchor.set(-rect.x / rect.width, -rect.y / rect.height); + sprite._onAnchorUpdate(); - this._spriteRect._renderWebGL(renderer); + sprite._renderWebGL(renderer); } /** @@ -1031,6 +1026,7 @@ this.fillColor, this.fillAlpha, this.filling, + this.nativeLines, shape ); diff --git a/src/core/graphics/GraphicsData.js b/src/core/graphics/GraphicsData.js index f5a87ae..b6f03c0 100644 --- a/src/core/graphics/GraphicsData.js +++ b/src/core/graphics/GraphicsData.js @@ -14,14 +14,19 @@ * @param {number} fillColor - the color of the fill * @param {number} fillAlpha - the alpha of the fill * @param {boolean} fill - whether or not the shape is filled with a colour + * @param {boolean} nativeLines - the method for drawing lines * @param {PIXI.Circle|PIXI.Rectangle|PIXI.Ellipse|PIXI.Polygon} shape - The shape object to draw. */ - constructor(lineWidth, lineColor, lineAlpha, fillColor, fillAlpha, fill, shape) + constructor(lineWidth, lineColor, lineAlpha, fillColor, fillAlpha, fill, nativeLines, shape) { /** * @member {number} the width of the line to draw */ this.lineWidth = lineWidth; + /** + * @member {boolean} if true the liens will be draw using LINES instead of TRIANGLE_STRIP + */ + this.nativeLines = nativeLines; /** * @member {number} the color of the line to draw @@ -85,6 +90,7 @@ this.fillColor, this.fillAlpha, this.fill, + this.nativeLines, this.shape ); } diff --git a/src/core/graphics/webgl/GraphicsRenderer.js b/src/core/graphics/webgl/GraphicsRenderer.js index 14083fe..67f5e92 100644 --- a/src/core/graphics/webgl/GraphicsRenderer.js +++ b/src/core/graphics/webgl/GraphicsRenderer.js @@ -102,7 +102,15 @@ shaderTemp.uniforms.alpha = graphics.worldAlpha; renderer.bindVao(webGLData.vao); - webGLData.vao.draw(gl.TRIANGLE_STRIP, webGLData.indices.length); + + if (graphics.nativeLines) + { + gl.drawArrays(gl.LINES, 0, webGLData.points.length / 6); + } + else + { + webGLData.vao.draw(gl.TRIANGLE_STRIP, webGLData.indices.length); + } } } diff --git a/src/core/graphics/webgl/utils/buildLine.js b/src/core/graphics/webgl/utils/buildLine.js index ecf43f7..4d6862e 100644 --- a/src/core/graphics/webgl/utils/buildLine.js +++ b/src/core/graphics/webgl/utils/buildLine.js @@ -11,7 +11,29 @@ * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties * @param {object} webGLData - an object containing all the webGL-specific information to create this shape */ -export default function buildLine(graphicsData, webGLData) +export default function (graphicsData, webGLData) +{ + if (graphicsData.nativeLines) + { + buildNativeLine(graphicsData, webGLData); + } + else + { + buildLine(graphicsData, webGLData); + } +} + +/** + * Builds a line to draw using the poligon method. + * + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @private + * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties + * @param {object} webGLData - an object containing all the webGL-specific information to create this shape + */ +function buildLine(graphicsData, webGLData) { // TODO OPTIMISE! let points = graphicsData.points; @@ -224,3 +246,46 @@ indices.push(indexStart - 1); } + +/** + * Builds a line to draw using the gl.drawArrays(gl.LINES) method + * + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @private + * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties + * @param {object} webGLData - an object containing all the webGL-specific information to create this shape + */ +function buildNativeLine(graphicsData, webGLData) +{ + let i = 0; + const points = graphicsData.points; + + if (points.length === 0) return; + + const verts = webGLData.points; + const length = points.length / 2; + + // sort color + const color = hex2rgb(graphicsData.lineColor); + const alpha = graphicsData.lineAlpha; + const r = color[0] * alpha; + const g = color[1] * alpha; + const b = color[2] * alpha; + + for (i = 1; i < length; i++) + { + const p1x = points[(i - 1) * 2]; + const p1y = points[((i - 1) * 2) + 1]; + + const p2x = points[i * 2]; + const p2y = points[(i * 2) + 1]; + + verts.push(p1x, p1y); + verts.push(r, g, b, alpha); + + verts.push(p2x, p2y); + verts.push(r, g, b, alpha); + } +} diff --git a/src/core/graphics/webgl/utils/buildRoundedRectangle.js b/src/core/graphics/webgl/utils/buildRoundedRectangle.js index 9abc807..a6c8461 100644 --- a/src/core/graphics/webgl/utils/buildRoundedRectangle.js +++ b/src/core/graphics/webgl/utils/buildRoundedRectangle.js @@ -77,6 +77,26 @@ } /** + * Calculate a single point for a quadratic bezier curve. + * Utility function used by quadraticBezierCurve. + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @private + * @param {number} n1 - first number + * @param {number} n2 - second number + * @param {number} perc - percentage + * @return {number} the result + * + */ +function getPt(n1, n2, perc) +{ + const diff = n2 - n1; + + return n1 + (diff * perc); +} + +/** * Calculate the points for a quadratic bezier curve. (helper function..) * Based on: https://stackoverflow.com/questions/785097/how-do-i-implement-a-bezier-curve-in-c * @@ -105,13 +125,6 @@ let x = 0; let y = 0; - function getPt(n1, n2, perc) - { - const diff = n2 - n1; - - return n1 + (diff * perc); - } - for (let i = 0, j = 0; i <= n; ++i) { j = i / n; diff --git a/src/core/index.js b/src/core/index.js index f7a817c..4f414e8 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -30,6 +30,7 @@ export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; export { default as CanvasGraphicsRenderer } from './graphics/canvas/CanvasGraphicsRenderer'; +export { default as Spritesheet } from './textures/Spritesheet'; export { default as Texture } from './textures/Texture'; export { default as BaseTexture } from './textures/BaseTexture'; export { default as RenderTexture } from './textures/RenderTexture'; diff --git a/src/core/math/Matrix.js b/src/core/math/Matrix.js index 0b77477..3679860 100644 --- a/src/core/math/Matrix.js +++ b/src/core/math/Matrix.js @@ -13,45 +13,50 @@ export default class Matrix { /** - * + * @param {number} [a=1] - x scale + * @param {number} [b=0] - y skew + * @param {number} [c=0] - x skew + * @param {number} [d=1] - y scale + * @param {number} [tx=0] - x translation + * @param {number} [ty=0] - y translation */ - constructor() + constructor(a = 1, b = 0, c = 0, d = 1, tx = 0, ty = 0) { /** * @member {number} * @default 1 */ - this.a = 1; + this.a = a; /** * @member {number} * @default 0 */ - this.b = 0; + this.b = b; /** * @member {number} * @default 0 */ - this.c = 0; + this.c = c; /** * @member {number} * @default 1 */ - this.d = 1; + this.d = d; /** * @member {number} * @default 0 */ - this.tx = 0; + this.tx = tx; /** * @member {number} * @default 0 */ - this.ty = 0; + this.ty = ty; this.array = null; } diff --git a/src/core/renderers/SystemRenderer.js b/src/core/renderers/SystemRenderer.js index fa116a6..a24c775 100644 --- a/src/core/renderers/SystemRenderer.js +++ b/src/core/renderers/SystemRenderer.js @@ -1,5 +1,5 @@ import { sayHello, hex2string, hex2rgb } from '../utils'; -import { Matrix } from '../math'; +import { Matrix, Rectangle } from '../math'; import { RENDERER_TYPE } from '../const'; import settings from '../settings'; import Container from '../display/Container'; @@ -21,8 +21,8 @@ { /** * @param {string} system - The name of the system this renderer is for. - * @param {number} [width=800] - the width of the canvas view - * @param {number} [height=600] - the height of the canvas view + * @param {number} [screenWidth=800] - the width of the screen + * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false @@ -37,7 +37,7 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(system, width, height, options) + constructor(system, screenWidth, screenHeight, options) { super(); @@ -69,20 +69,13 @@ this.type = RENDERER_TYPE.UNKNOWN; /** - * The width of the canvas view + * Measurements of the screen. (0, 0, screenWidth, screenHeight) * - * @member {number} - * @default 800 - */ - this.width = width || 800; - - /** - * The height of the canvas view + * Its safe to use as filterArea or hitArea for whole stage * - * @member {number} - * @default 600 + * @member {PIXI.Rectangle} */ - this.height = height || 600; + this.screen = new Rectangle(0, 0, screenWidth || 800, screenHeight || 600); /** * The canvas element that everything is drawn to @@ -107,7 +100,7 @@ this.transparent = options.transparent; /** - * Whether the render view should be resized automatically + * Whether css dimensions of canvas view should be resized to screen dimensions automatically * * @member {boolean} */ @@ -192,23 +185,48 @@ } /** - * Resizes the canvas view to the specified width and height + * Same as view.width, actual number of pixels in the canvas by horizontal * - * @param {number} width - the new width of the canvas view - * @param {number} height - the new height of the canvas view + * @member {number} + * @readonly + * @default 800 */ - resize(width, height) + get width() { - this.width = width * this.resolution; - this.height = height * this.resolution; + return this.view.width; + } - this.view.width = this.width; - this.view.height = this.height; + /** + * Same as view.height, actual number of pixels in the canvas by vertical + * + * @member {number} + * @readonly + * @default 600 + */ + get height() + { + return this.view.height; + } + + /** + * Resizes the screen and canvas to the specified width and height + * Canvas dimensions are multiplied by resolution + * + * @param {number} screenWidth - the new width of the screen + * @param {number} screenHeight - the new height of the screen + */ + resize(screenWidth, screenHeight) + { + this.screen.width = screenWidth; + this.screen.height = screenHeight; + + this.view.width = screenWidth * this.resolution; + this.view.height = screenHeight * this.resolution; if (this.autoResize) { - this.view.style.width = `${this.width / this.resolution}px`; - this.view.style.height = `${this.height / this.resolution}px`; + this.view.style.width = `${screenWidth}px`; + this.view.style.height = `${screenHeight}px`; } } @@ -249,11 +267,10 @@ this.type = RENDERER_TYPE.UNKNOWN; - this.width = 0; - this.height = 0; - this.view = null; + this.screen = null; + this.resolution = 0; this.transparent = false; diff --git a/src/core/renderers/canvas/CanvasRenderer.js b/src/core/renderers/canvas/CanvasRenderer.js index 1bd2d5e..e84e49c 100644 --- a/src/core/renderers/canvas/CanvasRenderer.js +++ b/src/core/renderers/canvas/CanvasRenderer.js @@ -18,8 +18,8 @@ export default class CanvasRenderer extends SystemRenderer { /** - * @param {number} [width=800] - the width of the canvas view - * @param {number} [height=600] - the height of the canvas view + * @param {number} [screenWidth=800] - the width of the screen + * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false @@ -34,9 +34,9 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(width, height, options = {}) + constructor(screenWidth, screenHeight, options = {}) { - super('Canvas', width, height, options); + super('Canvas', screenWidth, screenHeight, options); this.type = RENDERER_TYPE.CANVAS; @@ -96,7 +96,7 @@ this.context = null; this.renderingToScreen = false; - this.resize(width, height); + this.resize(screenWidth, screenHeight); } /** @@ -283,12 +283,12 @@ * * @extends PIXI.SystemRenderer#resize * - * @param {number} width - The new width of the canvas view - * @param {number} height - The new height of the canvas view + * @param {number} screenWidth - the new width of the screen + * @param {number} screenHeight - the new height of the screen */ - resize(width, height) + resize(screenWidth, screenHeight) { - super.resize(width, height); + super.resize(screenWidth, screenHeight); // reset the scale mode.. oddly this seems to be reset when the canvas is resized. // surely a browser bug?? Let pixi fix that for you.. diff --git a/src/core/renderers/webgl/TextureManager.js b/src/core/renderers/webgl/TextureManager.js index b685c1a..8227289 100644 --- a/src/core/renderers/webgl/TextureManager.js +++ b/src/core/renderers/webgl/TextureManager.js @@ -61,7 +61,7 @@ * Updates and/or Creates a WebGL texture for the renderer's context. * * @param {PIXI.BaseTexture|PIXI.Texture} texture - the texture to update - * @param {Number} location - the location the texture will be bound to. + * @param {number} location - the location the texture will be bound to. * @return {GLTexture} The gl texture. */ updateTexture(texture, location) diff --git a/src/core/renderers/webgl/WebGLRenderer.js b/src/core/renderers/webgl/WebGLRenderer.js index e4cd8a0..830b382 100644 --- a/src/core/renderers/webgl/WebGLRenderer.js +++ b/src/core/renderers/webgl/WebGLRenderer.js @@ -31,8 +31,8 @@ { /** * - * @param {number} [width=0] - the width of the canvas view - * @param {number} [height=0] - the height of the canvas view + * @param {number} [screenWidth=800] - the width of the screen + * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false @@ -53,9 +53,9 @@ * @param {boolean} [options.legacy=false] - If true Pixi will aim to ensure compatibility * with older / less advanced devices. If you experiance unexplained flickering try setting this to true. */ - constructor(width, height, options = {}) + constructor(screenWidth, screenHeight, options = {}) { - super('WebGL', width, height, options); + super('WebGL', screenWidth, screenHeight, options); this.legacy = !!options.legacy; @@ -245,7 +245,7 @@ glCore._testingContext = gl; // setup the width/height properties and gl viewport - this.resize(this.width, this.height); + this.resize(this.screen.width, this.screen.height); } /** @@ -343,16 +343,16 @@ /** * Resizes the webGL view to the specified width and height. * - * @param {number} width - the new width of the webGL view - * @param {number} height - the new height of the webGL view + * @param {number} screenWidth - the new width of the screen + * @param {number} screenHeight - the new height of the screen */ - resize(width, height) + resize(screenWidth, screenHeight) { // if(width * this.resolution === this.width && height * this.resolution === this.height)return; - SystemRenderer.prototype.resize.call(this, width, height); + SystemRenderer.prototype.resize.call(this, screenWidth, screenHeight); - this.rootRenderTarget.resize(width, height); + this.rootRenderTarget.resize(screenWidth, screenHeight); if (this._activeRenderTarget === this.rootRenderTarget) { @@ -396,6 +396,26 @@ } /** + * Erases the render texture and fills the drawing area with a colour + * + * @param {PIXI.RenderTexture} renderTexture - The render texture to clear + * @param {number} [clearColor] - The colour + * @return {PIXI.WebGLRenderer} Returns itself. + */ + clearRenderTexture(renderTexture, clearColor) + { + const baseTexture = renderTexture.baseTexture; + const renderTarget = baseTexture._glRenderTargets[this.CONTEXT_UID]; + + if (renderTarget) + { + renderTarget.clear(clearColor); + } + + return this; + } + + /** * Binds a render texture for rendering * * @param {PIXI.RenderTexture} renderTexture - The render texture to render @@ -474,9 +494,10 @@ * Changes the current GLShader to the one given in parameter * * @param {PIXI.glCore.Shader} shader - the new glShader + * @param {boolean} [autoProject=true] - Whether automatically set the projection matrix * @return {PIXI.WebGLRenderer} Returns itself. */ - _bindGLShader(shader) + _bindGLShader(shader, autoProject) { // TODO cache if (this._activeShader !== shader) @@ -484,8 +505,14 @@ this._activeShader = shader; shader.bind(); - // automatically set the projection matrix - shader.uniforms.projectionMatrix = this._activeRenderTarget.projectionMatrix.toArray(true); + // `autoProject` normally would be a default parameter set to true + // but because of how Babel transpiles default parameters + // it hinders the performance of this method. + if (autoProject !== false) + { + // automatically set the projection matrix + shader.uniforms.projectionMatrix = this._activeRenderTarget.projectionMatrix.toArray(true); + } } return this; diff --git a/src/core/renderers/webgl/WebGLState.js b/src/core/renderers/webgl/WebGLState.js index ba6ecc5..6c846d5 100755 --- a/src/core/renderers/webgl/WebGLState.js +++ b/src/core/renderers/webgl/WebGLState.js @@ -82,18 +82,20 @@ push() { // next state.. - let state = this.stack[++this.stackIndex]; + let state = this.stack[this.stackIndex]; if (!state) { state = this.stack[this.stackIndex] = new Uint8Array(16); } + ++this.stackIndex; + // copy state.. // set active state so we can force overrides of gl state for (let i = 0; i < this.activeState.length; i++) { - this.activeState[i] = state[i]; + state[i] = this.activeState[i]; } } diff --git a/src/core/renderers/webgl/filters/Filter.js b/src/core/renderers/webgl/filters/Filter.js index 2c3eaad..4eb6128 100644 --- a/src/core/renderers/webgl/filters/Filter.js +++ b/src/core/renderers/webgl/filters/Filter.js @@ -9,7 +9,7 @@ * @memberof PIXI * @extends PIXI.Shader */ -class Filter extends Shader +export default class Filter extends Shader { /** * @param {string} [vertexSrc] - The source of the vertex shader. @@ -56,15 +56,16 @@ * @param {PIXI.RenderTarget} input - The input render target. * @param {PIXI.RenderTarget} output - The target to output to. * @param {boolean} clear - Should the output be cleared before rendering to it + * @param {object} [currentState] - It's current state of filter. + * There are some useful properties in the currentState : + * target, filters, sourceFrame, destinationFrame, renderTarget, resolution */ - apply(filterManager, input, output, clear) + apply(filterManager, input, output, clear, currentState) // eslint-disable-line no-unused-vars { // do as you please! - filterManager.applyFilter(this, input, output, clear); + filterManager.applyFilter(this, input, output, clear, currentState); // or just do a regular render.. } } - -export default Filter; diff --git a/src/core/renderers/webgl/filters/extractUniformsFromSrc.js b/src/core/renderers/webgl/filters/extractUniformsFromSrc.js index d78a0b1..4893214 100644 --- a/src/core/renderers/webgl/filters/extractUniformsFromSrc.js +++ b/src/core/renderers/webgl/filters/extractUniformsFromSrc.js @@ -12,7 +12,7 @@ function extractUniformsFromString(string) { - const maskRegex = new RegExp('^(projectionMatrix|uSampler|filterArea)$'); + const maskRegex = new RegExp('^(projectionMatrix|uSampler|filterArea|filterClamp)$'); const uniforms = {}; let nameSplit; diff --git a/src/core/renderers/webgl/managers/FilterManager.js b/src/core/renderers/webgl/managers/FilterManager.js index 43fc869..6c7422d 100644 --- a/src/core/renderers/webgl/managers/FilterManager.js +++ b/src/core/renderers/webgl/managers/FilterManager.js @@ -2,6 +2,7 @@ import RenderTarget from '../utils/RenderTarget'; import Quad from '../utils/Quad'; import { Rectangle } from '../../../math'; +import Shader from '../../../Shader'; import * as filterTransforms from '../filters/filterTransforms'; import bitTwiddle from 'bit-twiddle'; @@ -22,8 +23,6 @@ this.filters = []; this.target = null; this.resolution = 1; - - this.firstRun = true; } } @@ -155,7 +154,7 @@ if (filters.length === 1) { - filters[0].apply(this, currentState.renderTarget, lastState.renderTarget, false); + filters[0].apply(this, currentState.renderTarget, lastState.renderTarget, false, currentState); this.freePotRenderTarget(currentState.renderTarget); } else @@ -177,7 +176,7 @@ for (i = 0; i < filters.length - 1; ++i) { - filters[i].apply(this, flip, flop, true); + filters[i].apply(this, flip, flop, true, currentState); const t = flip; @@ -185,7 +184,7 @@ flop = t; } - filters[i].apply(this, flip, lastState.renderTarget, false); + filters[i].apply(this, flip, lastState.renderTarget, false, currentState); this.freePotRenderTarget(flip); this.freePotRenderTarget(flop); @@ -212,16 +211,28 @@ const renderer = this.renderer; const gl = renderer.gl; - renderer.bindShader(filter, true); + let shader = filter.glShaders[renderer.CONTEXT_UID]; - // TODO theres a better way! - // quad can be a mesh and we then should be able to pull this off without accessing the shader directly - const shader = renderer.shaderManager.getGLShader(); - - // TODO change quad to mesh.. - if (!this.firstRun) + // cacheing.. + if (!shader) { - this.firstRun = true; + if (filter.glShaderKey) + { + shader = this.shaderCache[filter.glShaderKey]; + + if (!shader) + { + shader = new Shader(this.gl, filter.vertexSrc, filter.fragmentSrc); + + filter.glShaders[renderer.CONTEXT_UID] = this.shaderCache[filter.glShaderKey] = shader; + } + } + else + { + shader = filter.glShaders[renderer.CONTEXT_UID] = new Shader(this.gl, filter.vertexSrc, filter.fragmentSrc); + } + + // TODO - this only needs to be done once? renderer.bindVao(null); this.quad.initVao(shader); @@ -244,58 +255,146 @@ renderer.maskManager.pushScissorMask(null, renderer.maskManager.scissorData); } - // renderer._bindGLShader(shader); + renderer.bindShader(shader); - let currentState; - const uniforms = filter.uniforms; + // free unit 0 for us, doesn't matter what was there + // don't try to restore it, because syncUniforms can upload it to another slot + // and it'll be a problem + const tex = this.renderer.emptyTextures[0]; - if (uniforms.filterArea) - { - currentState = this.filterData.stack[this.filterData.index]; - - const filterArea = uniforms.filterArea; - - filterArea[0] = currentState.renderTarget.size.width; - filterArea[1] = currentState.renderTarget.size.height; - filterArea[2] = currentState.sourceFrame.x; - filterArea[3] = currentState.sourceFrame.y; - - filter.uniforms.filterArea = filterArea; - } - - // use this to clamp displaced texture coords so they belong to filterArea - // see displacementFilter fragment shader for an example - if (uniforms.filterClamp) - { - currentState = this.filterData.stack[this.filterData.index]; - - const filterClamp = uniforms.filterClamp; - - filterClamp[0] = 0; - filterClamp[1] = 0; - filterClamp[2] = (currentState.sourceFrame.width - 1) / currentState.renderTarget.size.width; - filterClamp[3] = (currentState.sourceFrame.height - 1) / currentState.renderTarget.size.height; - - filter.uniforms.filterClamp = filterClamp; - } - - renderer.shaderManager.setUniforms(filter.uniforms); + this.renderer.boundTextures[0] = tex; + // this syncs the pixi filters uniforms with glsl uniforms + this.syncUniforms(shader, filter); renderer.state.setBlendMode(filter.blendMode); - // temporary bypass cache.. - const tex = this.renderer.boundTextures[0]; - gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, input.texture.texture); this.quad.vao.draw(this.renderer.gl.TRIANGLES, 6, 0); - // restore cache. gl.bindTexture(gl.TEXTURE_2D, tex._glTextures[this.renderer.CONTEXT_UID].texture); } /** + * Uploads the uniforms of the filter. + * + * @param {GLShader} shader - The underlying gl shader. + * @param {PIXI.Filter} filter - The filter we are synchronizing. + */ + syncUniforms(shader, filter) + { + const uniformData = filter.uniformData; + const uniforms = filter.uniforms; + + // 0 is reserved for the pixi texture so we start at 1! + let textureCount = 1; + let currentState; + + // filterArea and filterClamp that are handled by FilterManager directly + // they must not appear in uniformData + + if (shader.uniforms.filterArea) + { + currentState = this.filterData.stack[this.filterData.index]; + + const filterArea = shader.uniforms.filterArea; + + filterArea[0] = currentState.renderTarget.size.width; + filterArea[1] = currentState.renderTarget.size.height; + filterArea[2] = currentState.sourceFrame.x; + filterArea[3] = currentState.sourceFrame.y; + + shader.uniforms.filterArea = filterArea; + } + + // use this to clamp displaced texture coords so they belong to filterArea + // see displacementFilter fragment shader for an example + if (shader.uniforms.filterClamp) + { + currentState = currentState || this.filterData.stack[this.filterData.index]; + + const filterClamp = shader.uniforms.filterClamp; + + filterClamp[0] = 0; + filterClamp[1] = 0; + filterClamp[2] = (currentState.sourceFrame.width - 1) / currentState.renderTarget.size.width; + filterClamp[3] = (currentState.sourceFrame.height - 1) / currentState.renderTarget.size.height; + + shader.uniforms.filterClamp = filterClamp; + } + + // TODO Cacheing layer.. + for (const i in uniformData) + { + if (uniformData[i].type === 'sampler2D' && uniforms[i] !== 0) + { + if (uniforms[i].baseTexture) + { + shader.uniforms[i] = this.renderer.bindTexture(uniforms[i].baseTexture, textureCount); + } + else + { + shader.uniforms[i] = textureCount; + + // TODO + // this is helpful as renderTargets can also be set. + // Although thinking about it, we could probably + // make the filter texture cache return a RenderTexture + // rather than a renderTarget + const gl = this.renderer.gl; + + this.renderer.boundTextures[textureCount] = this.renderer.emptyTextures[textureCount]; + gl.activeTexture(gl.TEXTURE0 + textureCount); + + uniforms[i].texture.bind(); + } + + textureCount++; + } + else if (uniformData[i].type === 'mat3') + { + // check if its pixi matrix.. + if (uniforms[i].a !== undefined) + { + shader.uniforms[i] = uniforms[i].toArray(true); + } + else + { + shader.uniforms[i] = uniforms[i]; + } + } + else if (uniformData[i].type === 'vec2') + { + // check if its a point.. + if (uniforms[i].x !== undefined) + { + const val = shader.uniforms[i] || new Float32Array(2); + + val[0] = uniforms[i].x; + val[1] = uniforms[i].y; + shader.uniforms[i] = val; + } + else + { + shader.uniforms[i] = uniforms[i]; + } + } + else if (uniformData[i].type === 'float') + { + if (shader.uniforms.data[i].value !== uniformData[i]) + { + shader.uniforms[i] = uniforms[i]; + } + } + else + { + shader.uniforms[i] = uniforms[i]; + } + } + } + + /** * Gets a render target from the pool, or creates a new one. * * @param {boolean} clear - Should we clear the render texture when we get it? @@ -442,7 +541,6 @@ renderTarget.resolution = resolution; renderTarget.defaultFrame.width = renderTarget.size.width = minWidth / resolution; renderTarget.defaultFrame.height = renderTarget.size.height = minHeight / resolution; - renderTarget.attachStencilBuffer(); return renderTarget; } diff --git a/src/core/settings.js b/src/core/settings.js index bb08cb0..f4dd087 100644 --- a/src/core/settings.js +++ b/src/core/settings.js @@ -77,11 +77,11 @@ * * @static * @memberof PIXI.settings - * @type {RegExp|string} + * @type {RegExp} * @example `@2x` - * @default /@(.+)x/ + * @default /@([0-9\.]+)x/ */ - RETINA_PREFIX: /@(.+)x/, + RETINA_PREFIX: /@([0-9\.]+)x/, /** * The default render options if none are supplied to {@link PIXI.WebGLRenderer} @@ -175,14 +175,24 @@ SCALE_MODE: 0, /** - * Default specify float precision in shaders. + * Default specify float precision in vertex shader. + * + * @static + * @memberof PIXI.settings + * @type {PIXI.PRECISION} + * @default PIXI.PRECISION.HIGH + */ + PRECISION_VERTEX: 'highp', + + /** + * Default specify float precision in fragment shader. * * @static * @memberof PIXI.settings * @type {PIXI.PRECISION} * @default PIXI.PRECISION.MEDIUM */ - PRECISION: 'mediump', + PRECISION_FRAGMENT: 'mediump', /** * Can we upload the same buffer in a single frame? diff --git a/src/core/sprites/Sprite.js b/src/core/sprites/Sprite.js index 28b0b18..121bf58 100644 --- a/src/core/sprites/Sprite.js +++ b/src/core/sprites/Sprite.js @@ -212,11 +212,11 @@ } else { - w0 = orig.width * (1 - anchor._x); - w1 = orig.width * -anchor._x; + w1 = -anchor._x * orig.width; + w0 = w1 + orig.width; - h0 = orig.height * (1 - anchor._y); - h1 = orig.height * -anchor._y; + h1 = -anchor._y * orig.height; + h0 = h1 + orig.height; } // xy @@ -269,11 +269,11 @@ const tx = wt.tx; const ty = wt.ty; - const w0 = (orig.width) * (1 - anchor._x); - const w1 = (orig.width) * -anchor._x; + const w1 = -anchor._x * orig.width; + const w0 = w1 + orig.width; - const h0 = orig.height * (1 - anchor._y); - const h1 = orig.height * -anchor._y; + const h1 = -anchor._y * orig.height; + const h0 = h1 + orig.height; // xy vertexData[0] = (a * w1) + (c * h1) + tx; @@ -440,7 +440,7 @@ * * @static * @param {number|string|PIXI.BaseTexture|HTMLCanvasElement|HTMLVideoElement} source Source to create texture from - * @return {PIXI.Sprite} The newly created texture + * @return {PIXI.Sprite} The newly created sprite */ static from(source) { @@ -538,8 +538,8 @@ } /** - * The tint applied to the sprite. This is a hex value. A value of - * 0xFFFFFF will remove any tint effect. + * The tint applied to the sprite. This is a hex value. + * A value of 0xFFFFFF will remove any tint effect. * * @member {number} * @default 0xFFFFFF diff --git a/src/core/sprites/canvas/CanvasTinter.js b/src/core/sprites/canvas/CanvasTinter.js index 1cf316b..27e5e0c 100644 --- a/src/core/sprites/canvas/CanvasTinter.js +++ b/src/core/sprites/canvas/CanvasTinter.js @@ -75,8 +75,8 @@ crop.width *= resolution; crop.height *= resolution; - canvas.width = crop.width; - canvas.height = crop.height; + canvas.width = Math.ceil(crop.width); + canvas.height = Math.ceil(crop.height); context.fillStyle = `#${(`00000${(color | 0).toString(16)}`).substr(-6)}`; @@ -130,8 +130,8 @@ crop.width *= resolution; crop.height *= resolution; - canvas.width = crop.width; - canvas.height = crop.height; + canvas.width = Math.ceil(crop.width); + canvas.height = Math.ceil(crop.height); context.globalCompositeOperation = 'copy'; context.fillStyle = `#${(`00000${(color | 0).toString(16)}`).substr(-6)}`; @@ -172,8 +172,8 @@ crop.width *= resolution; crop.height *= resolution; - canvas.width = crop.width; - canvas.height = crop.height; + canvas.width = Math.ceil(crop.width); + canvas.height = Math.ceil(crop.height); context.globalCompositeOperation = 'copy'; context.drawImage( diff --git a/src/core/sprites/webgl/SpriteRenderer.js b/src/core/sprites/webgl/SpriteRenderer.js index 2affcd5..7c352e2 100644 --- a/src/core/sprites/webgl/SpriteRenderer.js +++ b/src/core/sprites/webgl/SpriteRenderer.js @@ -30,7 +30,7 @@ /** * Number of values sent in the vertex buffer. - * positionX, positionY, colorR, colorG, colorB = 5 + * aVertexPosition(2), aTextureCoord(1), aColor(1), aTextureId(1) = 5 * * @member {number} */ @@ -75,7 +75,6 @@ this.shader = null; this.currentIndex = 0; - TICK = 0; this.groups = []; for (let k = 0; k < this.size; k++) diff --git a/src/core/textures/BaseTexture.js b/src/core/textures/BaseTexture.js index 853de7e..aeccf39 100644 --- a/src/core/textures/BaseTexture.js +++ b/src/core/textures/BaseTexture.js @@ -232,16 +232,24 @@ this.realWidth = this.source.naturalWidth || this.source.videoWidth || this.source.width; this.realHeight = this.source.naturalHeight || this.source.videoHeight || this.source.height; - this.width = this.realWidth / this.resolution; - this.height = this.realHeight / this.resolution; - - this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); + this._updateDimensions(); } this.emit('update', this); } /** + * Update dimensions from real values + */ + _updateDimensions() + { + this.width = this.realWidth / this.resolution; + this.height = this.realHeight / this.resolution; + + this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); + } + + /** * Load a source. * * If the source is not-immediately-available, such as an image that needs to be @@ -525,11 +533,7 @@ this.realWidth = Math.round(svgWidth * this.sourceScale); this.realHeight = Math.round(svgHeight * this.sourceScale); - this.width = this.realWidth / this.resolution; - this.height = this.realHeight / this.resolution; - - // Check pow2 after scale - this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); + this._updateDimensions(); // Create a canvas element const canvas = document.createElement('canvas'); @@ -690,4 +694,52 @@ return baseTexture; } + + /** + * Helper function that creates a base texture based on the source you provide. + * The source can be - image url, image element, canvas element. + * + * @static + * @param {string|HTMLImageElement|HTMLCanvasElement} source - The source to create base texture from. + * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values + * @param {number} [sourceScale=(auto)] - Scale for the original image, used with Svg images. + * @return {PIXI.BaseTexture} The new base texture. + */ + static from(source, scaleMode, sourceScale) + { + if (typeof source === 'string') + { + return BaseTexture.fromImage(source, undefined, scaleMode, sourceScale); + } + else if (source instanceof HTMLImageElement) + { + const imageUrl = source.src; + let baseTexture = BaseTextureCache[imageUrl]; + + if (!baseTexture) + { + baseTexture = new BaseTexture(source, scaleMode); + baseTexture.imageUrl = imageUrl; + + if (sourceScale) + { + baseTexture.sourceScale = sourceScale; + } + + // if there is an @2x at the end of the url we are going to assume its a highres image + baseTexture.resolution = getResolutionOfUrl(imageUrl); + + BaseTextureCache[imageUrl] = baseTexture; + } + + return baseTexture; + } + else if (source instanceof HTMLCanvasElement) + { + return BaseTexture.fromCanvas(source, scaleMode); + } + + // lets assume its a base texture! + return source; + } } diff --git a/src/core/textures/Spritesheet.js b/src/core/textures/Spritesheet.js new file mode 100644 index 0000000..d469c62 --- /dev/null +++ b/src/core/textures/Spritesheet.js @@ -0,0 +1,274 @@ +import { Rectangle, Texture } from '../'; +import { getResolutionOfUrl, TextureCache } from '../utils'; + +/** + * Utility class for maintaining reference to a collection + * of Textures on a single Spritesheet. + * + * @class + * @memberof PIXI + */ +export default class Spritesheet +{ + /** + * The maximum number of Textures to build per process. + * + * @type {number} + * @default 1000 + */ + static get BATCH_SIZE() + { + return 1000; + } + + /** + * @param {PIXI.BaseTexture} baseTexture Reference to the source BaseTexture object. + * @param {Object} data - Spritesheet image data. + * @param {string} [resolutionFilename] - The filename to consider when determining + * the resolution of the spritesheet. If not provided, the imageUrl will + * be used on the BaseTexture. + */ + constructor(baseTexture, data, resolutionFilename = null) + { + /** + * Reference to ths source texture + * @type {PIXI.BaseTexture} + */ + this.baseTexture = baseTexture; + + /** + * Map of spritesheet textures. + * @type {Object} + */ + this.textures = {}; + + /** + * Reference to the original JSON data. + * @type {Object} + */ + this.data = data; + + /** + * The resolution of the spritesheet. + * @type {number} + */ + this.resolution = this._updateResolution( + resolutionFilename || this.baseTexture.imageUrl + ); + + /** + * Map of spritesheet frames. + * @type {Object} + * @private + */ + this._frames = this.data.frames; + + /** + * Collection of frame names. + * @type {string[]} + * @private + */ + this._frameKeys = Object.keys(this._frames); + + /** + * Current batch index being processed. + * @type {number} + * @private + */ + this._batchIndex = 0; + + /** + * Callback when parse is completed. + * @type {Function} + * @private + */ + this._callback = null; + } + + /** + * Generate the resolution from the filename or fallback + * to the meta.scale field of the JSON data. + * + * @private + * @param {string} resolutionFilename - The filename to use for resolving + * the default resolution. + * @return {number} Resolution to use for spritesheet. + */ + _updateResolution(resolutionFilename) + { + const scale = this.data.meta.scale; + + // Use a defaultValue of `null` to check if a url-based resolution is set + let resolution = getResolutionOfUrl(resolutionFilename, null); + + // No resolution found via URL + if (resolution === null) + { + // Use the scale value or default to 1 + resolution = scale !== undefined ? parseFloat(scale) : 1; + } + + // For non-1 resolutions, update baseTexture + if (resolution !== 1) + { + this.baseTexture.resolution = resolution; + this.baseTexture.update(); + } + + return resolution; + } + + /** + * Parser spritesheet from loaded data. This is done asynchronously + * to prevent creating too many Texture within a single process. + * + * @param {Function} callback - Callback when complete returns + * a map of the Textures for this spritesheet. + */ + parse(callback) + { + this._batchIndex = 0; + this._callback = callback; + + if (this._frameKeys.length <= Spritesheet.BATCH_SIZE) + { + this._processFrames(0); + this._parseComplete(); + } + else + { + this._nextBatch(); + } + } + + /** + * Process a batch of frames + * + * @private + * @param {number} initialFrameIndex - The index of frame to start. + */ + _processFrames(initialFrameIndex) + { + let frameIndex = initialFrameIndex; + const maxFrames = Spritesheet.BATCH_SIZE; + + while (frameIndex - initialFrameIndex < maxFrames && frameIndex < this._frameKeys.length) + { + const i = this._frameKeys[frameIndex]; + const rect = this._frames[i].frame; + + if (rect) + { + let frame = null; + let trim = null; + const orig = new Rectangle( + 0, + 0, + this._frames[i].sourceSize.w / this.resolution, + this._frames[i].sourceSize.h / this.resolution + ); + + if (this._frames[i].rotated) + { + frame = new Rectangle( + rect.x / this.resolution, + rect.y / this.resolution, + rect.h / this.resolution, + rect.w / this.resolution + ); + } + else + { + frame = new Rectangle( + rect.x / this.resolution, + rect.y / this.resolution, + rect.w / this.resolution, + rect.h / this.resolution + ); + } + + // Check to see if the sprite is trimmed + if (this._frames[i].trimmed) + { + trim = new Rectangle( + this._frames[i].spriteSourceSize.x / this.resolution, + this._frames[i].spriteSourceSize.y / this.resolution, + rect.w / this.resolution, + rect.h / this.resolution + ); + } + + this.textures[i] = new Texture( + this.baseTexture, + frame, + orig, + trim, + this._frames[i].rotated ? 2 : 0 + ); + + // lets also add the frame to pixi's global cache for fromFrame and fromImage functions + TextureCache[i] = this.textures[i]; + } + + frameIndex++; + } + } + + /** + * The parse has completed. + * + * @private + */ + _parseComplete() + { + const callback = this._callback; + + this._callback = null; + this._batchIndex = 0; + callback.call(this, this.textures); + } + + /** + * Begin the next batch of textures. + * + * @private + */ + _nextBatch() + { + this._processFrames(this._batchIndex * Spritesheet.BATCH_SIZE); + this._batchIndex++; + setTimeout(() => + { + if (this._batchIndex * Spritesheet.BATCH_SIZE < this._frameKeys.length) + { + this._nextBatch(); + } + else + { + this._parseComplete(); + } + }, 0); + } + + /** + * Destroy Spritesheet and don't use after this. + * + * @param {boolean} [destroyBase=false] Whether to destroy the base texture as well + */ + destroy(destroyBase = false) + { + for (const i in this.textures) + { + this.textures[i].destroy(); + } + this._frames = null; + this._frameKeys = null; + this.data = null; + this.textures = null; + if (destroyBase) + { + this.baseTexture.destroy(); + } + this.baseTexture = null; + } +} diff --git a/src/core/textures/Texture.js b/src/core/textures/Texture.js index 6403d4f..afe4254 100644 --- a/src/core/textures/Texture.js +++ b/src/core/textures/Texture.js @@ -3,7 +3,7 @@ import TextureUvs from './TextureUvs'; import EventEmitter from 'eventemitter3'; import { Rectangle } from '../math'; -import { TextureCache, BaseTextureCache } from '../utils'; +import { TextureCache, BaseTextureCache, getResolutionOfUrl } from '../utils'; /** * A texture stores the information that represents an image or part of an image. It cannot be added @@ -365,7 +365,8 @@ * The source can be - frame id, image url, video url, canvas element, video element, base texture * * @static - * @param {number|string|PIXI.BaseTexture|HTMLCanvasElement|HTMLVideoElement} source - Source to create texture from + * @param {number|string|HTMLImageElement|HTMLCanvasElement|HTMLVideoElement|PIXI.BaseTexture} + * source - Source to create texture from * @return {PIXI.Texture} The newly created texture */ static from(source) @@ -393,7 +394,7 @@ } else if (source instanceof HTMLImageElement) { - return new Texture(new BaseTexture(source)); + return new Texture(BaseTexture.from(source)); } else if (source instanceof HTMLCanvasElement) { @@ -413,6 +414,43 @@ } /** + * Create a texture from a source and add to the cache. + * + * @static + * @param {HTMLImageElement|HTMLCanvasElement} source - The input source. + * @param {String} imageUrl - File name of texture, for cache and resolving resolution. + * @param {String} [name] - Human readible name for the texture cache. If no name is + * specified, only `imageUrl` will be used as the cache ID. + * @return {PIXI.Texture} Output texture + */ + static fromLoader(source, imageUrl, name) + { + const baseTexture = new BaseTexture(source, null, getResolutionOfUrl(imageUrl)); + const texture = new Texture(baseTexture); + + baseTexture.imageUrl = imageUrl; + + // No name, use imageUrl instead + if (!name) + { + name = imageUrl; + } + + // lets also add the frame to pixi's global cache for fromFrame and fromImage fucntions + BaseTextureCache[name] = baseTexture; + TextureCache[name] = texture; + + // also add references by url if they are different. + if (name !== imageUrl) + { + BaseTextureCache[imageUrl] = baseTexture; + TextureCache[imageUrl] = texture; + } + + return texture; + } + + /** * Adds a texture to the global TextureCache. This cache is shared across the whole PIXI object. * * @static @@ -522,6 +560,29 @@ } } +function createWhiteTexture() +{ + const canvas = document.createElement('canvas'); + + canvas.width = 10; + canvas.height = 10; + + const context = canvas.getContext('2d'); + + context.fillStyle = 'white'; + context.fillRect(0, 0, 10, 10); + + return new Texture(new BaseTexture(canvas)); +} + +function removeAllHandlers(tex) +{ + tex.destroy = function _emptyDestroy() { /* empty */ }; + tex.on = function _emptyOn() { /* empty */ }; + tex.once = function _emptyOnce() { /* empty */ }; + tex.emit = function _emptyEmit() { /* empty */ }; +} + /** * An empty texture, used often to not have to create multiple empty textures. * Can not be destroyed. @@ -530,7 +591,14 @@ * @constant */ Texture.EMPTY = new Texture(new BaseTexture()); -Texture.EMPTY.destroy = function _emptyDestroy() { /* empty */ }; -Texture.EMPTY.on = function _emptyOn() { /* empty */ }; -Texture.EMPTY.once = function _emptyOnce() { /* empty */ }; -Texture.EMPTY.emit = function _emptyEmit() { /* empty */ }; +removeAllHandlers(Texture.EMPTY); + +/** + * A white texture of 10x10 size, used for graphics and other things + * Can not be destroyed. + * + * @static + * @constant + */ +Texture.WHITE = createWhiteTexture(); +removeAllHandlers(Texture.WHITE); diff --git a/src/core/ticker/Ticker.js b/src/core/ticker/Ticker.js index d98a1f0..17a1517 100644 --- a/src/core/ticker/Ticker.js +++ b/src/core/ticker/Ticker.js @@ -67,11 +67,12 @@ * is based, this value is neither capped nor scaled. * If the platform supports DOMHighResTimeStamp, * this value will have a precision of 1 µs. + * Defaults to target frame time * * @member {number} - * @default 1 / TARGET_FPMS + * @default 16.66 */ - this.elapsedMS = 1 / settings.TARGET_FPMS; // default to target frame time + this.elapsedMS = 1 / settings.TARGET_FPMS; /** * The last time {@link PIXI.ticker.Ticker#update} was invoked. diff --git a/src/core/utils/index.js b/src/core/utils/index.js index 6ff609f..be44750 100644 --- a/src/core/utils/index.js +++ b/src/core/utils/index.js @@ -2,6 +2,7 @@ import settings from '../settings'; import EventEmitter from 'eventemitter3'; import pluginTarget from './pluginTarget'; +import * as mixins from './mixin'; import * as isMobile from 'ismobilejs'; let nextUid = 0; @@ -33,6 +34,7 @@ * @type {mixin} */ pluginTarget, + mixins, }; /** @@ -93,7 +95,7 @@ */ export function rgb2hex(rgb) { - return (((rgb[0] * 255) << 16) + ((rgb[1] * 255) << 8) + (rgb[2] * 255)); + return (((rgb[0] * 255) << 16) + ((rgb[1] * 255) << 8) + (rgb[2] * 255 | 0)); } /** @@ -368,3 +370,43 @@ * @private */ export const ProgramCache = {}; + +/** + * Destroys all texture in the cache + * + * @memberof PIXI.utils + * @function destroyTextureCache + */ +export function destroyTextureCache() +{ + let key; + + for (key in TextureCache) + { + TextureCache[key].destroy(); + } + for (key in BaseTextureCache) + { + BaseTextureCache[key].destroy(); + } +} + +/** + * Removes all textures from cache, but does not destroy them + * + * @memberof PIXI.utils + * @function clearTextureCache + */ +export function clearTextureCache() +{ + let key; + + for (key in TextureCache) + { + delete TextureCache[key]; + } + for (key in BaseTextureCache) + { + delete BaseTextureCache[key]; + } +} diff --git a/src/core/utils/mixin.js b/src/core/utils/mixin.js new file mode 100644 index 0000000..114de2a --- /dev/null +++ b/src/core/utils/mixin.js @@ -0,0 +1,59 @@ +/** + * Mixes all enumerable properties and methods from a source object to a target object. + * + * @memberof PIXI.utils.mixins + * @function mixin + * @param {object} target The prototype or instance that properties and methods should be added to. + * @param {object} source The source of properties and methods to mix in. + */ +export function mixin(target, source) +{ + if (!target || !source) return; + // in ES8/ES2017, this would be really easy: + // Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); + + // get all the enumerable property keys + const keys = Object.keys(source); + + // loop through properties + for (let i = 0; i < keys.length; ++i) + { + const propertyName = keys[i]; + + // Set the property using the property descriptor - this works for accessors and normal value properties + Object.defineProperty(target, propertyName, Object.getOwnPropertyDescriptor(source, propertyName)); + } +} + +const mixins = []; + +/** + * Queues a mixin to be handled towards the end of the initialization of PIXI, so that deprecation + * can take effect. + * + * @memberof PIXI.utils.mixins + * @function delayMixin + * @private + * @param {object} target The prototype or instance that properties and methods should be added to. + * @param {object} source The source of properties and methods to mix in. + */ +export function delayMixin(target, source) +{ + mixins.push(target, source); +} + +/** + * Handles all mixins queued via delayMixin(). + * + * @memberof PIXI.utils.mixins + * @function performMixins + * @private + */ +export function performMixins() +{ + for (let i = 0; i < mixins.length; i += 2) + { + mixin(mixins[i], mixins[i + 1]); + } + mixins.length = 0; +} diff --git a/src/deprecation.js b/src/deprecation.js index 045a19f..b9c73b3 100644 --- a/src/deprecation.js +++ b/src/deprecation.js @@ -5,6 +5,7 @@ import * as filters from './filters'; import * as prepare from './prepare'; import * as loaders from './loaders'; +import * as interaction from './interaction'; // provide method to give a stack track for warnings // useful for tracking-down where deprecated methods/properties/classes @@ -534,7 +535,7 @@ { parent: 'GC_MODES', target: 'GC_MODE' }, { parent: 'WRAP_MODES', target: 'WRAP_MODE' }, { parent: 'SCALE_MODES', target: 'SCALE_MODE' }, - { parent: 'PRECISION', target: 'PRECISION' }, + { parent: 'PRECISION', target: 'PRECISION_FRAGMENT' }, ]; for (let i = 0; i < defaults.length; i++) @@ -558,6 +559,32 @@ }); } +Object.defineProperties(core.settings, { + + /** + * @static + * @name PRECISION + * @memberof PIXI.settings + * @see PIXI.PRECISION + * @deprecated since version 4.4.0 + */ + PRECISION: { + enumerable: true, + get() + { + warn('PIXI.settings.PRECISION has been deprecated, please use PIXI.settings.PRECISION_FRAGMENT'); + + return core.settings.PRECISION_FRAGMENT; + }, + set(value) + { + warn('PIXI.settings.PRECISION has been deprecated, please use PIXI.settings.PRECISION_FRAGMENT'); + + core.settings.PRECISION_FRAGMENT = value; + }, + }, +}); + Object.defineProperties(extras, { /** @@ -694,7 +721,7 @@ warn('determineFontProperties is now deprecated, please use the static calculateFontProperties method, ' + 'e.g : Text.calculateFontProperties(fontStyle);'); - return Text.calculateFontProperties(fontStyle); + return core.Text.calculateFontProperties(fontStyle); }; Object.defineProperties(core.TextStyle.prototype, { @@ -1003,3 +1030,69 @@ }, }, }); + +/** + * @name PIXI.interaction.interactiveTarget#defaultCursor + * @static + * @type {number} + * @see PIXI.interaction.interactiveTarget#cursor + * @deprecated since 4.3.0 + */ +Object.defineProperty(interaction.interactiveTarget, 'defaultCursor', { + set(value) + { + warn('Property defaultCursor has been replaced with \'cursor\'. '); + this.cursor = value; + }, + get() + { + warn('Property defaultCursor has been replaced with \'cursor\'. '); + + return this.cursor; + }, + enumerable: true, +}); + +/** + * @name PIXI.interaction.InteractionManager#defaultCursorStyle + * @static + * @type {string} + * @see PIXI.interaction.InteractionManager#cursorStyles + * @deprecated since 4.3.0 + */ +Object.defineProperty(interaction.InteractionManager, 'defaultCursorStyle', { + set(value) + { + warn('Property defaultCursorStyle has been replaced with \'cursorStyles.default\'. '); + this.cursorStyles.default = value; + }, + get() + { + warn('Property defaultCursorStyle has been replaced with \'cursorStyles.default\'. '); + + return this.cursorStyles.default; + }, +}); + +/** + * @name PIXI.interaction.InteractionManager#currentCursorStyle + * @static + * @type {string} + * @see PIXI.interaction.InteractionManager#cursorStyles + * @deprecated since 4.3.0 + */ +Object.defineProperty(interaction.InteractionManager, 'currentCursorStyle', { + set(value) + { + warn('Property currentCursorStyle has been removed.' + + 'See the currentCursorMode property, which works differently.'); + this.currentCursorMode = value; + }, + get() + { + warn('Property currentCursorStyle has been removed.' + + 'See the currentCursorMode property, which works differently.'); + + return this.currentCursorMode; + }, +}); diff --git a/src/extras/BitmapText.js b/src/extras/BitmapText.js index fb6c343..0a0e150 100644 --- a/src/extras/BitmapText.js +++ b/src/extras/BitmapText.js @@ -455,6 +455,73 @@ return this._textHeight; } + + /** + * Register a bitmap font with data and a texture. + * + * @static + * @param {XMLDocument} xml - The XML document data. + * @param {PIXI.Texture} texture - Texture with all symbols. + * @return {Object} Result font object with font, size, lineHeight and char fields. + */ + static registerFont(xml, texture) + { + const data = {}; + const info = xml.getElementsByTagName('info')[0]; + const common = xml.getElementsByTagName('common')[0]; + + data.font = info.getAttribute('face'); + data.size = parseInt(info.getAttribute('size'), 10); + data.lineHeight = parseInt(common.getAttribute('lineHeight'), 10); + data.chars = {}; + + // parse letters + const letters = xml.getElementsByTagName('char'); + + for (let i = 0; i < letters.length; i++) + { + const letter = letters[i]; + const charCode = parseInt(letter.getAttribute('id'), 10); + + const textureRect = new core.Rectangle( + parseInt(letter.getAttribute('x'), 10) + texture.frame.x, + parseInt(letter.getAttribute('y'), 10) + texture.frame.y, + parseInt(letter.getAttribute('width'), 10), + parseInt(letter.getAttribute('height'), 10) + ); + + data.chars[charCode] = { + xOffset: parseInt(letter.getAttribute('xoffset'), 10), + yOffset: parseInt(letter.getAttribute('yoffset'), 10), + xAdvance: parseInt(letter.getAttribute('xadvance'), 10), + kerning: {}, + texture: new core.Texture(texture.baseTexture, textureRect), + + }; + } + + // parse kernings + const kernings = xml.getElementsByTagName('kerning'); + + for (let i = 0; i < kernings.length; i++) + { + const kerning = kernings[i]; + const first = parseInt(kerning.getAttribute('first'), 10); + const second = parseInt(kerning.getAttribute('second'), 10); + const amount = parseInt(kerning.getAttribute('amount'), 10); + + if (data.chars[second]) + { + data.chars[second].kerning[first] = amount; + } + } + + // I'm leaving this as a temporary fix so we can test the bitmap fonts in v3 + // but it's very likely to change + BitmapText.fonts[data.font] = data; + + return data; + } } BitmapText.fonts = {}; diff --git a/src/index.js b/src/index.js index 59356cf..c553427 100644 --- a/src/index.js +++ b/src/index.js @@ -16,6 +16,10 @@ import * as particles from './particles'; import * as prepare from './prepare'; +// handle mixins now, after all code has been added, including deprecation +import { utils } from './core'; +utils.mixins.performMixins(); + export { accessibility, extract, diff --git a/src/interaction/InteractionManager.js b/src/interaction/InteractionManager.js index 7d51134..8baf13a 100644 --- a/src/interaction/InteractionManager.js +++ b/src/interaction/InteractionManager.js @@ -1,18 +1,20 @@ 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'; -// Mix interactiveTarget into core.DisplayObject.prototype -Object.assign( +// Mix interactiveTarget into core.DisplayObject.prototype, after deprecation has been handled +core.utils.mixins.delayMixin( core.DisplayObject.prototype, 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 +70,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 +101,6 @@ this.eventData = new InteractionEvent(); /** - * Tiny little interactiveData pool ! - * - * @member {PIXI.interaction.InteractionData[]} - */ - this.interactiveDataPool = []; - - /** * The DOM element to bind to. * * @private @@ -153,66 +155,12 @@ */ this.supportsPointerEvents = !!window.PointerEvent; - /** - * Are touch events being 'normalized' and converted into pointer events if pointer events are not supported - * For example, on a touch screen mobile device, a touchstart would also be emitted as a pointerdown - * - * @private - * @readonly - * @member {boolean} - */ - this.normalizeTouchEvents = !this.supportsPointerEvents && this.supportsTouchEvents; - - /** - * Are mouse events being 'normalized' and converted into pointer events if pointer events are not supported - * For example, on a desktop pc, a mousedown would also be emitted as a pointerdown - * - * @private - * @readonly - * @member {boolean} - */ - this.normalizeMouseEvents = !this.supportsPointerEvents && !MobileDevice.any; - // this will make it so that you don't have to call bind all the time /** * @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 +168,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,41 +199,32 @@ this.onPointerOver = this.onPointerOver.bind(this); /** - * @private - * @member {Function} + * Dictionary of how different cursor modes are handled. Strings are handled as CSS cursor + * values, objects are handled as dictionaries of CSS values for interactionDOMElement, + * and functions are called instead of changing the CSS. + * Default CSS cursor values are provided for 'default' and 'pointer' modes. + * @member {Object.)>} */ - this.onTouchStart = this.onTouchStart.bind(this); - this.processTouchStart = this.processTouchStart.bind(this); + this.cursorStyles = { + default: 'inherit', + pointer: 'pointer', + }; /** - * @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. - * - * @member {string} - * @default 'inherit' - */ - this.defaultCursorStyle = 'inherit'; - - /** - * The css style of the cursor that is being used. + * The mode of the cursor that is being used. + * The value of this is a key from the cursorStyles dictionary. * * @member {string} */ - this.currentCursorStyle = 'inherit'; + this.currentCursorMode = null; + + /** + * Internal cached let. + * + * @private + * @member {string} + */ + this.cursor = null; /** * Internal cached let. @@ -303,6 +249,7 @@ * object. * * @event mousedown + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -311,6 +258,7 @@ * on the display object. * * @event rightdown + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -319,6 +267,7 @@ * object. * * @event mouseup + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -327,6 +276,7 @@ * over the display object. * * @event rightup + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -335,6 +285,7 @@ * the display object. * * @event click + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -343,6 +294,7 @@ * and released on the display object. * * @event rightclick + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -352,6 +304,7 @@ * [mousedown]{@link PIXI.interaction.InteractionManager#event:mousedown}. * * @event mouseupoutside + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -361,6 +314,7 @@ * [rightdown]{@link PIXI.interaction.InteractionManager#event:rightdown}. * * @event rightupoutside + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -368,6 +322,7 @@ * Fired when a pointer device (usually a mouse) is moved while over the display object * * @event mousemove + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -375,6 +330,7 @@ * Fired when a pointer device (usually a mouse) is moved onto the display object * * @event mouseover + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -382,6 +338,7 @@ * Fired when a pointer device (usually a mouse) is moved off the display object * * @event mouseout + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -389,6 +346,7 @@ * Fired when a pointer device button is pressed on the display object. * * @event pointerdown + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -396,6 +354,14 @@ * Fired when a pointer device button is released over the display object. * * @event pointerup + * @type {PIXI.interaction.InteractionData} + * @memberof PIXI.interaction.InteractionManager# + */ + + /** + * Fired when the operating system cancels a pointer event + * + * @event pointercancel * @memberof PIXI.interaction.InteractionManager# */ @@ -403,6 +369,7 @@ * Fired when a pointer device button is pressed and released on the display object. * * @event pointertap + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -411,6 +378,7 @@ * registered a [pointerdown]{@link PIXI.interaction.InteractionManager#event:pointerdown}. * * @event pointerupoutside + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -418,6 +386,7 @@ * Fired when a pointer device is moved while over the display object * * @event pointermove + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -425,6 +394,7 @@ * Fired when a pointer device is moved onto the display object * * @event pointerover + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -432,6 +402,7 @@ * Fired when a pointer device is moved off the display object * * @event pointerout + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -439,6 +410,7 @@ * Fired when a touch point is placed on the display object. * * @event touchstart + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -446,6 +418,14 @@ * Fired when a touch point is removed from the display object. * * @event touchend + * @type {PIXI.interaction.InteractionData} + * @memberof PIXI.interaction.InteractionManager# + */ + + /** + * Fired when the operating system cancels a touch + * + * @event touchcancel * @memberof PIXI.interaction.InteractionManager# */ @@ -453,6 +433,7 @@ * Fired when a touch point is placed and removed from the display object. * * @event tap + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -461,6 +442,7 @@ * registered a [touchstart]{@link PIXI.interaction.InteractionManager#event:touchstart}. * * @event touchendoutside + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -468,6 +450,7 @@ * Fired when a touch point is moved along the display object. * * @event touchmove + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ } @@ -524,45 +507,30 @@ { window.document.addEventListener('pointermove', this.onPointerMove, true); this.interactionDOMElement.addEventListener('pointerdown', this.onPointerDown, true); - this.interactionDOMElement.addEventListener('pointerout', this.onPointerOut, true); + // pointerout is fired in addition to pointerup (for touch events) and pointercancel + // we already handle those, so for the purposes of what we do in onPointerOut, we only + // care about the pointerleave event + this.interactionDOMElement.addEventListener('pointerleave', 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; @@ -596,45 +564,26 @@ { window.document.removeEventListener('pointermove', this.onPointerMove, true); this.interactionDOMElement.removeEventListener('pointerdown', this.onPointerDown, true); - this.interactionDOMElement.removeEventListener('pointerout', this.onPointerOut, true); + this.interactionDOMElement.removeEventListener('pointerleave', 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) + 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.onPointerDown, true); + this.interactionDOMElement.removeEventListener('touchcancel', this.onPointerCancel, 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); - - 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 = null; @@ -664,7 +613,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; @@ -672,25 +621,80 @@ return; } - this.cursor = this.defaultCursorStyle; + this.cursor = null; // 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(); - - this.processInteractive(this.mouse.global, this.renderer._lastObjectRendered, this.processMouseOverOut, true); - - if (this.currentCursorStyle !== this.cursor) + for (const k in this.activeInteractionData) { - this.currentCursorStyle = this.cursor; - this.interactionDOMElement.style.cursor = this.cursor; + // eslint-disable-next-line no-prototype-builtins + if (this.activeInteractionData.hasOwnProperty(k)) + { + const interactionData = this.activeInteractionData[k]; + + if (interactionData.originalEvent && interactionData.pointerType !== 'touch') + { + const interactionEvent = this.configureInteractionEventForDOMEvent( + this.eventData, + interactionData.originalEvent, + interactionData + ); + + this.processInteractive( + interactionEvent, + this.renderer._lastObjectRendered, + this.processPointerOverOut, + true + ); + } + } } + this.setCursorMode(this.cursor); + // TODO } /** + * Sets the current cursor mode, handling any callbacks or CSS style changes. + * + * @param {string} mode - cursor mode, a key from the cursorStyles dictionary + */ + setCursorMode(mode) + { + mode = mode || 'default'; + // if the mode didn't actually change, bail early + if (this.currentCursorMode === mode) + { + return; + } + this.currentCursorMode = mode; + const style = this.cursorStyles[mode]; + + // only do things if there is a cursor style for it + if (style) + { + switch (typeof style) + { + case 'string': + // string styles are handled as cursor CSS + this.interactionDOMElement.style.cursor = style; + break; + case 'function': + // functions are just called, and passed the cursor mode + style(mode); + break; + case 'object': + // if it is an object, assume that it is a dictionary of CSS styles, + // apply it to the interactionDOMElement + Object.assign(this.interactionDOMElement.style, style); + break; + } + } + } + + /** * Dispatches an event on the display object that was interacted with * * @param {PIXI.Container|PIXI.Sprite|PIXI.extras.TilingSprite} displayObject - the display object in question @@ -748,22 +752,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 @@ -787,9 +795,8 @@ { interactiveParent = false; } - - // it has a mask! Then lets hit test that before continuing.. - if (hitTest && displayObject._mask) + // it has a mask! Then lets hit test that before continuing + else if (hitTest && displayObject._mask) { if (!displayObject._mask.containsPoint(point)) { @@ -797,14 +804,7 @@ } } - // it has a filterArea! Same as mask but easier, its a rectangle - if (hitTest && displayObject.filterArea) - { - if (!displayObject.filterArea.contains(point.x, point.y)) - { - hitTest = false; - } - } + let keepHitTestingAfterChildren = hitTest; // ** FREE TIP **! If an object is not interactive or has no buttons in it // (such as a game scene!) set interactiveChildren to false for that displayObject. @@ -818,7 +818,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 @@ -838,15 +838,20 @@ // This means we no longer need to hit test anything else. We still need to run // through all objects, but we don't need to perform any hit tests. - // { - hitTest = false; - // } + keepHitTestingAfterChildren = false; + + if (child.interactive) + { + hitTest = false; + } // we can break now as we have hit an object. } } } + hitTest = keepHitTestingAfterChildren; + // no point running this if the item is not interactive or does not have an interactive parent. if (interactive) { @@ -868,14 +873,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 +886,175 @@ } /** - * 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].isNormalized) { - 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); + if (!displayObject.trackedPointers[id]) + { + displayObject.trackedPointers[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.trackedPointers[id].rightDown = true; + } + else + { + displayObject.trackedPointers[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.trackedPointers[id] !== undefined) + { + delete displayObject.trackedPointers[id]; + this.dispatchEvent(displayObject, 'pointercancel', interactionEvent); + + if (e.type === 'touchcancel' || e.pointerType === 'touch') + { + this.dispatchEvent(displayObject, 'touchcancel', interactionEvent); + } } } @@ -1159,42 +1066,94 @@ */ 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) { - if (hit) - { - this.dispatchEvent(displayObject, 'pointerup', this.eventData); + const e = interactionEvent.data.originalEvent; - if (displayObject._pointerDown) + const id = interactionEvent.data.identifier; + + const trackingData = displayObject.trackedPointers[id]; + + const isTouch = (e.type === 'touchend' || e.pointerType === 'touch'); + + const isMouse = (e.type.indexOf('mouse') === 0 || e.pointerType === 'mouse'); + + // 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) { - displayObject._pointerDown = false; - this.dispatchEvent(displayObject, 'pointertap', this.eventData); + 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); + } + // update the down state of the tracking data + if (trackingData) + { + if (isRightButton) + { + trackingData.rightDown = hit; + } + else + { + trackingData.leftDown = hit; + } } } - else if (displayObject._pointerDown) + + // Pointers and Touches, and Mouse + if (hit) { - displayObject._pointerDown = false; - this.dispatchEvent(displayObject, 'pointerupoutside', this.eventData); + this.dispatchEvent(displayObject, 'pointerup', interactionEvent); + if (isTouch) this.dispatchEvent(displayObject, 'touchend', interactionEvent); + + if (trackingData) + { + this.dispatchEvent(displayObject, 'pointertap', interactionEvent); + if (isTouch) + { + this.dispatchEvent(displayObject, 'tap', interactionEvent); + // touches are no longer over (if they ever were) when we get the touchend + // so we should ensure that we don't keep pretending that they are + trackingData.over = false; + } + } + } + else if (trackingData) + { + this.dispatchEvent(displayObject, 'pointerupoutside', interactionEvent); + if (isTouch) this.dispatchEvent(displayObject, 'touchendoutside', interactionEvent); + } + // Only remove the tracking data if there is no over/down state still associated with it + if (trackingData && trackingData.none) + { + delete displayObject.trackedPointers[id]; } } @@ -1202,39 +1161,78 @@ * 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 = null; + } - 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') + { + this.setCursorMode(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 (isMouse) { - 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 +1240,94 @@ * 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.setCursorMode(null); + } - 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'); + + let trackingData = displayObject.trackedPointers[id]; + + // if we just moused over the display object, then we need to track that state + if (hit && !trackingData) + { + trackingData = displayObject.trackedPointers[id] = new InteractionTrackingData(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); + } + } + + // only change the cursor if it has not already been changed (by something deeper in the + // display tree) + if (isMouse && this.cursor === null) + { + this.cursor = displayObject.cursor; } } - 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); + } + // if there is no mouse down information for the pointer, then it is safe to delete + if (trackingData.none) + { + delete displayObject.trackedPointers[id]; + } } } @@ -1287,253 +1335,154 @@ * 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 && pointerEvent.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; + + // mark the touch as normalized, just so that we know we did it + touch.isNormalized = true; + + normalizedEvents.push(touch); + } } - else if (this.normalizeMouseEvents) + // apparently PointerEvent subclasses MouseEvent, so yay + else if (event instanceof MouseEvent && (!this.supportsPointerEvents || !(event instanceof window.PointerEvent))) { if (typeof event.isPrimary === 'undefined') event.isPrimary = true; if (typeof event.width === 'undefined') event.width = 1; @@ -1541,10 +1490,21 @@ 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; + + // mark the mouse event as normalized, just so that we know we did it + event.isNormalized = true; + + normalizedEvents.push(event); } + else + { + normalizedEvents.push(event); + } + + return normalizedEvents; } /** @@ -1563,30 +1523,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 +1542,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..6a1f69e --- /dev/null +++ b/src/interaction/InteractionTrackingData.js @@ -0,0 +1,147 @@ +/** + * 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 inactive (not over or down)? + * + * @member {number} + * @memberof PIXI.interaction.InteractionTrackingData# + */ + get none() + { + return this._flags === this.constructor.FLAGS.NONE; + } + + /** + * 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..d252fba 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 * ); */ @@ -38,68 +38,56 @@ hitArea: null, /** - * If enabled, the mouse cursor will change when hovered over the displayObject if it is interactive + * If enabled, the mouse cursor use the pointer behavior when hovered over the displayObject if it is interactive + * Setting this changes the 'cursor' property to `'pointer'`. * - * @inner {boolean} + * @member {boolean} + * @memberof PIXI.interaction.interactiveTarget# */ - buttonMode: false, + get buttonMode() + { + return this.cursor === 'pointer'; + }, + set buttonMode(value) + { + if (value) + { + this.cursor = 'pointer'; + } + else if (this.cursor === 'pointer') + { + this.cursor = null; + } + }, /** - * If buttonMode is enabled, this defines what CSS cursor property is used when the mouse cursor - * is hovered over the displayObject + * This defines what cursor mode is used when the mouse cursor + * is hovered over the displayObject. * * @see https://developer.mozilla.org/en/docs/Web/CSS/cursor * * @inner {string} */ - defaultCursor: 'pointer', - - // some internal checks.. - /** - * Internal check to detect if the mouse cursor is hovered over the displayObject - * - * @inner {boolean} - * @private - */ - _over: false, + cursor: null, /** - * Internal check to detect if the left mouse button is pressed on the displayObject + * Internal set of all active pointers, by identifier * - * @inner {boolean} + * @member {Map} + * @memberof PIXI.interaction.interactiveTarget# * @private */ - _isLeftDown: false, + get trackedPointers() + { + if (this._trackedPointers === undefined) this._trackedPointers = {}; + + return this._trackedPointers; + }, /** - * Internal check to detect if the right mouse button is pressed on the displayObject + * Map of all tracked pointers, by identifier. Use trackedPointers to access. * - * @inner {boolean} - * @private + * @private {Map} */ - _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, + _trackedPointers: undefined, }; diff --git a/src/loaders/bitmapFontParser.js b/src/loaders/bitmapFontParser.js index cc2618c..f4cf996 100644 --- a/src/loaders/bitmapFontParser.js +++ b/src/loaders/bitmapFontParser.js @@ -1,63 +1,19 @@ import * as path from 'path'; -import { Rectangle, Texture, utils } from '../core'; +import { utils } from '../core'; import { Resource } from 'resource-loader'; import { BitmapText } from '../extras'; +/** + * Register a BitmapText font from loader resource. + * + * @function parseBitmapFontData + * @memberof PIXI.loaders + * @param {PIXI.loaders.Resource} resource - Loader resource. + * @param {PIXI.Texture} texture - Reference to texture. + */ export function parse(resource, texture) { - const data = {}; - const info = resource.data.getElementsByTagName('info')[0]; - const common = resource.data.getElementsByTagName('common')[0]; - - data.font = info.getAttribute('face'); - data.size = parseInt(info.getAttribute('size'), 10); - data.lineHeight = parseInt(common.getAttribute('lineHeight'), 10); - data.chars = {}; - - // parse letters - const letters = resource.data.getElementsByTagName('char'); - - for (let i = 0; i < letters.length; i++) - { - const charCode = parseInt(letters[i].getAttribute('id'), 10); - - const textureRect = new Rectangle( - parseInt(letters[i].getAttribute('x'), 10) + texture.frame.x, - parseInt(letters[i].getAttribute('y'), 10) + texture.frame.y, - parseInt(letters[i].getAttribute('width'), 10), - parseInt(letters[i].getAttribute('height'), 10) - ); - - data.chars[charCode] = { - xOffset: parseInt(letters[i].getAttribute('xoffset'), 10), - yOffset: parseInt(letters[i].getAttribute('yoffset'), 10), - xAdvance: parseInt(letters[i].getAttribute('xadvance'), 10), - kerning: {}, - texture: new Texture(texture.baseTexture, textureRect), - - }; - } - - // parse kernings - const kernings = resource.data.getElementsByTagName('kerning'); - - for (let i = 0; i < kernings.length; i++) - { - const first = parseInt(kernings[i].getAttribute('first'), 10); - const second = parseInt(kernings[i].getAttribute('second'), 10); - const amount = parseInt(kernings[i].getAttribute('amount'), 10); - - if (data.chars[second]) - { - data.chars[second].kerning[first] = amount; - } - } - - resource.bitmapFont = data; - - // I'm leaving this as a temporary fix so we can test the bitmap fonts in v3 - // but it's very likely to change - BitmapText.fonts[data.font] = data; + resource.bitmapFont = BitmapText.registerFont(resource.data, texture); } export default function () diff --git a/src/loaders/index.js b/src/loaders/index.js index 8942436..5189793 100644 --- a/src/loaders/index.js +++ b/src/loaders/index.js @@ -5,4 +5,11 @@ export { default as bitmapFontParser, parse as parseBitmapFontData } from './bitmapFontParser'; export { default as spritesheetParser } from './spritesheetParser'; export { default as textureParser } from './textureParser'; + +/** + * Reference to **resource-loader**'s Resource class. + * See https://github.com/englercj/resource-loader + * @class Resource + * @memberof PIXI.loaders + */ export { Resource } from 'resource-loader'; diff --git a/src/loaders/loader.js b/src/loaders/loader.js index 0a670cb..767a411 100644 --- a/src/loaders/loader.js +++ b/src/loaders/loader.js @@ -7,20 +7,47 @@ /** * - * The new loader, extends Resource Loader by Chad Engler : https://github.com/englercj/resource-loader + * The new loader, extends Resource Loader by Chad Engler: https://github.com/englercj/resource-loader * * ```js - * let loader = PIXI.loader; // pixi exposes a premade instance for you to use. + * const loader = PIXI.loader; // pixi exposes a premade instance for you to use. * //or - * let loader = new PIXI.loaders.Loader(); // you can also create your own if you want + * const loader = new PIXI.loaders.Loader(); // you can also create your own if you want * - * loader.add('bunny', 'data/bunny.png'); - * loader.add('spaceship', 'assets/spritesheet.json'); + * const sprites = {}; + * + * // Chainable `add` to enqueue a resource + * loader.add('bunny', 'data/bunny.png') + * .add('spaceship', 'assets/spritesheet.json'); * loader.add('scoreFont', 'assets/score.fnt'); * - * loader.once('complete',onAssetsLoaded); + * // Chainable `pre` to add a middleware that runs for each resource, *before* loading that resource. + * // This is useful to implement custom caching modules (using filesystem, indexeddb, memory, etc). + * loader.pre(cachingMiddleware); * - * loader.load(); + * // Chainable `use` to add a middleware that runs for each resource, *after* loading that resource. + * // This is useful to implement custom parsing modules (like spritesheet parsers, spine parser, etc). + * loader.use(parsingMiddleware); + * + * // The `load` method loads the queue of resources, and calls the passed in callback called once all + * // resources have loaded. + * loader.load((loader, resources) => { + * // resources is an object where the key is the name of the resource loaded and the value is the resource object. + * // They have a couple default properties: + * // - `url`: The URL that the resource was loaded from + * // - `error`: The error that happened when trying to load (if any) + * // - `data`: The raw data that was loaded + * // also may contain other properties based on the middleware that runs. + * sprites.bunny = new PIXI.TilingSprite(resources.bunny.texture); + * sprites.spaceship = new PIXI.TilingSprite(resources.spaceship.texture); + * sprites.scoreFont = new PIXI.TilingSprite(resources.scoreFont.texture); + * }); + * + * // throughout the process multiple signals can be dispatched. + * loader.onProgress.add(() => {}); // called once per loaded/errored file + * loader.onError.add(() => {}); // called once per errored file + * loader.onLoad.add(() => {}); // called once per loaded file + * loader.onComplete.add(() => {}); // called once when the queued resources all load. * ``` * * @see https://github.com/englercj/resource-loader diff --git a/package.json b/package.json index 6a366ba..71ade15 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pixi.js", - "version": "4.3.4", + "version": "4.3.5", "description": "Pixi.js is a fast lightweight 2D library that works across all devices.", "author": "Mat Groves", "contributors": [ @@ -61,7 +61,7 @@ "ismobilejs": "^0.4.0", "object-assign": "^4.0.1", "pixi-gl-core": "^1.0.3", - "resource-loader": "^2.0.4" + "resource-loader": "^2.0.6" }, "devDependencies": { "babel-cli": "^6.18.0", @@ -70,9 +70,9 @@ "babel-preset-es2015": "^6.14.0", "babelify": "^7.3.0", "del": "^2.2.0", - "electron-prebuilt": "^1.3.2", + "electron": "^1.4.15", "eslint": "^3.5.0", - "floss": "^1.2.0", + "floss": "^2.0.1", "gh-pages": "^0.11.0", "jaguarjs-jsdoc": "^1.0.1", "js-md5": "^0.4.1", diff --git a/src/accessibility/AccessibilityManager.js b/src/accessibility/AccessibilityManager.js index df2716a..f16bcb1 100644 --- a/src/accessibility/AccessibilityManager.js +++ b/src/accessibility/AccessibilityManager.js @@ -3,7 +3,7 @@ import accessibleTarget from './accessibleTarget'; // add some extra variables to the container.. -Object.assign( +core.utils.mixins.delayMixin( core.DisplayObject.prototype, accessibleTarget ); diff --git a/src/core/Application.js b/src/core/Application.js index ea0f058..5340578 100644 --- a/src/core/Application.js +++ b/src/core/Application.js @@ -1,6 +1,6 @@ import { autoDetectRenderer } from './autoDetectRenderer'; import Container from './display/Container'; -import Ticker from './ticker/Ticker'; +import { shared, Ticker } from './ticker'; /** * Convenience class to create a new PIXI application. @@ -35,9 +35,9 @@ * @param {boolean} [noWebGL=false] - prevents selection of WebGL renderer, even if such is present * @param {boolean} [options.legacy=false] - If true Pixi will aim to ensure compatibility * with older / less advanced devices. If you experience unexplained flickering try setting this to true. - + * @param {boolean} [useSharedTicker=false] - `true` to use PIXI.ticker.shared, `false` to create new ticker. */ - constructor(width, height, options, noWebGL) + constructor(width, height, options, noWebGL, useSharedTicker = false) { /** * WebGL renderer if available, otherwise CanvasRenderer @@ -46,23 +46,46 @@ this.renderer = autoDetectRenderer(width, height, options, noWebGL); /** - * The root display container that's renderered. + * The root display container that's rendered. * @member {PIXI.Container} */ this.stage = new Container(); /** + * Internal reference to the ticker + * @member {PIXI.ticker.Ticker} + * @private + */ + this._ticker = null; + + /** * Ticker for doing render updates. * @member {PIXI.ticker.Ticker} + * @default PIXI.ticker.shared */ - this.ticker = new Ticker(); - - this.ticker.add(this.render, this); + this.ticker = useSharedTicker ? shared : new Ticker(); // Start the rendering this.start(); } + set ticker(ticker) // eslint-disable-line require-jsdoc + { + if (this._ticker) + { + this._ticker.remove(this.render, this); + } + this._ticker = ticker; + if (ticker) + { + ticker.add(this.render, this); + } + } + get ticker() // eslint-disable-line require-jsdoc + { + return this._ticker; + } + /** * Render the current stage. */ @@ -76,7 +99,7 @@ */ stop() { - this.ticker.stop(); + this._ticker.stop(); } /** @@ -84,7 +107,7 @@ */ start() { - this.ticker.start(); + this._ticker.start(); } /** @@ -98,13 +121,22 @@ } /** + * Reference to the renderer's screen rectangle. Its safe to use as filterArea or hitArea for whole screen + * @member {PIXI.Rectangle} + * @readonly + */ + get screen() + { + return this.renderer.screen; + } + + /** * Destroy and don't use after this. * @param {Boolean} [removeView=false] Automatically remove canvas from DOM. */ destroy(removeView) { this.stop(); - this.ticker.remove(this.render, this); this.ticker = null; this.stage.destroy(); diff --git a/src/core/Shader.js b/src/core/Shader.js new file mode 100644 index 0000000..78b17e6 --- /dev/null +++ b/src/core/Shader.js @@ -0,0 +1,46 @@ +import { GLShader } from 'pixi-gl-core'; +import settings from './settings'; + +function checkPrecision(src, def) +{ + if (src instanceof Array) + { + if (src[0].substring(0, 9) !== 'precision') + { + const copy = src.slice(0); + + copy.unshift(`precision ${def} float;`); + + return copy; + } + } + else if (src.substring(0, 9) !== 'precision') + { + return `precision ${def} float;\n${src}`; + } + + return src; +} + +/** + * Wrapper class, webGL Shader for Pixi. + * Adds precision string if vertexSrc or fragmentSrc have no mention of it. + * + * @class + * @extends GLShader + * @memberof PIXI + */ +export default class Shader extends GLShader +{ + /** + * + * @param {WebGLRenderingContext} gl - The current WebGL rendering context + * @param {string|string[]} vertexSrc - The vertex shader source as an array of strings. + * @param {string|string[]} fragmentSrc - The fragment shader source as an array of strings. + */ + constructor(gl, vertexSrc, fragmentSrc) + { + super(gl, checkPrecision(vertexSrc, settings.PRECISION_VERTEX), + checkPrecision(fragmentSrc, settings.PRECISION_FRAGMENT)); + } +} diff --git a/src/core/const.js b/src/core/const.js index ef6f662..3de0efe 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -188,7 +188,7 @@ * The gc modes that are supported by pixi. * * The {@link PIXI.settings.GC_MODE} Garbage Collection mode for pixi textures is AUTO - * If set to GC_MODE, the renderer will occasianally check textures usage. If they are not + * If set to GC_MODE, the renderer will occasionally check textures usage. If they are not * used for a specified period of time they will be removed from the GPU. They will of course * be uploaded again when they are required. This is a silent behind the scenes process that * should ensure that the GPU does not get filled up. diff --git a/src/core/display/Container.js b/src/core/display/Container.js index ed8cd5f..17fde8d 100644 --- a/src/core/display/Container.js +++ b/src/core/display/Container.js @@ -73,11 +73,12 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.push(child); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -108,11 +109,12 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.splice(index, 0, child); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -225,10 +227,11 @@ if (index === -1) return null; child.parent = null; + // ensure child transform will be recalculated + child.transform._parentID = -1; removeItems(this.children, index, 1); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -249,11 +252,12 @@ { const child = this.getChildAt(index); + // ensure child transform will be recalculated.. child.parent = null; + child.transform._parentID = -1; removeItems(this.children, index, 1); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -284,13 +288,12 @@ for (let i = 0; i < removed.length; ++i) { removed[i].parent = null; + if (removed[i].transform) + { + removed[i].transform._parentID = -1; + } } - // ensure a transform will be recalculated.. - if (this.transform) - { - this.transform._parentID = -1; - } this._boundsID++; this.onChildrenChange(beginIndex); diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index f37f2f5..be4c4fb 100644 --- a/src/core/graphics/Graphics.js +++ b/src/core/graphics/Graphics.js @@ -28,8 +28,9 @@ { /** * + * @param {boolean} [nativeLines=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP */ - constructor() + constructor(nativeLines = false) { super(); @@ -50,6 +51,13 @@ this.lineWidth = 0; /** + * If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * + * @member {boolean} + */ + this.nativeLines = nativeLines; + + /** * The color of any lines drawn. * * @member {string} @@ -757,28 +765,14 @@ if (!this._spriteRect) { - if (!Graphics._SPRITE_TEXTURE) - { - Graphics._SPRITE_TEXTURE = RenderTexture.create(10, 10); - - const canvas = document.createElement('canvas'); - - canvas.width = 10; - canvas.height = 10; - - const context = canvas.getContext('2d'); - - context.fillStyle = 'white'; - context.fillRect(0, 0, 10, 10); - - Graphics._SPRITE_TEXTURE = Texture.fromCanvas(canvas); - } - - this._spriteRect = new Sprite(Graphics._SPRITE_TEXTURE); + this._spriteRect = new Sprite(new Texture(Texture.WHITE)); } + + const sprite = this._spriteRect; + if (this.tint === 0xffffff) { - this._spriteRect.tint = this.graphicsData[0].fillColor; + sprite.tint = this.graphicsData[0].fillColor; } else { @@ -792,20 +786,21 @@ t1[1] *= t2[1]; t1[2] *= t2[2]; - this._spriteRect.tint = rgb2hex(t1); + sprite.tint = rgb2hex(t1); } - this._spriteRect.alpha = this.graphicsData[0].fillAlpha; - this._spriteRect.worldAlpha = this.worldAlpha * this._spriteRect.alpha; + sprite.alpha = this.graphicsData[0].fillAlpha; + sprite.worldAlpha = this.worldAlpha * sprite.alpha; + sprite.blendMode = this.blendMode; - Graphics._SPRITE_TEXTURE._frame.width = rect.width; - Graphics._SPRITE_TEXTURE._frame.height = rect.height; + sprite.texture._frame.width = rect.width; + sprite.texture._frame.height = rect.height; - this._spriteRect.transform.worldTransform = this.transform.worldTransform; + sprite.transform.worldTransform = this.transform.worldTransform; - this._spriteRect.anchor.set(-rect.x / rect.width, -rect.y / rect.height); - this._spriteRect._onAnchorUpdate(); + sprite.anchor.set(-rect.x / rect.width, -rect.y / rect.height); + sprite._onAnchorUpdate(); - this._spriteRect._renderWebGL(renderer); + sprite._renderWebGL(renderer); } /** @@ -1031,6 +1026,7 @@ this.fillColor, this.fillAlpha, this.filling, + this.nativeLines, shape ); diff --git a/src/core/graphics/GraphicsData.js b/src/core/graphics/GraphicsData.js index f5a87ae..b6f03c0 100644 --- a/src/core/graphics/GraphicsData.js +++ b/src/core/graphics/GraphicsData.js @@ -14,14 +14,19 @@ * @param {number} fillColor - the color of the fill * @param {number} fillAlpha - the alpha of the fill * @param {boolean} fill - whether or not the shape is filled with a colour + * @param {boolean} nativeLines - the method for drawing lines * @param {PIXI.Circle|PIXI.Rectangle|PIXI.Ellipse|PIXI.Polygon} shape - The shape object to draw. */ - constructor(lineWidth, lineColor, lineAlpha, fillColor, fillAlpha, fill, shape) + constructor(lineWidth, lineColor, lineAlpha, fillColor, fillAlpha, fill, nativeLines, shape) { /** * @member {number} the width of the line to draw */ this.lineWidth = lineWidth; + /** + * @member {boolean} if true the liens will be draw using LINES instead of TRIANGLE_STRIP + */ + this.nativeLines = nativeLines; /** * @member {number} the color of the line to draw @@ -85,6 +90,7 @@ this.fillColor, this.fillAlpha, this.fill, + this.nativeLines, this.shape ); } diff --git a/src/core/graphics/webgl/GraphicsRenderer.js b/src/core/graphics/webgl/GraphicsRenderer.js index 14083fe..67f5e92 100644 --- a/src/core/graphics/webgl/GraphicsRenderer.js +++ b/src/core/graphics/webgl/GraphicsRenderer.js @@ -102,7 +102,15 @@ shaderTemp.uniforms.alpha = graphics.worldAlpha; renderer.bindVao(webGLData.vao); - webGLData.vao.draw(gl.TRIANGLE_STRIP, webGLData.indices.length); + + if (graphics.nativeLines) + { + gl.drawArrays(gl.LINES, 0, webGLData.points.length / 6); + } + else + { + webGLData.vao.draw(gl.TRIANGLE_STRIP, webGLData.indices.length); + } } } diff --git a/src/core/graphics/webgl/utils/buildLine.js b/src/core/graphics/webgl/utils/buildLine.js index ecf43f7..4d6862e 100644 --- a/src/core/graphics/webgl/utils/buildLine.js +++ b/src/core/graphics/webgl/utils/buildLine.js @@ -11,7 +11,29 @@ * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties * @param {object} webGLData - an object containing all the webGL-specific information to create this shape */ -export default function buildLine(graphicsData, webGLData) +export default function (graphicsData, webGLData) +{ + if (graphicsData.nativeLines) + { + buildNativeLine(graphicsData, webGLData); + } + else + { + buildLine(graphicsData, webGLData); + } +} + +/** + * Builds a line to draw using the poligon method. + * + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @private + * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties + * @param {object} webGLData - an object containing all the webGL-specific information to create this shape + */ +function buildLine(graphicsData, webGLData) { // TODO OPTIMISE! let points = graphicsData.points; @@ -224,3 +246,46 @@ indices.push(indexStart - 1); } + +/** + * Builds a line to draw using the gl.drawArrays(gl.LINES) method + * + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @private + * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties + * @param {object} webGLData - an object containing all the webGL-specific information to create this shape + */ +function buildNativeLine(graphicsData, webGLData) +{ + let i = 0; + const points = graphicsData.points; + + if (points.length === 0) return; + + const verts = webGLData.points; + const length = points.length / 2; + + // sort color + const color = hex2rgb(graphicsData.lineColor); + const alpha = graphicsData.lineAlpha; + const r = color[0] * alpha; + const g = color[1] * alpha; + const b = color[2] * alpha; + + for (i = 1; i < length; i++) + { + const p1x = points[(i - 1) * 2]; + const p1y = points[((i - 1) * 2) + 1]; + + const p2x = points[i * 2]; + const p2y = points[(i * 2) + 1]; + + verts.push(p1x, p1y); + verts.push(r, g, b, alpha); + + verts.push(p2x, p2y); + verts.push(r, g, b, alpha); + } +} diff --git a/src/core/graphics/webgl/utils/buildRoundedRectangle.js b/src/core/graphics/webgl/utils/buildRoundedRectangle.js index 9abc807..a6c8461 100644 --- a/src/core/graphics/webgl/utils/buildRoundedRectangle.js +++ b/src/core/graphics/webgl/utils/buildRoundedRectangle.js @@ -77,6 +77,26 @@ } /** + * Calculate a single point for a quadratic bezier curve. + * Utility function used by quadraticBezierCurve. + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @private + * @param {number} n1 - first number + * @param {number} n2 - second number + * @param {number} perc - percentage + * @return {number} the result + * + */ +function getPt(n1, n2, perc) +{ + const diff = n2 - n1; + + return n1 + (diff * perc); +} + +/** * Calculate the points for a quadratic bezier curve. (helper function..) * Based on: https://stackoverflow.com/questions/785097/how-do-i-implement-a-bezier-curve-in-c * @@ -105,13 +125,6 @@ let x = 0; let y = 0; - function getPt(n1, n2, perc) - { - const diff = n2 - n1; - - return n1 + (diff * perc); - } - for (let i = 0, j = 0; i <= n; ++i) { j = i / n; diff --git a/src/core/index.js b/src/core/index.js index f7a817c..4f414e8 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -30,6 +30,7 @@ export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; export { default as CanvasGraphicsRenderer } from './graphics/canvas/CanvasGraphicsRenderer'; +export { default as Spritesheet } from './textures/Spritesheet'; export { default as Texture } from './textures/Texture'; export { default as BaseTexture } from './textures/BaseTexture'; export { default as RenderTexture } from './textures/RenderTexture'; diff --git a/src/core/math/Matrix.js b/src/core/math/Matrix.js index 0b77477..3679860 100644 --- a/src/core/math/Matrix.js +++ b/src/core/math/Matrix.js @@ -13,45 +13,50 @@ export default class Matrix { /** - * + * @param {number} [a=1] - x scale + * @param {number} [b=0] - y skew + * @param {number} [c=0] - x skew + * @param {number} [d=1] - y scale + * @param {number} [tx=0] - x translation + * @param {number} [ty=0] - y translation */ - constructor() + constructor(a = 1, b = 0, c = 0, d = 1, tx = 0, ty = 0) { /** * @member {number} * @default 1 */ - this.a = 1; + this.a = a; /** * @member {number} * @default 0 */ - this.b = 0; + this.b = b; /** * @member {number} * @default 0 */ - this.c = 0; + this.c = c; /** * @member {number} * @default 1 */ - this.d = 1; + this.d = d; /** * @member {number} * @default 0 */ - this.tx = 0; + this.tx = tx; /** * @member {number} * @default 0 */ - this.ty = 0; + this.ty = ty; this.array = null; } diff --git a/src/core/renderers/SystemRenderer.js b/src/core/renderers/SystemRenderer.js index fa116a6..a24c775 100644 --- a/src/core/renderers/SystemRenderer.js +++ b/src/core/renderers/SystemRenderer.js @@ -1,5 +1,5 @@ import { sayHello, hex2string, hex2rgb } from '../utils'; -import { Matrix } from '../math'; +import { Matrix, Rectangle } from '../math'; import { RENDERER_TYPE } from '../const'; import settings from '../settings'; import Container from '../display/Container'; @@ -21,8 +21,8 @@ { /** * @param {string} system - The name of the system this renderer is for. - * @param {number} [width=800] - the width of the canvas view - * @param {number} [height=600] - the height of the canvas view + * @param {number} [screenWidth=800] - the width of the screen + * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false @@ -37,7 +37,7 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(system, width, height, options) + constructor(system, screenWidth, screenHeight, options) { super(); @@ -69,20 +69,13 @@ this.type = RENDERER_TYPE.UNKNOWN; /** - * The width of the canvas view + * Measurements of the screen. (0, 0, screenWidth, screenHeight) * - * @member {number} - * @default 800 - */ - this.width = width || 800; - - /** - * The height of the canvas view + * Its safe to use as filterArea or hitArea for whole stage * - * @member {number} - * @default 600 + * @member {PIXI.Rectangle} */ - this.height = height || 600; + this.screen = new Rectangle(0, 0, screenWidth || 800, screenHeight || 600); /** * The canvas element that everything is drawn to @@ -107,7 +100,7 @@ this.transparent = options.transparent; /** - * Whether the render view should be resized automatically + * Whether css dimensions of canvas view should be resized to screen dimensions automatically * * @member {boolean} */ @@ -192,23 +185,48 @@ } /** - * Resizes the canvas view to the specified width and height + * Same as view.width, actual number of pixels in the canvas by horizontal * - * @param {number} width - the new width of the canvas view - * @param {number} height - the new height of the canvas view + * @member {number} + * @readonly + * @default 800 */ - resize(width, height) + get width() { - this.width = width * this.resolution; - this.height = height * this.resolution; + return this.view.width; + } - this.view.width = this.width; - this.view.height = this.height; + /** + * Same as view.height, actual number of pixels in the canvas by vertical + * + * @member {number} + * @readonly + * @default 600 + */ + get height() + { + return this.view.height; + } + + /** + * Resizes the screen and canvas to the specified width and height + * Canvas dimensions are multiplied by resolution + * + * @param {number} screenWidth - the new width of the screen + * @param {number} screenHeight - the new height of the screen + */ + resize(screenWidth, screenHeight) + { + this.screen.width = screenWidth; + this.screen.height = screenHeight; + + this.view.width = screenWidth * this.resolution; + this.view.height = screenHeight * this.resolution; if (this.autoResize) { - this.view.style.width = `${this.width / this.resolution}px`; - this.view.style.height = `${this.height / this.resolution}px`; + this.view.style.width = `${screenWidth}px`; + this.view.style.height = `${screenHeight}px`; } } @@ -249,11 +267,10 @@ this.type = RENDERER_TYPE.UNKNOWN; - this.width = 0; - this.height = 0; - this.view = null; + this.screen = null; + this.resolution = 0; this.transparent = false; diff --git a/src/core/renderers/canvas/CanvasRenderer.js b/src/core/renderers/canvas/CanvasRenderer.js index 1bd2d5e..e84e49c 100644 --- a/src/core/renderers/canvas/CanvasRenderer.js +++ b/src/core/renderers/canvas/CanvasRenderer.js @@ -18,8 +18,8 @@ export default class CanvasRenderer extends SystemRenderer { /** - * @param {number} [width=800] - the width of the canvas view - * @param {number} [height=600] - the height of the canvas view + * @param {number} [screenWidth=800] - the width of the screen + * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false @@ -34,9 +34,9 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(width, height, options = {}) + constructor(screenWidth, screenHeight, options = {}) { - super('Canvas', width, height, options); + super('Canvas', screenWidth, screenHeight, options); this.type = RENDERER_TYPE.CANVAS; @@ -96,7 +96,7 @@ this.context = null; this.renderingToScreen = false; - this.resize(width, height); + this.resize(screenWidth, screenHeight); } /** @@ -283,12 +283,12 @@ * * @extends PIXI.SystemRenderer#resize * - * @param {number} width - The new width of the canvas view - * @param {number} height - The new height of the canvas view + * @param {number} screenWidth - the new width of the screen + * @param {number} screenHeight - the new height of the screen */ - resize(width, height) + resize(screenWidth, screenHeight) { - super.resize(width, height); + super.resize(screenWidth, screenHeight); // reset the scale mode.. oddly this seems to be reset when the canvas is resized. // surely a browser bug?? Let pixi fix that for you.. diff --git a/src/core/renderers/webgl/TextureManager.js b/src/core/renderers/webgl/TextureManager.js index b685c1a..8227289 100644 --- a/src/core/renderers/webgl/TextureManager.js +++ b/src/core/renderers/webgl/TextureManager.js @@ -61,7 +61,7 @@ * Updates and/or Creates a WebGL texture for the renderer's context. * * @param {PIXI.BaseTexture|PIXI.Texture} texture - the texture to update - * @param {Number} location - the location the texture will be bound to. + * @param {number} location - the location the texture will be bound to. * @return {GLTexture} The gl texture. */ updateTexture(texture, location) diff --git a/src/core/renderers/webgl/WebGLRenderer.js b/src/core/renderers/webgl/WebGLRenderer.js index e4cd8a0..830b382 100644 --- a/src/core/renderers/webgl/WebGLRenderer.js +++ b/src/core/renderers/webgl/WebGLRenderer.js @@ -31,8 +31,8 @@ { /** * - * @param {number} [width=0] - the width of the canvas view - * @param {number} [height=0] - the height of the canvas view + * @param {number} [screenWidth=800] - the width of the screen + * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false @@ -53,9 +53,9 @@ * @param {boolean} [options.legacy=false] - If true Pixi will aim to ensure compatibility * with older / less advanced devices. If you experiance unexplained flickering try setting this to true. */ - constructor(width, height, options = {}) + constructor(screenWidth, screenHeight, options = {}) { - super('WebGL', width, height, options); + super('WebGL', screenWidth, screenHeight, options); this.legacy = !!options.legacy; @@ -245,7 +245,7 @@ glCore._testingContext = gl; // setup the width/height properties and gl viewport - this.resize(this.width, this.height); + this.resize(this.screen.width, this.screen.height); } /** @@ -343,16 +343,16 @@ /** * Resizes the webGL view to the specified width and height. * - * @param {number} width - the new width of the webGL view - * @param {number} height - the new height of the webGL view + * @param {number} screenWidth - the new width of the screen + * @param {number} screenHeight - the new height of the screen */ - resize(width, height) + resize(screenWidth, screenHeight) { // if(width * this.resolution === this.width && height * this.resolution === this.height)return; - SystemRenderer.prototype.resize.call(this, width, height); + SystemRenderer.prototype.resize.call(this, screenWidth, screenHeight); - this.rootRenderTarget.resize(width, height); + this.rootRenderTarget.resize(screenWidth, screenHeight); if (this._activeRenderTarget === this.rootRenderTarget) { @@ -396,6 +396,26 @@ } /** + * Erases the render texture and fills the drawing area with a colour + * + * @param {PIXI.RenderTexture} renderTexture - The render texture to clear + * @param {number} [clearColor] - The colour + * @return {PIXI.WebGLRenderer} Returns itself. + */ + clearRenderTexture(renderTexture, clearColor) + { + const baseTexture = renderTexture.baseTexture; + const renderTarget = baseTexture._glRenderTargets[this.CONTEXT_UID]; + + if (renderTarget) + { + renderTarget.clear(clearColor); + } + + return this; + } + + /** * Binds a render texture for rendering * * @param {PIXI.RenderTexture} renderTexture - The render texture to render @@ -474,9 +494,10 @@ * Changes the current GLShader to the one given in parameter * * @param {PIXI.glCore.Shader} shader - the new glShader + * @param {boolean} [autoProject=true] - Whether automatically set the projection matrix * @return {PIXI.WebGLRenderer} Returns itself. */ - _bindGLShader(shader) + _bindGLShader(shader, autoProject) { // TODO cache if (this._activeShader !== shader) @@ -484,8 +505,14 @@ this._activeShader = shader; shader.bind(); - // automatically set the projection matrix - shader.uniforms.projectionMatrix = this._activeRenderTarget.projectionMatrix.toArray(true); + // `autoProject` normally would be a default parameter set to true + // but because of how Babel transpiles default parameters + // it hinders the performance of this method. + if (autoProject !== false) + { + // automatically set the projection matrix + shader.uniforms.projectionMatrix = this._activeRenderTarget.projectionMatrix.toArray(true); + } } return this; diff --git a/src/core/renderers/webgl/WebGLState.js b/src/core/renderers/webgl/WebGLState.js index ba6ecc5..6c846d5 100755 --- a/src/core/renderers/webgl/WebGLState.js +++ b/src/core/renderers/webgl/WebGLState.js @@ -82,18 +82,20 @@ push() { // next state.. - let state = this.stack[++this.stackIndex]; + let state = this.stack[this.stackIndex]; if (!state) { state = this.stack[this.stackIndex] = new Uint8Array(16); } + ++this.stackIndex; + // copy state.. // set active state so we can force overrides of gl state for (let i = 0; i < this.activeState.length; i++) { - this.activeState[i] = state[i]; + state[i] = this.activeState[i]; } } diff --git a/src/core/renderers/webgl/filters/Filter.js b/src/core/renderers/webgl/filters/Filter.js index 2c3eaad..4eb6128 100644 --- a/src/core/renderers/webgl/filters/Filter.js +++ b/src/core/renderers/webgl/filters/Filter.js @@ -9,7 +9,7 @@ * @memberof PIXI * @extends PIXI.Shader */ -class Filter extends Shader +export default class Filter extends Shader { /** * @param {string} [vertexSrc] - The source of the vertex shader. @@ -56,15 +56,16 @@ * @param {PIXI.RenderTarget} input - The input render target. * @param {PIXI.RenderTarget} output - The target to output to. * @param {boolean} clear - Should the output be cleared before rendering to it + * @param {object} [currentState] - It's current state of filter. + * There are some useful properties in the currentState : + * target, filters, sourceFrame, destinationFrame, renderTarget, resolution */ - apply(filterManager, input, output, clear) + apply(filterManager, input, output, clear, currentState) // eslint-disable-line no-unused-vars { // do as you please! - filterManager.applyFilter(this, input, output, clear); + filterManager.applyFilter(this, input, output, clear, currentState); // or just do a regular render.. } } - -export default Filter; diff --git a/src/core/renderers/webgl/filters/extractUniformsFromSrc.js b/src/core/renderers/webgl/filters/extractUniformsFromSrc.js index d78a0b1..4893214 100644 --- a/src/core/renderers/webgl/filters/extractUniformsFromSrc.js +++ b/src/core/renderers/webgl/filters/extractUniformsFromSrc.js @@ -12,7 +12,7 @@ function extractUniformsFromString(string) { - const maskRegex = new RegExp('^(projectionMatrix|uSampler|filterArea)$'); + const maskRegex = new RegExp('^(projectionMatrix|uSampler|filterArea|filterClamp)$'); const uniforms = {}; let nameSplit; diff --git a/src/core/renderers/webgl/managers/FilterManager.js b/src/core/renderers/webgl/managers/FilterManager.js index 43fc869..6c7422d 100644 --- a/src/core/renderers/webgl/managers/FilterManager.js +++ b/src/core/renderers/webgl/managers/FilterManager.js @@ -2,6 +2,7 @@ import RenderTarget from '../utils/RenderTarget'; import Quad from '../utils/Quad'; import { Rectangle } from '../../../math'; +import Shader from '../../../Shader'; import * as filterTransforms from '../filters/filterTransforms'; import bitTwiddle from 'bit-twiddle'; @@ -22,8 +23,6 @@ this.filters = []; this.target = null; this.resolution = 1; - - this.firstRun = true; } } @@ -155,7 +154,7 @@ if (filters.length === 1) { - filters[0].apply(this, currentState.renderTarget, lastState.renderTarget, false); + filters[0].apply(this, currentState.renderTarget, lastState.renderTarget, false, currentState); this.freePotRenderTarget(currentState.renderTarget); } else @@ -177,7 +176,7 @@ for (i = 0; i < filters.length - 1; ++i) { - filters[i].apply(this, flip, flop, true); + filters[i].apply(this, flip, flop, true, currentState); const t = flip; @@ -185,7 +184,7 @@ flop = t; } - filters[i].apply(this, flip, lastState.renderTarget, false); + filters[i].apply(this, flip, lastState.renderTarget, false, currentState); this.freePotRenderTarget(flip); this.freePotRenderTarget(flop); @@ -212,16 +211,28 @@ const renderer = this.renderer; const gl = renderer.gl; - renderer.bindShader(filter, true); + let shader = filter.glShaders[renderer.CONTEXT_UID]; - // TODO theres a better way! - // quad can be a mesh and we then should be able to pull this off without accessing the shader directly - const shader = renderer.shaderManager.getGLShader(); - - // TODO change quad to mesh.. - if (!this.firstRun) + // cacheing.. + if (!shader) { - this.firstRun = true; + if (filter.glShaderKey) + { + shader = this.shaderCache[filter.glShaderKey]; + + if (!shader) + { + shader = new Shader(this.gl, filter.vertexSrc, filter.fragmentSrc); + + filter.glShaders[renderer.CONTEXT_UID] = this.shaderCache[filter.glShaderKey] = shader; + } + } + else + { + shader = filter.glShaders[renderer.CONTEXT_UID] = new Shader(this.gl, filter.vertexSrc, filter.fragmentSrc); + } + + // TODO - this only needs to be done once? renderer.bindVao(null); this.quad.initVao(shader); @@ -244,58 +255,146 @@ renderer.maskManager.pushScissorMask(null, renderer.maskManager.scissorData); } - // renderer._bindGLShader(shader); + renderer.bindShader(shader); - let currentState; - const uniforms = filter.uniforms; + // free unit 0 for us, doesn't matter what was there + // don't try to restore it, because syncUniforms can upload it to another slot + // and it'll be a problem + const tex = this.renderer.emptyTextures[0]; - if (uniforms.filterArea) - { - currentState = this.filterData.stack[this.filterData.index]; - - const filterArea = uniforms.filterArea; - - filterArea[0] = currentState.renderTarget.size.width; - filterArea[1] = currentState.renderTarget.size.height; - filterArea[2] = currentState.sourceFrame.x; - filterArea[3] = currentState.sourceFrame.y; - - filter.uniforms.filterArea = filterArea; - } - - // use this to clamp displaced texture coords so they belong to filterArea - // see displacementFilter fragment shader for an example - if (uniforms.filterClamp) - { - currentState = this.filterData.stack[this.filterData.index]; - - const filterClamp = uniforms.filterClamp; - - filterClamp[0] = 0; - filterClamp[1] = 0; - filterClamp[2] = (currentState.sourceFrame.width - 1) / currentState.renderTarget.size.width; - filterClamp[3] = (currentState.sourceFrame.height - 1) / currentState.renderTarget.size.height; - - filter.uniforms.filterClamp = filterClamp; - } - - renderer.shaderManager.setUniforms(filter.uniforms); + this.renderer.boundTextures[0] = tex; + // this syncs the pixi filters uniforms with glsl uniforms + this.syncUniforms(shader, filter); renderer.state.setBlendMode(filter.blendMode); - // temporary bypass cache.. - const tex = this.renderer.boundTextures[0]; - gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, input.texture.texture); this.quad.vao.draw(this.renderer.gl.TRIANGLES, 6, 0); - // restore cache. gl.bindTexture(gl.TEXTURE_2D, tex._glTextures[this.renderer.CONTEXT_UID].texture); } /** + * Uploads the uniforms of the filter. + * + * @param {GLShader} shader - The underlying gl shader. + * @param {PIXI.Filter} filter - The filter we are synchronizing. + */ + syncUniforms(shader, filter) + { + const uniformData = filter.uniformData; + const uniforms = filter.uniforms; + + // 0 is reserved for the pixi texture so we start at 1! + let textureCount = 1; + let currentState; + + // filterArea and filterClamp that are handled by FilterManager directly + // they must not appear in uniformData + + if (shader.uniforms.filterArea) + { + currentState = this.filterData.stack[this.filterData.index]; + + const filterArea = shader.uniforms.filterArea; + + filterArea[0] = currentState.renderTarget.size.width; + filterArea[1] = currentState.renderTarget.size.height; + filterArea[2] = currentState.sourceFrame.x; + filterArea[3] = currentState.sourceFrame.y; + + shader.uniforms.filterArea = filterArea; + } + + // use this to clamp displaced texture coords so they belong to filterArea + // see displacementFilter fragment shader for an example + if (shader.uniforms.filterClamp) + { + currentState = currentState || this.filterData.stack[this.filterData.index]; + + const filterClamp = shader.uniforms.filterClamp; + + filterClamp[0] = 0; + filterClamp[1] = 0; + filterClamp[2] = (currentState.sourceFrame.width - 1) / currentState.renderTarget.size.width; + filterClamp[3] = (currentState.sourceFrame.height - 1) / currentState.renderTarget.size.height; + + shader.uniforms.filterClamp = filterClamp; + } + + // TODO Cacheing layer.. + for (const i in uniformData) + { + if (uniformData[i].type === 'sampler2D' && uniforms[i] !== 0) + { + if (uniforms[i].baseTexture) + { + shader.uniforms[i] = this.renderer.bindTexture(uniforms[i].baseTexture, textureCount); + } + else + { + shader.uniforms[i] = textureCount; + + // TODO + // this is helpful as renderTargets can also be set. + // Although thinking about it, we could probably + // make the filter texture cache return a RenderTexture + // rather than a renderTarget + const gl = this.renderer.gl; + + this.renderer.boundTextures[textureCount] = this.renderer.emptyTextures[textureCount]; + gl.activeTexture(gl.TEXTURE0 + textureCount); + + uniforms[i].texture.bind(); + } + + textureCount++; + } + else if (uniformData[i].type === 'mat3') + { + // check if its pixi matrix.. + if (uniforms[i].a !== undefined) + { + shader.uniforms[i] = uniforms[i].toArray(true); + } + else + { + shader.uniforms[i] = uniforms[i]; + } + } + else if (uniformData[i].type === 'vec2') + { + // check if its a point.. + if (uniforms[i].x !== undefined) + { + const val = shader.uniforms[i] || new Float32Array(2); + + val[0] = uniforms[i].x; + val[1] = uniforms[i].y; + shader.uniforms[i] = val; + } + else + { + shader.uniforms[i] = uniforms[i]; + } + } + else if (uniformData[i].type === 'float') + { + if (shader.uniforms.data[i].value !== uniformData[i]) + { + shader.uniforms[i] = uniforms[i]; + } + } + else + { + shader.uniforms[i] = uniforms[i]; + } + } + } + + /** * Gets a render target from the pool, or creates a new one. * * @param {boolean} clear - Should we clear the render texture when we get it? @@ -442,7 +541,6 @@ renderTarget.resolution = resolution; renderTarget.defaultFrame.width = renderTarget.size.width = minWidth / resolution; renderTarget.defaultFrame.height = renderTarget.size.height = minHeight / resolution; - renderTarget.attachStencilBuffer(); return renderTarget; } diff --git a/src/core/settings.js b/src/core/settings.js index bb08cb0..f4dd087 100644 --- a/src/core/settings.js +++ b/src/core/settings.js @@ -77,11 +77,11 @@ * * @static * @memberof PIXI.settings - * @type {RegExp|string} + * @type {RegExp} * @example `@2x` - * @default /@(.+)x/ + * @default /@([0-9\.]+)x/ */ - RETINA_PREFIX: /@(.+)x/, + RETINA_PREFIX: /@([0-9\.]+)x/, /** * The default render options if none are supplied to {@link PIXI.WebGLRenderer} @@ -175,14 +175,24 @@ SCALE_MODE: 0, /** - * Default specify float precision in shaders. + * Default specify float precision in vertex shader. + * + * @static + * @memberof PIXI.settings + * @type {PIXI.PRECISION} + * @default PIXI.PRECISION.HIGH + */ + PRECISION_VERTEX: 'highp', + + /** + * Default specify float precision in fragment shader. * * @static * @memberof PIXI.settings * @type {PIXI.PRECISION} * @default PIXI.PRECISION.MEDIUM */ - PRECISION: 'mediump', + PRECISION_FRAGMENT: 'mediump', /** * Can we upload the same buffer in a single frame? diff --git a/src/core/sprites/Sprite.js b/src/core/sprites/Sprite.js index 28b0b18..121bf58 100644 --- a/src/core/sprites/Sprite.js +++ b/src/core/sprites/Sprite.js @@ -212,11 +212,11 @@ } else { - w0 = orig.width * (1 - anchor._x); - w1 = orig.width * -anchor._x; + w1 = -anchor._x * orig.width; + w0 = w1 + orig.width; - h0 = orig.height * (1 - anchor._y); - h1 = orig.height * -anchor._y; + h1 = -anchor._y * orig.height; + h0 = h1 + orig.height; } // xy @@ -269,11 +269,11 @@ const tx = wt.tx; const ty = wt.ty; - const w0 = (orig.width) * (1 - anchor._x); - const w1 = (orig.width) * -anchor._x; + const w1 = -anchor._x * orig.width; + const w0 = w1 + orig.width; - const h0 = orig.height * (1 - anchor._y); - const h1 = orig.height * -anchor._y; + const h1 = -anchor._y * orig.height; + const h0 = h1 + orig.height; // xy vertexData[0] = (a * w1) + (c * h1) + tx; @@ -440,7 +440,7 @@ * * @static * @param {number|string|PIXI.BaseTexture|HTMLCanvasElement|HTMLVideoElement} source Source to create texture from - * @return {PIXI.Sprite} The newly created texture + * @return {PIXI.Sprite} The newly created sprite */ static from(source) { @@ -538,8 +538,8 @@ } /** - * The tint applied to the sprite. This is a hex value. A value of - * 0xFFFFFF will remove any tint effect. + * The tint applied to the sprite. This is a hex value. + * A value of 0xFFFFFF will remove any tint effect. * * @member {number} * @default 0xFFFFFF diff --git a/src/core/sprites/canvas/CanvasTinter.js b/src/core/sprites/canvas/CanvasTinter.js index 1cf316b..27e5e0c 100644 --- a/src/core/sprites/canvas/CanvasTinter.js +++ b/src/core/sprites/canvas/CanvasTinter.js @@ -75,8 +75,8 @@ crop.width *= resolution; crop.height *= resolution; - canvas.width = crop.width; - canvas.height = crop.height; + canvas.width = Math.ceil(crop.width); + canvas.height = Math.ceil(crop.height); context.fillStyle = `#${(`00000${(color | 0).toString(16)}`).substr(-6)}`; @@ -130,8 +130,8 @@ crop.width *= resolution; crop.height *= resolution; - canvas.width = crop.width; - canvas.height = crop.height; + canvas.width = Math.ceil(crop.width); + canvas.height = Math.ceil(crop.height); context.globalCompositeOperation = 'copy'; context.fillStyle = `#${(`00000${(color | 0).toString(16)}`).substr(-6)}`; @@ -172,8 +172,8 @@ crop.width *= resolution; crop.height *= resolution; - canvas.width = crop.width; - canvas.height = crop.height; + canvas.width = Math.ceil(crop.width); + canvas.height = Math.ceil(crop.height); context.globalCompositeOperation = 'copy'; context.drawImage( diff --git a/src/core/sprites/webgl/SpriteRenderer.js b/src/core/sprites/webgl/SpriteRenderer.js index 2affcd5..7c352e2 100644 --- a/src/core/sprites/webgl/SpriteRenderer.js +++ b/src/core/sprites/webgl/SpriteRenderer.js @@ -30,7 +30,7 @@ /** * Number of values sent in the vertex buffer. - * positionX, positionY, colorR, colorG, colorB = 5 + * aVertexPosition(2), aTextureCoord(1), aColor(1), aTextureId(1) = 5 * * @member {number} */ @@ -75,7 +75,6 @@ this.shader = null; this.currentIndex = 0; - TICK = 0; this.groups = []; for (let k = 0; k < this.size; k++) diff --git a/src/core/textures/BaseTexture.js b/src/core/textures/BaseTexture.js index 853de7e..aeccf39 100644 --- a/src/core/textures/BaseTexture.js +++ b/src/core/textures/BaseTexture.js @@ -232,16 +232,24 @@ this.realWidth = this.source.naturalWidth || this.source.videoWidth || this.source.width; this.realHeight = this.source.naturalHeight || this.source.videoHeight || this.source.height; - this.width = this.realWidth / this.resolution; - this.height = this.realHeight / this.resolution; - - this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); + this._updateDimensions(); } this.emit('update', this); } /** + * Update dimensions from real values + */ + _updateDimensions() + { + this.width = this.realWidth / this.resolution; + this.height = this.realHeight / this.resolution; + + this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); + } + + /** * Load a source. * * If the source is not-immediately-available, such as an image that needs to be @@ -525,11 +533,7 @@ this.realWidth = Math.round(svgWidth * this.sourceScale); this.realHeight = Math.round(svgHeight * this.sourceScale); - this.width = this.realWidth / this.resolution; - this.height = this.realHeight / this.resolution; - - // Check pow2 after scale - this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); + this._updateDimensions(); // Create a canvas element const canvas = document.createElement('canvas'); @@ -690,4 +694,52 @@ return baseTexture; } + + /** + * Helper function that creates a base texture based on the source you provide. + * The source can be - image url, image element, canvas element. + * + * @static + * @param {string|HTMLImageElement|HTMLCanvasElement} source - The source to create base texture from. + * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values + * @param {number} [sourceScale=(auto)] - Scale for the original image, used with Svg images. + * @return {PIXI.BaseTexture} The new base texture. + */ + static from(source, scaleMode, sourceScale) + { + if (typeof source === 'string') + { + return BaseTexture.fromImage(source, undefined, scaleMode, sourceScale); + } + else if (source instanceof HTMLImageElement) + { + const imageUrl = source.src; + let baseTexture = BaseTextureCache[imageUrl]; + + if (!baseTexture) + { + baseTexture = new BaseTexture(source, scaleMode); + baseTexture.imageUrl = imageUrl; + + if (sourceScale) + { + baseTexture.sourceScale = sourceScale; + } + + // if there is an @2x at the end of the url we are going to assume its a highres image + baseTexture.resolution = getResolutionOfUrl(imageUrl); + + BaseTextureCache[imageUrl] = baseTexture; + } + + return baseTexture; + } + else if (source instanceof HTMLCanvasElement) + { + return BaseTexture.fromCanvas(source, scaleMode); + } + + // lets assume its a base texture! + return source; + } } diff --git a/src/core/textures/Spritesheet.js b/src/core/textures/Spritesheet.js new file mode 100644 index 0000000..d469c62 --- /dev/null +++ b/src/core/textures/Spritesheet.js @@ -0,0 +1,274 @@ +import { Rectangle, Texture } from '../'; +import { getResolutionOfUrl, TextureCache } from '../utils'; + +/** + * Utility class for maintaining reference to a collection + * of Textures on a single Spritesheet. + * + * @class + * @memberof PIXI + */ +export default class Spritesheet +{ + /** + * The maximum number of Textures to build per process. + * + * @type {number} + * @default 1000 + */ + static get BATCH_SIZE() + { + return 1000; + } + + /** + * @param {PIXI.BaseTexture} baseTexture Reference to the source BaseTexture object. + * @param {Object} data - Spritesheet image data. + * @param {string} [resolutionFilename] - The filename to consider when determining + * the resolution of the spritesheet. If not provided, the imageUrl will + * be used on the BaseTexture. + */ + constructor(baseTexture, data, resolutionFilename = null) + { + /** + * Reference to ths source texture + * @type {PIXI.BaseTexture} + */ + this.baseTexture = baseTexture; + + /** + * Map of spritesheet textures. + * @type {Object} + */ + this.textures = {}; + + /** + * Reference to the original JSON data. + * @type {Object} + */ + this.data = data; + + /** + * The resolution of the spritesheet. + * @type {number} + */ + this.resolution = this._updateResolution( + resolutionFilename || this.baseTexture.imageUrl + ); + + /** + * Map of spritesheet frames. + * @type {Object} + * @private + */ + this._frames = this.data.frames; + + /** + * Collection of frame names. + * @type {string[]} + * @private + */ + this._frameKeys = Object.keys(this._frames); + + /** + * Current batch index being processed. + * @type {number} + * @private + */ + this._batchIndex = 0; + + /** + * Callback when parse is completed. + * @type {Function} + * @private + */ + this._callback = null; + } + + /** + * Generate the resolution from the filename or fallback + * to the meta.scale field of the JSON data. + * + * @private + * @param {string} resolutionFilename - The filename to use for resolving + * the default resolution. + * @return {number} Resolution to use for spritesheet. + */ + _updateResolution(resolutionFilename) + { + const scale = this.data.meta.scale; + + // Use a defaultValue of `null` to check if a url-based resolution is set + let resolution = getResolutionOfUrl(resolutionFilename, null); + + // No resolution found via URL + if (resolution === null) + { + // Use the scale value or default to 1 + resolution = scale !== undefined ? parseFloat(scale) : 1; + } + + // For non-1 resolutions, update baseTexture + if (resolution !== 1) + { + this.baseTexture.resolution = resolution; + this.baseTexture.update(); + } + + return resolution; + } + + /** + * Parser spritesheet from loaded data. This is done asynchronously + * to prevent creating too many Texture within a single process. + * + * @param {Function} callback - Callback when complete returns + * a map of the Textures for this spritesheet. + */ + parse(callback) + { + this._batchIndex = 0; + this._callback = callback; + + if (this._frameKeys.length <= Spritesheet.BATCH_SIZE) + { + this._processFrames(0); + this._parseComplete(); + } + else + { + this._nextBatch(); + } + } + + /** + * Process a batch of frames + * + * @private + * @param {number} initialFrameIndex - The index of frame to start. + */ + _processFrames(initialFrameIndex) + { + let frameIndex = initialFrameIndex; + const maxFrames = Spritesheet.BATCH_SIZE; + + while (frameIndex - initialFrameIndex < maxFrames && frameIndex < this._frameKeys.length) + { + const i = this._frameKeys[frameIndex]; + const rect = this._frames[i].frame; + + if (rect) + { + let frame = null; + let trim = null; + const orig = new Rectangle( + 0, + 0, + this._frames[i].sourceSize.w / this.resolution, + this._frames[i].sourceSize.h / this.resolution + ); + + if (this._frames[i].rotated) + { + frame = new Rectangle( + rect.x / this.resolution, + rect.y / this.resolution, + rect.h / this.resolution, + rect.w / this.resolution + ); + } + else + { + frame = new Rectangle( + rect.x / this.resolution, + rect.y / this.resolution, + rect.w / this.resolution, + rect.h / this.resolution + ); + } + + // Check to see if the sprite is trimmed + if (this._frames[i].trimmed) + { + trim = new Rectangle( + this._frames[i].spriteSourceSize.x / this.resolution, + this._frames[i].spriteSourceSize.y / this.resolution, + rect.w / this.resolution, + rect.h / this.resolution + ); + } + + this.textures[i] = new Texture( + this.baseTexture, + frame, + orig, + trim, + this._frames[i].rotated ? 2 : 0 + ); + + // lets also add the frame to pixi's global cache for fromFrame and fromImage functions + TextureCache[i] = this.textures[i]; + } + + frameIndex++; + } + } + + /** + * The parse has completed. + * + * @private + */ + _parseComplete() + { + const callback = this._callback; + + this._callback = null; + this._batchIndex = 0; + callback.call(this, this.textures); + } + + /** + * Begin the next batch of textures. + * + * @private + */ + _nextBatch() + { + this._processFrames(this._batchIndex * Spritesheet.BATCH_SIZE); + this._batchIndex++; + setTimeout(() => + { + if (this._batchIndex * Spritesheet.BATCH_SIZE < this._frameKeys.length) + { + this._nextBatch(); + } + else + { + this._parseComplete(); + } + }, 0); + } + + /** + * Destroy Spritesheet and don't use after this. + * + * @param {boolean} [destroyBase=false] Whether to destroy the base texture as well + */ + destroy(destroyBase = false) + { + for (const i in this.textures) + { + this.textures[i].destroy(); + } + this._frames = null; + this._frameKeys = null; + this.data = null; + this.textures = null; + if (destroyBase) + { + this.baseTexture.destroy(); + } + this.baseTexture = null; + } +} diff --git a/src/core/textures/Texture.js b/src/core/textures/Texture.js index 6403d4f..afe4254 100644 --- a/src/core/textures/Texture.js +++ b/src/core/textures/Texture.js @@ -3,7 +3,7 @@ import TextureUvs from './TextureUvs'; import EventEmitter from 'eventemitter3'; import { Rectangle } from '../math'; -import { TextureCache, BaseTextureCache } from '../utils'; +import { TextureCache, BaseTextureCache, getResolutionOfUrl } from '../utils'; /** * A texture stores the information that represents an image or part of an image. It cannot be added @@ -365,7 +365,8 @@ * The source can be - frame id, image url, video url, canvas element, video element, base texture * * @static - * @param {number|string|PIXI.BaseTexture|HTMLCanvasElement|HTMLVideoElement} source - Source to create texture from + * @param {number|string|HTMLImageElement|HTMLCanvasElement|HTMLVideoElement|PIXI.BaseTexture} + * source - Source to create texture from * @return {PIXI.Texture} The newly created texture */ static from(source) @@ -393,7 +394,7 @@ } else if (source instanceof HTMLImageElement) { - return new Texture(new BaseTexture(source)); + return new Texture(BaseTexture.from(source)); } else if (source instanceof HTMLCanvasElement) { @@ -413,6 +414,43 @@ } /** + * Create a texture from a source and add to the cache. + * + * @static + * @param {HTMLImageElement|HTMLCanvasElement} source - The input source. + * @param {String} imageUrl - File name of texture, for cache and resolving resolution. + * @param {String} [name] - Human readible name for the texture cache. If no name is + * specified, only `imageUrl` will be used as the cache ID. + * @return {PIXI.Texture} Output texture + */ + static fromLoader(source, imageUrl, name) + { + const baseTexture = new BaseTexture(source, null, getResolutionOfUrl(imageUrl)); + const texture = new Texture(baseTexture); + + baseTexture.imageUrl = imageUrl; + + // No name, use imageUrl instead + if (!name) + { + name = imageUrl; + } + + // lets also add the frame to pixi's global cache for fromFrame and fromImage fucntions + BaseTextureCache[name] = baseTexture; + TextureCache[name] = texture; + + // also add references by url if they are different. + if (name !== imageUrl) + { + BaseTextureCache[imageUrl] = baseTexture; + TextureCache[imageUrl] = texture; + } + + return texture; + } + + /** * Adds a texture to the global TextureCache. This cache is shared across the whole PIXI object. * * @static @@ -522,6 +560,29 @@ } } +function createWhiteTexture() +{ + const canvas = document.createElement('canvas'); + + canvas.width = 10; + canvas.height = 10; + + const context = canvas.getContext('2d'); + + context.fillStyle = 'white'; + context.fillRect(0, 0, 10, 10); + + return new Texture(new BaseTexture(canvas)); +} + +function removeAllHandlers(tex) +{ + tex.destroy = function _emptyDestroy() { /* empty */ }; + tex.on = function _emptyOn() { /* empty */ }; + tex.once = function _emptyOnce() { /* empty */ }; + tex.emit = function _emptyEmit() { /* empty */ }; +} + /** * An empty texture, used often to not have to create multiple empty textures. * Can not be destroyed. @@ -530,7 +591,14 @@ * @constant */ Texture.EMPTY = new Texture(new BaseTexture()); -Texture.EMPTY.destroy = function _emptyDestroy() { /* empty */ }; -Texture.EMPTY.on = function _emptyOn() { /* empty */ }; -Texture.EMPTY.once = function _emptyOnce() { /* empty */ }; -Texture.EMPTY.emit = function _emptyEmit() { /* empty */ }; +removeAllHandlers(Texture.EMPTY); + +/** + * A white texture of 10x10 size, used for graphics and other things + * Can not be destroyed. + * + * @static + * @constant + */ +Texture.WHITE = createWhiteTexture(); +removeAllHandlers(Texture.WHITE); diff --git a/src/core/ticker/Ticker.js b/src/core/ticker/Ticker.js index d98a1f0..17a1517 100644 --- a/src/core/ticker/Ticker.js +++ b/src/core/ticker/Ticker.js @@ -67,11 +67,12 @@ * is based, this value is neither capped nor scaled. * If the platform supports DOMHighResTimeStamp, * this value will have a precision of 1 µs. + * Defaults to target frame time * * @member {number} - * @default 1 / TARGET_FPMS + * @default 16.66 */ - this.elapsedMS = 1 / settings.TARGET_FPMS; // default to target frame time + this.elapsedMS = 1 / settings.TARGET_FPMS; /** * The last time {@link PIXI.ticker.Ticker#update} was invoked. diff --git a/src/core/utils/index.js b/src/core/utils/index.js index 6ff609f..be44750 100644 --- a/src/core/utils/index.js +++ b/src/core/utils/index.js @@ -2,6 +2,7 @@ import settings from '../settings'; import EventEmitter from 'eventemitter3'; import pluginTarget from './pluginTarget'; +import * as mixins from './mixin'; import * as isMobile from 'ismobilejs'; let nextUid = 0; @@ -33,6 +34,7 @@ * @type {mixin} */ pluginTarget, + mixins, }; /** @@ -93,7 +95,7 @@ */ export function rgb2hex(rgb) { - return (((rgb[0] * 255) << 16) + ((rgb[1] * 255) << 8) + (rgb[2] * 255)); + return (((rgb[0] * 255) << 16) + ((rgb[1] * 255) << 8) + (rgb[2] * 255 | 0)); } /** @@ -368,3 +370,43 @@ * @private */ export const ProgramCache = {}; + +/** + * Destroys all texture in the cache + * + * @memberof PIXI.utils + * @function destroyTextureCache + */ +export function destroyTextureCache() +{ + let key; + + for (key in TextureCache) + { + TextureCache[key].destroy(); + } + for (key in BaseTextureCache) + { + BaseTextureCache[key].destroy(); + } +} + +/** + * Removes all textures from cache, but does not destroy them + * + * @memberof PIXI.utils + * @function clearTextureCache + */ +export function clearTextureCache() +{ + let key; + + for (key in TextureCache) + { + delete TextureCache[key]; + } + for (key in BaseTextureCache) + { + delete BaseTextureCache[key]; + } +} diff --git a/src/core/utils/mixin.js b/src/core/utils/mixin.js new file mode 100644 index 0000000..114de2a --- /dev/null +++ b/src/core/utils/mixin.js @@ -0,0 +1,59 @@ +/** + * Mixes all enumerable properties and methods from a source object to a target object. + * + * @memberof PIXI.utils.mixins + * @function mixin + * @param {object} target The prototype or instance that properties and methods should be added to. + * @param {object} source The source of properties and methods to mix in. + */ +export function mixin(target, source) +{ + if (!target || !source) return; + // in ES8/ES2017, this would be really easy: + // Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); + + // get all the enumerable property keys + const keys = Object.keys(source); + + // loop through properties + for (let i = 0; i < keys.length; ++i) + { + const propertyName = keys[i]; + + // Set the property using the property descriptor - this works for accessors and normal value properties + Object.defineProperty(target, propertyName, Object.getOwnPropertyDescriptor(source, propertyName)); + } +} + +const mixins = []; + +/** + * Queues a mixin to be handled towards the end of the initialization of PIXI, so that deprecation + * can take effect. + * + * @memberof PIXI.utils.mixins + * @function delayMixin + * @private + * @param {object} target The prototype or instance that properties and methods should be added to. + * @param {object} source The source of properties and methods to mix in. + */ +export function delayMixin(target, source) +{ + mixins.push(target, source); +} + +/** + * Handles all mixins queued via delayMixin(). + * + * @memberof PIXI.utils.mixins + * @function performMixins + * @private + */ +export function performMixins() +{ + for (let i = 0; i < mixins.length; i += 2) + { + mixin(mixins[i], mixins[i + 1]); + } + mixins.length = 0; +} diff --git a/src/deprecation.js b/src/deprecation.js index 045a19f..b9c73b3 100644 --- a/src/deprecation.js +++ b/src/deprecation.js @@ -5,6 +5,7 @@ import * as filters from './filters'; import * as prepare from './prepare'; import * as loaders from './loaders'; +import * as interaction from './interaction'; // provide method to give a stack track for warnings // useful for tracking-down where deprecated methods/properties/classes @@ -534,7 +535,7 @@ { parent: 'GC_MODES', target: 'GC_MODE' }, { parent: 'WRAP_MODES', target: 'WRAP_MODE' }, { parent: 'SCALE_MODES', target: 'SCALE_MODE' }, - { parent: 'PRECISION', target: 'PRECISION' }, + { parent: 'PRECISION', target: 'PRECISION_FRAGMENT' }, ]; for (let i = 0; i < defaults.length; i++) @@ -558,6 +559,32 @@ }); } +Object.defineProperties(core.settings, { + + /** + * @static + * @name PRECISION + * @memberof PIXI.settings + * @see PIXI.PRECISION + * @deprecated since version 4.4.0 + */ + PRECISION: { + enumerable: true, + get() + { + warn('PIXI.settings.PRECISION has been deprecated, please use PIXI.settings.PRECISION_FRAGMENT'); + + return core.settings.PRECISION_FRAGMENT; + }, + set(value) + { + warn('PIXI.settings.PRECISION has been deprecated, please use PIXI.settings.PRECISION_FRAGMENT'); + + core.settings.PRECISION_FRAGMENT = value; + }, + }, +}); + Object.defineProperties(extras, { /** @@ -694,7 +721,7 @@ warn('determineFontProperties is now deprecated, please use the static calculateFontProperties method, ' + 'e.g : Text.calculateFontProperties(fontStyle);'); - return Text.calculateFontProperties(fontStyle); + return core.Text.calculateFontProperties(fontStyle); }; Object.defineProperties(core.TextStyle.prototype, { @@ -1003,3 +1030,69 @@ }, }, }); + +/** + * @name PIXI.interaction.interactiveTarget#defaultCursor + * @static + * @type {number} + * @see PIXI.interaction.interactiveTarget#cursor + * @deprecated since 4.3.0 + */ +Object.defineProperty(interaction.interactiveTarget, 'defaultCursor', { + set(value) + { + warn('Property defaultCursor has been replaced with \'cursor\'. '); + this.cursor = value; + }, + get() + { + warn('Property defaultCursor has been replaced with \'cursor\'. '); + + return this.cursor; + }, + enumerable: true, +}); + +/** + * @name PIXI.interaction.InteractionManager#defaultCursorStyle + * @static + * @type {string} + * @see PIXI.interaction.InteractionManager#cursorStyles + * @deprecated since 4.3.0 + */ +Object.defineProperty(interaction.InteractionManager, 'defaultCursorStyle', { + set(value) + { + warn('Property defaultCursorStyle has been replaced with \'cursorStyles.default\'. '); + this.cursorStyles.default = value; + }, + get() + { + warn('Property defaultCursorStyle has been replaced with \'cursorStyles.default\'. '); + + return this.cursorStyles.default; + }, +}); + +/** + * @name PIXI.interaction.InteractionManager#currentCursorStyle + * @static + * @type {string} + * @see PIXI.interaction.InteractionManager#cursorStyles + * @deprecated since 4.3.0 + */ +Object.defineProperty(interaction.InteractionManager, 'currentCursorStyle', { + set(value) + { + warn('Property currentCursorStyle has been removed.' + + 'See the currentCursorMode property, which works differently.'); + this.currentCursorMode = value; + }, + get() + { + warn('Property currentCursorStyle has been removed.' + + 'See the currentCursorMode property, which works differently.'); + + return this.currentCursorMode; + }, +}); diff --git a/src/extras/BitmapText.js b/src/extras/BitmapText.js index fb6c343..0a0e150 100644 --- a/src/extras/BitmapText.js +++ b/src/extras/BitmapText.js @@ -455,6 +455,73 @@ return this._textHeight; } + + /** + * Register a bitmap font with data and a texture. + * + * @static + * @param {XMLDocument} xml - The XML document data. + * @param {PIXI.Texture} texture - Texture with all symbols. + * @return {Object} Result font object with font, size, lineHeight and char fields. + */ + static registerFont(xml, texture) + { + const data = {}; + const info = xml.getElementsByTagName('info')[0]; + const common = xml.getElementsByTagName('common')[0]; + + data.font = info.getAttribute('face'); + data.size = parseInt(info.getAttribute('size'), 10); + data.lineHeight = parseInt(common.getAttribute('lineHeight'), 10); + data.chars = {}; + + // parse letters + const letters = xml.getElementsByTagName('char'); + + for (let i = 0; i < letters.length; i++) + { + const letter = letters[i]; + const charCode = parseInt(letter.getAttribute('id'), 10); + + const textureRect = new core.Rectangle( + parseInt(letter.getAttribute('x'), 10) + texture.frame.x, + parseInt(letter.getAttribute('y'), 10) + texture.frame.y, + parseInt(letter.getAttribute('width'), 10), + parseInt(letter.getAttribute('height'), 10) + ); + + data.chars[charCode] = { + xOffset: parseInt(letter.getAttribute('xoffset'), 10), + yOffset: parseInt(letter.getAttribute('yoffset'), 10), + xAdvance: parseInt(letter.getAttribute('xadvance'), 10), + kerning: {}, + texture: new core.Texture(texture.baseTexture, textureRect), + + }; + } + + // parse kernings + const kernings = xml.getElementsByTagName('kerning'); + + for (let i = 0; i < kernings.length; i++) + { + const kerning = kernings[i]; + const first = parseInt(kerning.getAttribute('first'), 10); + const second = parseInt(kerning.getAttribute('second'), 10); + const amount = parseInt(kerning.getAttribute('amount'), 10); + + if (data.chars[second]) + { + data.chars[second].kerning[first] = amount; + } + } + + // I'm leaving this as a temporary fix so we can test the bitmap fonts in v3 + // but it's very likely to change + BitmapText.fonts[data.font] = data; + + return data; + } } BitmapText.fonts = {}; diff --git a/src/index.js b/src/index.js index 59356cf..c553427 100644 --- a/src/index.js +++ b/src/index.js @@ -16,6 +16,10 @@ import * as particles from './particles'; import * as prepare from './prepare'; +// handle mixins now, after all code has been added, including deprecation +import { utils } from './core'; +utils.mixins.performMixins(); + export { accessibility, extract, diff --git a/src/interaction/InteractionManager.js b/src/interaction/InteractionManager.js index 7d51134..8baf13a 100644 --- a/src/interaction/InteractionManager.js +++ b/src/interaction/InteractionManager.js @@ -1,18 +1,20 @@ 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'; -// Mix interactiveTarget into core.DisplayObject.prototype -Object.assign( +// Mix interactiveTarget into core.DisplayObject.prototype, after deprecation has been handled +core.utils.mixins.delayMixin( core.DisplayObject.prototype, 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 +70,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 +101,6 @@ this.eventData = new InteractionEvent(); /** - * Tiny little interactiveData pool ! - * - * @member {PIXI.interaction.InteractionData[]} - */ - this.interactiveDataPool = []; - - /** * The DOM element to bind to. * * @private @@ -153,66 +155,12 @@ */ this.supportsPointerEvents = !!window.PointerEvent; - /** - * Are touch events being 'normalized' and converted into pointer events if pointer events are not supported - * For example, on a touch screen mobile device, a touchstart would also be emitted as a pointerdown - * - * @private - * @readonly - * @member {boolean} - */ - this.normalizeTouchEvents = !this.supportsPointerEvents && this.supportsTouchEvents; - - /** - * Are mouse events being 'normalized' and converted into pointer events if pointer events are not supported - * For example, on a desktop pc, a mousedown would also be emitted as a pointerdown - * - * @private - * @readonly - * @member {boolean} - */ - this.normalizeMouseEvents = !this.supportsPointerEvents && !MobileDevice.any; - // this will make it so that you don't have to call bind all the time /** * @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 +168,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,41 +199,32 @@ this.onPointerOver = this.onPointerOver.bind(this); /** - * @private - * @member {Function} + * Dictionary of how different cursor modes are handled. Strings are handled as CSS cursor + * values, objects are handled as dictionaries of CSS values for interactionDOMElement, + * and functions are called instead of changing the CSS. + * Default CSS cursor values are provided for 'default' and 'pointer' modes. + * @member {Object.)>} */ - this.onTouchStart = this.onTouchStart.bind(this); - this.processTouchStart = this.processTouchStart.bind(this); + this.cursorStyles = { + default: 'inherit', + pointer: 'pointer', + }; /** - * @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. - * - * @member {string} - * @default 'inherit' - */ - this.defaultCursorStyle = 'inherit'; - - /** - * The css style of the cursor that is being used. + * The mode of the cursor that is being used. + * The value of this is a key from the cursorStyles dictionary. * * @member {string} */ - this.currentCursorStyle = 'inherit'; + this.currentCursorMode = null; + + /** + * Internal cached let. + * + * @private + * @member {string} + */ + this.cursor = null; /** * Internal cached let. @@ -303,6 +249,7 @@ * object. * * @event mousedown + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -311,6 +258,7 @@ * on the display object. * * @event rightdown + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -319,6 +267,7 @@ * object. * * @event mouseup + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -327,6 +276,7 @@ * over the display object. * * @event rightup + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -335,6 +285,7 @@ * the display object. * * @event click + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -343,6 +294,7 @@ * and released on the display object. * * @event rightclick + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -352,6 +304,7 @@ * [mousedown]{@link PIXI.interaction.InteractionManager#event:mousedown}. * * @event mouseupoutside + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -361,6 +314,7 @@ * [rightdown]{@link PIXI.interaction.InteractionManager#event:rightdown}. * * @event rightupoutside + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -368,6 +322,7 @@ * Fired when a pointer device (usually a mouse) is moved while over the display object * * @event mousemove + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -375,6 +330,7 @@ * Fired when a pointer device (usually a mouse) is moved onto the display object * * @event mouseover + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -382,6 +338,7 @@ * Fired when a pointer device (usually a mouse) is moved off the display object * * @event mouseout + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -389,6 +346,7 @@ * Fired when a pointer device button is pressed on the display object. * * @event pointerdown + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -396,6 +354,14 @@ * Fired when a pointer device button is released over the display object. * * @event pointerup + * @type {PIXI.interaction.InteractionData} + * @memberof PIXI.interaction.InteractionManager# + */ + + /** + * Fired when the operating system cancels a pointer event + * + * @event pointercancel * @memberof PIXI.interaction.InteractionManager# */ @@ -403,6 +369,7 @@ * Fired when a pointer device button is pressed and released on the display object. * * @event pointertap + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -411,6 +378,7 @@ * registered a [pointerdown]{@link PIXI.interaction.InteractionManager#event:pointerdown}. * * @event pointerupoutside + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -418,6 +386,7 @@ * Fired when a pointer device is moved while over the display object * * @event pointermove + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -425,6 +394,7 @@ * Fired when a pointer device is moved onto the display object * * @event pointerover + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -432,6 +402,7 @@ * Fired when a pointer device is moved off the display object * * @event pointerout + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -439,6 +410,7 @@ * Fired when a touch point is placed on the display object. * * @event touchstart + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -446,6 +418,14 @@ * Fired when a touch point is removed from the display object. * * @event touchend + * @type {PIXI.interaction.InteractionData} + * @memberof PIXI.interaction.InteractionManager# + */ + + /** + * Fired when the operating system cancels a touch + * + * @event touchcancel * @memberof PIXI.interaction.InteractionManager# */ @@ -453,6 +433,7 @@ * Fired when a touch point is placed and removed from the display object. * * @event tap + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -461,6 +442,7 @@ * registered a [touchstart]{@link PIXI.interaction.InteractionManager#event:touchstart}. * * @event touchendoutside + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -468,6 +450,7 @@ * Fired when a touch point is moved along the display object. * * @event touchmove + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ } @@ -524,45 +507,30 @@ { window.document.addEventListener('pointermove', this.onPointerMove, true); this.interactionDOMElement.addEventListener('pointerdown', this.onPointerDown, true); - this.interactionDOMElement.addEventListener('pointerout', this.onPointerOut, true); + // pointerout is fired in addition to pointerup (for touch events) and pointercancel + // we already handle those, so for the purposes of what we do in onPointerOut, we only + // care about the pointerleave event + this.interactionDOMElement.addEventListener('pointerleave', 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; @@ -596,45 +564,26 @@ { window.document.removeEventListener('pointermove', this.onPointerMove, true); this.interactionDOMElement.removeEventListener('pointerdown', this.onPointerDown, true); - this.interactionDOMElement.removeEventListener('pointerout', this.onPointerOut, true); + this.interactionDOMElement.removeEventListener('pointerleave', 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) + 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.onPointerDown, true); + this.interactionDOMElement.removeEventListener('touchcancel', this.onPointerCancel, 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); - - 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 = null; @@ -664,7 +613,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; @@ -672,25 +621,80 @@ return; } - this.cursor = this.defaultCursorStyle; + this.cursor = null; // 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(); - - this.processInteractive(this.mouse.global, this.renderer._lastObjectRendered, this.processMouseOverOut, true); - - if (this.currentCursorStyle !== this.cursor) + for (const k in this.activeInteractionData) { - this.currentCursorStyle = this.cursor; - this.interactionDOMElement.style.cursor = this.cursor; + // eslint-disable-next-line no-prototype-builtins + if (this.activeInteractionData.hasOwnProperty(k)) + { + const interactionData = this.activeInteractionData[k]; + + if (interactionData.originalEvent && interactionData.pointerType !== 'touch') + { + const interactionEvent = this.configureInteractionEventForDOMEvent( + this.eventData, + interactionData.originalEvent, + interactionData + ); + + this.processInteractive( + interactionEvent, + this.renderer._lastObjectRendered, + this.processPointerOverOut, + true + ); + } + } } + this.setCursorMode(this.cursor); + // TODO } /** + * Sets the current cursor mode, handling any callbacks or CSS style changes. + * + * @param {string} mode - cursor mode, a key from the cursorStyles dictionary + */ + setCursorMode(mode) + { + mode = mode || 'default'; + // if the mode didn't actually change, bail early + if (this.currentCursorMode === mode) + { + return; + } + this.currentCursorMode = mode; + const style = this.cursorStyles[mode]; + + // only do things if there is a cursor style for it + if (style) + { + switch (typeof style) + { + case 'string': + // string styles are handled as cursor CSS + this.interactionDOMElement.style.cursor = style; + break; + case 'function': + // functions are just called, and passed the cursor mode + style(mode); + break; + case 'object': + // if it is an object, assume that it is a dictionary of CSS styles, + // apply it to the interactionDOMElement + Object.assign(this.interactionDOMElement.style, style); + break; + } + } + } + + /** * Dispatches an event on the display object that was interacted with * * @param {PIXI.Container|PIXI.Sprite|PIXI.extras.TilingSprite} displayObject - the display object in question @@ -748,22 +752,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 @@ -787,9 +795,8 @@ { interactiveParent = false; } - - // it has a mask! Then lets hit test that before continuing.. - if (hitTest && displayObject._mask) + // it has a mask! Then lets hit test that before continuing + else if (hitTest && displayObject._mask) { if (!displayObject._mask.containsPoint(point)) { @@ -797,14 +804,7 @@ } } - // it has a filterArea! Same as mask but easier, its a rectangle - if (hitTest && displayObject.filterArea) - { - if (!displayObject.filterArea.contains(point.x, point.y)) - { - hitTest = false; - } - } + let keepHitTestingAfterChildren = hitTest; // ** FREE TIP **! If an object is not interactive or has no buttons in it // (such as a game scene!) set interactiveChildren to false for that displayObject. @@ -818,7 +818,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 @@ -838,15 +838,20 @@ // This means we no longer need to hit test anything else. We still need to run // through all objects, but we don't need to perform any hit tests. - // { - hitTest = false; - // } + keepHitTestingAfterChildren = false; + + if (child.interactive) + { + hitTest = false; + } // we can break now as we have hit an object. } } } + hitTest = keepHitTestingAfterChildren; + // no point running this if the item is not interactive or does not have an interactive parent. if (interactive) { @@ -868,14 +873,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 +886,175 @@ } /** - * 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].isNormalized) { - 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); + if (!displayObject.trackedPointers[id]) + { + displayObject.trackedPointers[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.trackedPointers[id].rightDown = true; + } + else + { + displayObject.trackedPointers[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.trackedPointers[id] !== undefined) + { + delete displayObject.trackedPointers[id]; + this.dispatchEvent(displayObject, 'pointercancel', interactionEvent); + + if (e.type === 'touchcancel' || e.pointerType === 'touch') + { + this.dispatchEvent(displayObject, 'touchcancel', interactionEvent); + } } } @@ -1159,42 +1066,94 @@ */ 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) { - if (hit) - { - this.dispatchEvent(displayObject, 'pointerup', this.eventData); + const e = interactionEvent.data.originalEvent; - if (displayObject._pointerDown) + const id = interactionEvent.data.identifier; + + const trackingData = displayObject.trackedPointers[id]; + + const isTouch = (e.type === 'touchend' || e.pointerType === 'touch'); + + const isMouse = (e.type.indexOf('mouse') === 0 || e.pointerType === 'mouse'); + + // 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) { - displayObject._pointerDown = false; - this.dispatchEvent(displayObject, 'pointertap', this.eventData); + 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); + } + // update the down state of the tracking data + if (trackingData) + { + if (isRightButton) + { + trackingData.rightDown = hit; + } + else + { + trackingData.leftDown = hit; + } } } - else if (displayObject._pointerDown) + + // Pointers and Touches, and Mouse + if (hit) { - displayObject._pointerDown = false; - this.dispatchEvent(displayObject, 'pointerupoutside', this.eventData); + this.dispatchEvent(displayObject, 'pointerup', interactionEvent); + if (isTouch) this.dispatchEvent(displayObject, 'touchend', interactionEvent); + + if (trackingData) + { + this.dispatchEvent(displayObject, 'pointertap', interactionEvent); + if (isTouch) + { + this.dispatchEvent(displayObject, 'tap', interactionEvent); + // touches are no longer over (if they ever were) when we get the touchend + // so we should ensure that we don't keep pretending that they are + trackingData.over = false; + } + } + } + else if (trackingData) + { + this.dispatchEvent(displayObject, 'pointerupoutside', interactionEvent); + if (isTouch) this.dispatchEvent(displayObject, 'touchendoutside', interactionEvent); + } + // Only remove the tracking data if there is no over/down state still associated with it + if (trackingData && trackingData.none) + { + delete displayObject.trackedPointers[id]; } } @@ -1202,39 +1161,78 @@ * 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 = null; + } - 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') + { + this.setCursorMode(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 (isMouse) { - 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 +1240,94 @@ * 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.setCursorMode(null); + } - 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'); + + let trackingData = displayObject.trackedPointers[id]; + + // if we just moused over the display object, then we need to track that state + if (hit && !trackingData) + { + trackingData = displayObject.trackedPointers[id] = new InteractionTrackingData(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); + } + } + + // only change the cursor if it has not already been changed (by something deeper in the + // display tree) + if (isMouse && this.cursor === null) + { + this.cursor = displayObject.cursor; } } - 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); + } + // if there is no mouse down information for the pointer, then it is safe to delete + if (trackingData.none) + { + delete displayObject.trackedPointers[id]; + } } } @@ -1287,253 +1335,154 @@ * 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 && pointerEvent.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; + + // mark the touch as normalized, just so that we know we did it + touch.isNormalized = true; + + normalizedEvents.push(touch); + } } - else if (this.normalizeMouseEvents) + // apparently PointerEvent subclasses MouseEvent, so yay + else if (event instanceof MouseEvent && (!this.supportsPointerEvents || !(event instanceof window.PointerEvent))) { if (typeof event.isPrimary === 'undefined') event.isPrimary = true; if (typeof event.width === 'undefined') event.width = 1; @@ -1541,10 +1490,21 @@ 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; + + // mark the mouse event as normalized, just so that we know we did it + event.isNormalized = true; + + normalizedEvents.push(event); } + else + { + normalizedEvents.push(event); + } + + return normalizedEvents; } /** @@ -1563,30 +1523,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 +1542,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..6a1f69e --- /dev/null +++ b/src/interaction/InteractionTrackingData.js @@ -0,0 +1,147 @@ +/** + * 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 inactive (not over or down)? + * + * @member {number} + * @memberof PIXI.interaction.InteractionTrackingData# + */ + get none() + { + return this._flags === this.constructor.FLAGS.NONE; + } + + /** + * 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..d252fba 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 * ); */ @@ -38,68 +38,56 @@ hitArea: null, /** - * If enabled, the mouse cursor will change when hovered over the displayObject if it is interactive + * If enabled, the mouse cursor use the pointer behavior when hovered over the displayObject if it is interactive + * Setting this changes the 'cursor' property to `'pointer'`. * - * @inner {boolean} + * @member {boolean} + * @memberof PIXI.interaction.interactiveTarget# */ - buttonMode: false, + get buttonMode() + { + return this.cursor === 'pointer'; + }, + set buttonMode(value) + { + if (value) + { + this.cursor = 'pointer'; + } + else if (this.cursor === 'pointer') + { + this.cursor = null; + } + }, /** - * If buttonMode is enabled, this defines what CSS cursor property is used when the mouse cursor - * is hovered over the displayObject + * This defines what cursor mode is used when the mouse cursor + * is hovered over the displayObject. * * @see https://developer.mozilla.org/en/docs/Web/CSS/cursor * * @inner {string} */ - defaultCursor: 'pointer', - - // some internal checks.. - /** - * Internal check to detect if the mouse cursor is hovered over the displayObject - * - * @inner {boolean} - * @private - */ - _over: false, + cursor: null, /** - * Internal check to detect if the left mouse button is pressed on the displayObject + * Internal set of all active pointers, by identifier * - * @inner {boolean} + * @member {Map} + * @memberof PIXI.interaction.interactiveTarget# * @private */ - _isLeftDown: false, + get trackedPointers() + { + if (this._trackedPointers === undefined) this._trackedPointers = {}; + + return this._trackedPointers; + }, /** - * Internal check to detect if the right mouse button is pressed on the displayObject + * Map of all tracked pointers, by identifier. Use trackedPointers to access. * - * @inner {boolean} - * @private + * @private {Map} */ - _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, + _trackedPointers: undefined, }; diff --git a/src/loaders/bitmapFontParser.js b/src/loaders/bitmapFontParser.js index cc2618c..f4cf996 100644 --- a/src/loaders/bitmapFontParser.js +++ b/src/loaders/bitmapFontParser.js @@ -1,63 +1,19 @@ import * as path from 'path'; -import { Rectangle, Texture, utils } from '../core'; +import { utils } from '../core'; import { Resource } from 'resource-loader'; import { BitmapText } from '../extras'; +/** + * Register a BitmapText font from loader resource. + * + * @function parseBitmapFontData + * @memberof PIXI.loaders + * @param {PIXI.loaders.Resource} resource - Loader resource. + * @param {PIXI.Texture} texture - Reference to texture. + */ export function parse(resource, texture) { - const data = {}; - const info = resource.data.getElementsByTagName('info')[0]; - const common = resource.data.getElementsByTagName('common')[0]; - - data.font = info.getAttribute('face'); - data.size = parseInt(info.getAttribute('size'), 10); - data.lineHeight = parseInt(common.getAttribute('lineHeight'), 10); - data.chars = {}; - - // parse letters - const letters = resource.data.getElementsByTagName('char'); - - for (let i = 0; i < letters.length; i++) - { - const charCode = parseInt(letters[i].getAttribute('id'), 10); - - const textureRect = new Rectangle( - parseInt(letters[i].getAttribute('x'), 10) + texture.frame.x, - parseInt(letters[i].getAttribute('y'), 10) + texture.frame.y, - parseInt(letters[i].getAttribute('width'), 10), - parseInt(letters[i].getAttribute('height'), 10) - ); - - data.chars[charCode] = { - xOffset: parseInt(letters[i].getAttribute('xoffset'), 10), - yOffset: parseInt(letters[i].getAttribute('yoffset'), 10), - xAdvance: parseInt(letters[i].getAttribute('xadvance'), 10), - kerning: {}, - texture: new Texture(texture.baseTexture, textureRect), - - }; - } - - // parse kernings - const kernings = resource.data.getElementsByTagName('kerning'); - - for (let i = 0; i < kernings.length; i++) - { - const first = parseInt(kernings[i].getAttribute('first'), 10); - const second = parseInt(kernings[i].getAttribute('second'), 10); - const amount = parseInt(kernings[i].getAttribute('amount'), 10); - - if (data.chars[second]) - { - data.chars[second].kerning[first] = amount; - } - } - - resource.bitmapFont = data; - - // I'm leaving this as a temporary fix so we can test the bitmap fonts in v3 - // but it's very likely to change - BitmapText.fonts[data.font] = data; + resource.bitmapFont = BitmapText.registerFont(resource.data, texture); } export default function () diff --git a/src/loaders/index.js b/src/loaders/index.js index 8942436..5189793 100644 --- a/src/loaders/index.js +++ b/src/loaders/index.js @@ -5,4 +5,11 @@ export { default as bitmapFontParser, parse as parseBitmapFontData } from './bitmapFontParser'; export { default as spritesheetParser } from './spritesheetParser'; export { default as textureParser } from './textureParser'; + +/** + * Reference to **resource-loader**'s Resource class. + * See https://github.com/englercj/resource-loader + * @class Resource + * @memberof PIXI.loaders + */ export { Resource } from 'resource-loader'; diff --git a/src/loaders/loader.js b/src/loaders/loader.js index 0a670cb..767a411 100644 --- a/src/loaders/loader.js +++ b/src/loaders/loader.js @@ -7,20 +7,47 @@ /** * - * The new loader, extends Resource Loader by Chad Engler : https://github.com/englercj/resource-loader + * The new loader, extends Resource Loader by Chad Engler: https://github.com/englercj/resource-loader * * ```js - * let loader = PIXI.loader; // pixi exposes a premade instance for you to use. + * const loader = PIXI.loader; // pixi exposes a premade instance for you to use. * //or - * let loader = new PIXI.loaders.Loader(); // you can also create your own if you want + * const loader = new PIXI.loaders.Loader(); // you can also create your own if you want * - * loader.add('bunny', 'data/bunny.png'); - * loader.add('spaceship', 'assets/spritesheet.json'); + * const sprites = {}; + * + * // Chainable `add` to enqueue a resource + * loader.add('bunny', 'data/bunny.png') + * .add('spaceship', 'assets/spritesheet.json'); * loader.add('scoreFont', 'assets/score.fnt'); * - * loader.once('complete',onAssetsLoaded); + * // Chainable `pre` to add a middleware that runs for each resource, *before* loading that resource. + * // This is useful to implement custom caching modules (using filesystem, indexeddb, memory, etc). + * loader.pre(cachingMiddleware); * - * loader.load(); + * // Chainable `use` to add a middleware that runs for each resource, *after* loading that resource. + * // This is useful to implement custom parsing modules (like spritesheet parsers, spine parser, etc). + * loader.use(parsingMiddleware); + * + * // The `load` method loads the queue of resources, and calls the passed in callback called once all + * // resources have loaded. + * loader.load((loader, resources) => { + * // resources is an object where the key is the name of the resource loaded and the value is the resource object. + * // They have a couple default properties: + * // - `url`: The URL that the resource was loaded from + * // - `error`: The error that happened when trying to load (if any) + * // - `data`: The raw data that was loaded + * // also may contain other properties based on the middleware that runs. + * sprites.bunny = new PIXI.TilingSprite(resources.bunny.texture); + * sprites.spaceship = new PIXI.TilingSprite(resources.spaceship.texture); + * sprites.scoreFont = new PIXI.TilingSprite(resources.scoreFont.texture); + * }); + * + * // throughout the process multiple signals can be dispatched. + * loader.onProgress.add(() => {}); // called once per loaded/errored file + * loader.onError.add(() => {}); // called once per errored file + * loader.onLoad.add(() => {}); // called once per loaded file + * loader.onComplete.add(() => {}); // called once when the queued resources all load. * ``` * * @see https://github.com/englercj/resource-loader diff --git a/src/loaders/spritesheetParser.js b/src/loaders/spritesheetParser.js index 7570332..e57dfb2 100644 --- a/src/loaders/spritesheetParser.js +++ b/src/loaders/spritesheetParser.js @@ -1,8 +1,6 @@ import { Resource } from 'resource-loader'; import path from 'path'; -import * as core from '../core'; - -const BATCH_SIZE = 1000; +import { Spritesheet } from '../core'; export default function () { @@ -43,134 +41,18 @@ // load the image for this sheet this.add(imageResourceName, resourcePath, loadOptions, function onImageLoad(res) { - resource.textures = {}; + const spritesheet = new Spritesheet( + res.texture.baseTexture, + resource.data, + resource.url + ); - const frames = resource.data.frames; - const frameKeys = Object.keys(frames); - const baseTexture = res.texture.baseTexture; - const scale = resource.data.meta.scale; - - // Use a defaultValue of `null` to check if a url-based resolution is set - let resolution = core.utils.getResolutionOfUrl(resource.url, null); - - // No resolution found via URL - if (resolution === null) + spritesheet.parse(() => { - // Use the scale value or default to 1 - resolution = scale !== undefined ? scale : 1; - } - - // For non-1 resolutions, update baseTexture - if (resolution !== 1) - { - baseTexture.resolution = resolution; - baseTexture.update(); - } - - let batchIndex = 0; - - function processFrames(initialFrameIndex, maxFrames) - { - let frameIndex = initialFrameIndex; - - while (frameIndex - initialFrameIndex < maxFrames && frameIndex < frameKeys.length) - { - const i = frameKeys[frameIndex]; - const rect = frames[i].frame; - - if (rect) - { - let frame = null; - let trim = null; - const orig = new core.Rectangle( - 0, - 0, - frames[i].sourceSize.w / resolution, - frames[i].sourceSize.h / resolution - ); - - if (frames[i].rotated) - { - frame = new core.Rectangle( - rect.x / resolution, - rect.y / resolution, - rect.h / resolution, - rect.w / resolution - ); - } - else - { - frame = new core.Rectangle( - rect.x / resolution, - rect.y / resolution, - rect.w / resolution, - rect.h / resolution - ); - } - - // Check to see if the sprite is trimmed - if (frames[i].trimmed) - { - trim = new core.Rectangle( - frames[i].spriteSourceSize.x / resolution, - frames[i].spriteSourceSize.y / resolution, - rect.w / resolution, - rect.h / resolution - ); - } - - resource.textures[i] = new core.Texture( - baseTexture, - frame, - orig, - trim, - frames[i].rotated ? 2 : 0 - ); - - // lets also add the frame to pixi's global cache for fromFrame and fromImage functions - core.utils.TextureCache[i] = resource.textures[i]; - } - - frameIndex++; - } - } - - function shouldProcessNextBatch() - { - return batchIndex * BATCH_SIZE < frameKeys.length; - } - - function processNextBatch(done) - { - processFrames(batchIndex * BATCH_SIZE, BATCH_SIZE); - batchIndex++; - setTimeout(done, 0); - } - - function iteration() - { - processNextBatch(() => - { - if (shouldProcessNextBatch()) - { - iteration(); - } - else - { - next(); - } - }); - } - - if (frameKeys.length <= BATCH_SIZE) - { - processFrames(0, BATCH_SIZE); + resource.spritesheet = spritesheet; + resource.textures = spritesheet.textures; next(); - } - else - { - iteration(); - } + }); }); }; } diff --git a/package.json b/package.json index 6a366ba..71ade15 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pixi.js", - "version": "4.3.4", + "version": "4.3.5", "description": "Pixi.js is a fast lightweight 2D library that works across all devices.", "author": "Mat Groves", "contributors": [ @@ -61,7 +61,7 @@ "ismobilejs": "^0.4.0", "object-assign": "^4.0.1", "pixi-gl-core": "^1.0.3", - "resource-loader": "^2.0.4" + "resource-loader": "^2.0.6" }, "devDependencies": { "babel-cli": "^6.18.0", @@ -70,9 +70,9 @@ "babel-preset-es2015": "^6.14.0", "babelify": "^7.3.0", "del": "^2.2.0", - "electron-prebuilt": "^1.3.2", + "electron": "^1.4.15", "eslint": "^3.5.0", - "floss": "^1.2.0", + "floss": "^2.0.1", "gh-pages": "^0.11.0", "jaguarjs-jsdoc": "^1.0.1", "js-md5": "^0.4.1", diff --git a/src/accessibility/AccessibilityManager.js b/src/accessibility/AccessibilityManager.js index df2716a..f16bcb1 100644 --- a/src/accessibility/AccessibilityManager.js +++ b/src/accessibility/AccessibilityManager.js @@ -3,7 +3,7 @@ import accessibleTarget from './accessibleTarget'; // add some extra variables to the container.. -Object.assign( +core.utils.mixins.delayMixin( core.DisplayObject.prototype, accessibleTarget ); diff --git a/src/core/Application.js b/src/core/Application.js index ea0f058..5340578 100644 --- a/src/core/Application.js +++ b/src/core/Application.js @@ -1,6 +1,6 @@ import { autoDetectRenderer } from './autoDetectRenderer'; import Container from './display/Container'; -import Ticker from './ticker/Ticker'; +import { shared, Ticker } from './ticker'; /** * Convenience class to create a new PIXI application. @@ -35,9 +35,9 @@ * @param {boolean} [noWebGL=false] - prevents selection of WebGL renderer, even if such is present * @param {boolean} [options.legacy=false] - If true Pixi will aim to ensure compatibility * with older / less advanced devices. If you experience unexplained flickering try setting this to true. - + * @param {boolean} [useSharedTicker=false] - `true` to use PIXI.ticker.shared, `false` to create new ticker. */ - constructor(width, height, options, noWebGL) + constructor(width, height, options, noWebGL, useSharedTicker = false) { /** * WebGL renderer if available, otherwise CanvasRenderer @@ -46,23 +46,46 @@ this.renderer = autoDetectRenderer(width, height, options, noWebGL); /** - * The root display container that's renderered. + * The root display container that's rendered. * @member {PIXI.Container} */ this.stage = new Container(); /** + * Internal reference to the ticker + * @member {PIXI.ticker.Ticker} + * @private + */ + this._ticker = null; + + /** * Ticker for doing render updates. * @member {PIXI.ticker.Ticker} + * @default PIXI.ticker.shared */ - this.ticker = new Ticker(); - - this.ticker.add(this.render, this); + this.ticker = useSharedTicker ? shared : new Ticker(); // Start the rendering this.start(); } + set ticker(ticker) // eslint-disable-line require-jsdoc + { + if (this._ticker) + { + this._ticker.remove(this.render, this); + } + this._ticker = ticker; + if (ticker) + { + ticker.add(this.render, this); + } + } + get ticker() // eslint-disable-line require-jsdoc + { + return this._ticker; + } + /** * Render the current stage. */ @@ -76,7 +99,7 @@ */ stop() { - this.ticker.stop(); + this._ticker.stop(); } /** @@ -84,7 +107,7 @@ */ start() { - this.ticker.start(); + this._ticker.start(); } /** @@ -98,13 +121,22 @@ } /** + * Reference to the renderer's screen rectangle. Its safe to use as filterArea or hitArea for whole screen + * @member {PIXI.Rectangle} + * @readonly + */ + get screen() + { + return this.renderer.screen; + } + + /** * Destroy and don't use after this. * @param {Boolean} [removeView=false] Automatically remove canvas from DOM. */ destroy(removeView) { this.stop(); - this.ticker.remove(this.render, this); this.ticker = null; this.stage.destroy(); diff --git a/src/core/Shader.js b/src/core/Shader.js new file mode 100644 index 0000000..78b17e6 --- /dev/null +++ b/src/core/Shader.js @@ -0,0 +1,46 @@ +import { GLShader } from 'pixi-gl-core'; +import settings from './settings'; + +function checkPrecision(src, def) +{ + if (src instanceof Array) + { + if (src[0].substring(0, 9) !== 'precision') + { + const copy = src.slice(0); + + copy.unshift(`precision ${def} float;`); + + return copy; + } + } + else if (src.substring(0, 9) !== 'precision') + { + return `precision ${def} float;\n${src}`; + } + + return src; +} + +/** + * Wrapper class, webGL Shader for Pixi. + * Adds precision string if vertexSrc or fragmentSrc have no mention of it. + * + * @class + * @extends GLShader + * @memberof PIXI + */ +export default class Shader extends GLShader +{ + /** + * + * @param {WebGLRenderingContext} gl - The current WebGL rendering context + * @param {string|string[]} vertexSrc - The vertex shader source as an array of strings. + * @param {string|string[]} fragmentSrc - The fragment shader source as an array of strings. + */ + constructor(gl, vertexSrc, fragmentSrc) + { + super(gl, checkPrecision(vertexSrc, settings.PRECISION_VERTEX), + checkPrecision(fragmentSrc, settings.PRECISION_FRAGMENT)); + } +} diff --git a/src/core/const.js b/src/core/const.js index ef6f662..3de0efe 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -188,7 +188,7 @@ * The gc modes that are supported by pixi. * * The {@link PIXI.settings.GC_MODE} Garbage Collection mode for pixi textures is AUTO - * If set to GC_MODE, the renderer will occasianally check textures usage. If they are not + * If set to GC_MODE, the renderer will occasionally check textures usage. If they are not * used for a specified period of time they will be removed from the GPU. They will of course * be uploaded again when they are required. This is a silent behind the scenes process that * should ensure that the GPU does not get filled up. diff --git a/src/core/display/Container.js b/src/core/display/Container.js index ed8cd5f..17fde8d 100644 --- a/src/core/display/Container.js +++ b/src/core/display/Container.js @@ -73,11 +73,12 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.push(child); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -108,11 +109,12 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.splice(index, 0, child); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -225,10 +227,11 @@ if (index === -1) return null; child.parent = null; + // ensure child transform will be recalculated + child.transform._parentID = -1; removeItems(this.children, index, 1); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -249,11 +252,12 @@ { const child = this.getChildAt(index); + // ensure child transform will be recalculated.. child.parent = null; + child.transform._parentID = -1; removeItems(this.children, index, 1); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -284,13 +288,12 @@ for (let i = 0; i < removed.length; ++i) { removed[i].parent = null; + if (removed[i].transform) + { + removed[i].transform._parentID = -1; + } } - // ensure a transform will be recalculated.. - if (this.transform) - { - this.transform._parentID = -1; - } this._boundsID++; this.onChildrenChange(beginIndex); diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index f37f2f5..be4c4fb 100644 --- a/src/core/graphics/Graphics.js +++ b/src/core/graphics/Graphics.js @@ -28,8 +28,9 @@ { /** * + * @param {boolean} [nativeLines=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP */ - constructor() + constructor(nativeLines = false) { super(); @@ -50,6 +51,13 @@ this.lineWidth = 0; /** + * If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * + * @member {boolean} + */ + this.nativeLines = nativeLines; + + /** * The color of any lines drawn. * * @member {string} @@ -757,28 +765,14 @@ if (!this._spriteRect) { - if (!Graphics._SPRITE_TEXTURE) - { - Graphics._SPRITE_TEXTURE = RenderTexture.create(10, 10); - - const canvas = document.createElement('canvas'); - - canvas.width = 10; - canvas.height = 10; - - const context = canvas.getContext('2d'); - - context.fillStyle = 'white'; - context.fillRect(0, 0, 10, 10); - - Graphics._SPRITE_TEXTURE = Texture.fromCanvas(canvas); - } - - this._spriteRect = new Sprite(Graphics._SPRITE_TEXTURE); + this._spriteRect = new Sprite(new Texture(Texture.WHITE)); } + + const sprite = this._spriteRect; + if (this.tint === 0xffffff) { - this._spriteRect.tint = this.graphicsData[0].fillColor; + sprite.tint = this.graphicsData[0].fillColor; } else { @@ -792,20 +786,21 @@ t1[1] *= t2[1]; t1[2] *= t2[2]; - this._spriteRect.tint = rgb2hex(t1); + sprite.tint = rgb2hex(t1); } - this._spriteRect.alpha = this.graphicsData[0].fillAlpha; - this._spriteRect.worldAlpha = this.worldAlpha * this._spriteRect.alpha; + sprite.alpha = this.graphicsData[0].fillAlpha; + sprite.worldAlpha = this.worldAlpha * sprite.alpha; + sprite.blendMode = this.blendMode; - Graphics._SPRITE_TEXTURE._frame.width = rect.width; - Graphics._SPRITE_TEXTURE._frame.height = rect.height; + sprite.texture._frame.width = rect.width; + sprite.texture._frame.height = rect.height; - this._spriteRect.transform.worldTransform = this.transform.worldTransform; + sprite.transform.worldTransform = this.transform.worldTransform; - this._spriteRect.anchor.set(-rect.x / rect.width, -rect.y / rect.height); - this._spriteRect._onAnchorUpdate(); + sprite.anchor.set(-rect.x / rect.width, -rect.y / rect.height); + sprite._onAnchorUpdate(); - this._spriteRect._renderWebGL(renderer); + sprite._renderWebGL(renderer); } /** @@ -1031,6 +1026,7 @@ this.fillColor, this.fillAlpha, this.filling, + this.nativeLines, shape ); diff --git a/src/core/graphics/GraphicsData.js b/src/core/graphics/GraphicsData.js index f5a87ae..b6f03c0 100644 --- a/src/core/graphics/GraphicsData.js +++ b/src/core/graphics/GraphicsData.js @@ -14,14 +14,19 @@ * @param {number} fillColor - the color of the fill * @param {number} fillAlpha - the alpha of the fill * @param {boolean} fill - whether or not the shape is filled with a colour + * @param {boolean} nativeLines - the method for drawing lines * @param {PIXI.Circle|PIXI.Rectangle|PIXI.Ellipse|PIXI.Polygon} shape - The shape object to draw. */ - constructor(lineWidth, lineColor, lineAlpha, fillColor, fillAlpha, fill, shape) + constructor(lineWidth, lineColor, lineAlpha, fillColor, fillAlpha, fill, nativeLines, shape) { /** * @member {number} the width of the line to draw */ this.lineWidth = lineWidth; + /** + * @member {boolean} if true the liens will be draw using LINES instead of TRIANGLE_STRIP + */ + this.nativeLines = nativeLines; /** * @member {number} the color of the line to draw @@ -85,6 +90,7 @@ this.fillColor, this.fillAlpha, this.fill, + this.nativeLines, this.shape ); } diff --git a/src/core/graphics/webgl/GraphicsRenderer.js b/src/core/graphics/webgl/GraphicsRenderer.js index 14083fe..67f5e92 100644 --- a/src/core/graphics/webgl/GraphicsRenderer.js +++ b/src/core/graphics/webgl/GraphicsRenderer.js @@ -102,7 +102,15 @@ shaderTemp.uniforms.alpha = graphics.worldAlpha; renderer.bindVao(webGLData.vao); - webGLData.vao.draw(gl.TRIANGLE_STRIP, webGLData.indices.length); + + if (graphics.nativeLines) + { + gl.drawArrays(gl.LINES, 0, webGLData.points.length / 6); + } + else + { + webGLData.vao.draw(gl.TRIANGLE_STRIP, webGLData.indices.length); + } } } diff --git a/src/core/graphics/webgl/utils/buildLine.js b/src/core/graphics/webgl/utils/buildLine.js index ecf43f7..4d6862e 100644 --- a/src/core/graphics/webgl/utils/buildLine.js +++ b/src/core/graphics/webgl/utils/buildLine.js @@ -11,7 +11,29 @@ * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties * @param {object} webGLData - an object containing all the webGL-specific information to create this shape */ -export default function buildLine(graphicsData, webGLData) +export default function (graphicsData, webGLData) +{ + if (graphicsData.nativeLines) + { + buildNativeLine(graphicsData, webGLData); + } + else + { + buildLine(graphicsData, webGLData); + } +} + +/** + * Builds a line to draw using the poligon method. + * + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @private + * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties + * @param {object} webGLData - an object containing all the webGL-specific information to create this shape + */ +function buildLine(graphicsData, webGLData) { // TODO OPTIMISE! let points = graphicsData.points; @@ -224,3 +246,46 @@ indices.push(indexStart - 1); } + +/** + * Builds a line to draw using the gl.drawArrays(gl.LINES) method + * + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @private + * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties + * @param {object} webGLData - an object containing all the webGL-specific information to create this shape + */ +function buildNativeLine(graphicsData, webGLData) +{ + let i = 0; + const points = graphicsData.points; + + if (points.length === 0) return; + + const verts = webGLData.points; + const length = points.length / 2; + + // sort color + const color = hex2rgb(graphicsData.lineColor); + const alpha = graphicsData.lineAlpha; + const r = color[0] * alpha; + const g = color[1] * alpha; + const b = color[2] * alpha; + + for (i = 1; i < length; i++) + { + const p1x = points[(i - 1) * 2]; + const p1y = points[((i - 1) * 2) + 1]; + + const p2x = points[i * 2]; + const p2y = points[(i * 2) + 1]; + + verts.push(p1x, p1y); + verts.push(r, g, b, alpha); + + verts.push(p2x, p2y); + verts.push(r, g, b, alpha); + } +} diff --git a/src/core/graphics/webgl/utils/buildRoundedRectangle.js b/src/core/graphics/webgl/utils/buildRoundedRectangle.js index 9abc807..a6c8461 100644 --- a/src/core/graphics/webgl/utils/buildRoundedRectangle.js +++ b/src/core/graphics/webgl/utils/buildRoundedRectangle.js @@ -77,6 +77,26 @@ } /** + * Calculate a single point for a quadratic bezier curve. + * Utility function used by quadraticBezierCurve. + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @private + * @param {number} n1 - first number + * @param {number} n2 - second number + * @param {number} perc - percentage + * @return {number} the result + * + */ +function getPt(n1, n2, perc) +{ + const diff = n2 - n1; + + return n1 + (diff * perc); +} + +/** * Calculate the points for a quadratic bezier curve. (helper function..) * Based on: https://stackoverflow.com/questions/785097/how-do-i-implement-a-bezier-curve-in-c * @@ -105,13 +125,6 @@ let x = 0; let y = 0; - function getPt(n1, n2, perc) - { - const diff = n2 - n1; - - return n1 + (diff * perc); - } - for (let i = 0, j = 0; i <= n; ++i) { j = i / n; diff --git a/src/core/index.js b/src/core/index.js index f7a817c..4f414e8 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -30,6 +30,7 @@ export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; export { default as CanvasGraphicsRenderer } from './graphics/canvas/CanvasGraphicsRenderer'; +export { default as Spritesheet } from './textures/Spritesheet'; export { default as Texture } from './textures/Texture'; export { default as BaseTexture } from './textures/BaseTexture'; export { default as RenderTexture } from './textures/RenderTexture'; diff --git a/src/core/math/Matrix.js b/src/core/math/Matrix.js index 0b77477..3679860 100644 --- a/src/core/math/Matrix.js +++ b/src/core/math/Matrix.js @@ -13,45 +13,50 @@ export default class Matrix { /** - * + * @param {number} [a=1] - x scale + * @param {number} [b=0] - y skew + * @param {number} [c=0] - x skew + * @param {number} [d=1] - y scale + * @param {number} [tx=0] - x translation + * @param {number} [ty=0] - y translation */ - constructor() + constructor(a = 1, b = 0, c = 0, d = 1, tx = 0, ty = 0) { /** * @member {number} * @default 1 */ - this.a = 1; + this.a = a; /** * @member {number} * @default 0 */ - this.b = 0; + this.b = b; /** * @member {number} * @default 0 */ - this.c = 0; + this.c = c; /** * @member {number} * @default 1 */ - this.d = 1; + this.d = d; /** * @member {number} * @default 0 */ - this.tx = 0; + this.tx = tx; /** * @member {number} * @default 0 */ - this.ty = 0; + this.ty = ty; this.array = null; } diff --git a/src/core/renderers/SystemRenderer.js b/src/core/renderers/SystemRenderer.js index fa116a6..a24c775 100644 --- a/src/core/renderers/SystemRenderer.js +++ b/src/core/renderers/SystemRenderer.js @@ -1,5 +1,5 @@ import { sayHello, hex2string, hex2rgb } from '../utils'; -import { Matrix } from '../math'; +import { Matrix, Rectangle } from '../math'; import { RENDERER_TYPE } from '../const'; import settings from '../settings'; import Container from '../display/Container'; @@ -21,8 +21,8 @@ { /** * @param {string} system - The name of the system this renderer is for. - * @param {number} [width=800] - the width of the canvas view - * @param {number} [height=600] - the height of the canvas view + * @param {number} [screenWidth=800] - the width of the screen + * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false @@ -37,7 +37,7 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(system, width, height, options) + constructor(system, screenWidth, screenHeight, options) { super(); @@ -69,20 +69,13 @@ this.type = RENDERER_TYPE.UNKNOWN; /** - * The width of the canvas view + * Measurements of the screen. (0, 0, screenWidth, screenHeight) * - * @member {number} - * @default 800 - */ - this.width = width || 800; - - /** - * The height of the canvas view + * Its safe to use as filterArea or hitArea for whole stage * - * @member {number} - * @default 600 + * @member {PIXI.Rectangle} */ - this.height = height || 600; + this.screen = new Rectangle(0, 0, screenWidth || 800, screenHeight || 600); /** * The canvas element that everything is drawn to @@ -107,7 +100,7 @@ this.transparent = options.transparent; /** - * Whether the render view should be resized automatically + * Whether css dimensions of canvas view should be resized to screen dimensions automatically * * @member {boolean} */ @@ -192,23 +185,48 @@ } /** - * Resizes the canvas view to the specified width and height + * Same as view.width, actual number of pixels in the canvas by horizontal * - * @param {number} width - the new width of the canvas view - * @param {number} height - the new height of the canvas view + * @member {number} + * @readonly + * @default 800 */ - resize(width, height) + get width() { - this.width = width * this.resolution; - this.height = height * this.resolution; + return this.view.width; + } - this.view.width = this.width; - this.view.height = this.height; + /** + * Same as view.height, actual number of pixels in the canvas by vertical + * + * @member {number} + * @readonly + * @default 600 + */ + get height() + { + return this.view.height; + } + + /** + * Resizes the screen and canvas to the specified width and height + * Canvas dimensions are multiplied by resolution + * + * @param {number} screenWidth - the new width of the screen + * @param {number} screenHeight - the new height of the screen + */ + resize(screenWidth, screenHeight) + { + this.screen.width = screenWidth; + this.screen.height = screenHeight; + + this.view.width = screenWidth * this.resolution; + this.view.height = screenHeight * this.resolution; if (this.autoResize) { - this.view.style.width = `${this.width / this.resolution}px`; - this.view.style.height = `${this.height / this.resolution}px`; + this.view.style.width = `${screenWidth}px`; + this.view.style.height = `${screenHeight}px`; } } @@ -249,11 +267,10 @@ this.type = RENDERER_TYPE.UNKNOWN; - this.width = 0; - this.height = 0; - this.view = null; + this.screen = null; + this.resolution = 0; this.transparent = false; diff --git a/src/core/renderers/canvas/CanvasRenderer.js b/src/core/renderers/canvas/CanvasRenderer.js index 1bd2d5e..e84e49c 100644 --- a/src/core/renderers/canvas/CanvasRenderer.js +++ b/src/core/renderers/canvas/CanvasRenderer.js @@ -18,8 +18,8 @@ export default class CanvasRenderer extends SystemRenderer { /** - * @param {number} [width=800] - the width of the canvas view - * @param {number} [height=600] - the height of the canvas view + * @param {number} [screenWidth=800] - the width of the screen + * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false @@ -34,9 +34,9 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(width, height, options = {}) + constructor(screenWidth, screenHeight, options = {}) { - super('Canvas', width, height, options); + super('Canvas', screenWidth, screenHeight, options); this.type = RENDERER_TYPE.CANVAS; @@ -96,7 +96,7 @@ this.context = null; this.renderingToScreen = false; - this.resize(width, height); + this.resize(screenWidth, screenHeight); } /** @@ -283,12 +283,12 @@ * * @extends PIXI.SystemRenderer#resize * - * @param {number} width - The new width of the canvas view - * @param {number} height - The new height of the canvas view + * @param {number} screenWidth - the new width of the screen + * @param {number} screenHeight - the new height of the screen */ - resize(width, height) + resize(screenWidth, screenHeight) { - super.resize(width, height); + super.resize(screenWidth, screenHeight); // reset the scale mode.. oddly this seems to be reset when the canvas is resized. // surely a browser bug?? Let pixi fix that for you.. diff --git a/src/core/renderers/webgl/TextureManager.js b/src/core/renderers/webgl/TextureManager.js index b685c1a..8227289 100644 --- a/src/core/renderers/webgl/TextureManager.js +++ b/src/core/renderers/webgl/TextureManager.js @@ -61,7 +61,7 @@ * Updates and/or Creates a WebGL texture for the renderer's context. * * @param {PIXI.BaseTexture|PIXI.Texture} texture - the texture to update - * @param {Number} location - the location the texture will be bound to. + * @param {number} location - the location the texture will be bound to. * @return {GLTexture} The gl texture. */ updateTexture(texture, location) diff --git a/src/core/renderers/webgl/WebGLRenderer.js b/src/core/renderers/webgl/WebGLRenderer.js index e4cd8a0..830b382 100644 --- a/src/core/renderers/webgl/WebGLRenderer.js +++ b/src/core/renderers/webgl/WebGLRenderer.js @@ -31,8 +31,8 @@ { /** * - * @param {number} [width=0] - the width of the canvas view - * @param {number} [height=0] - the height of the canvas view + * @param {number} [screenWidth=800] - the width of the screen + * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false @@ -53,9 +53,9 @@ * @param {boolean} [options.legacy=false] - If true Pixi will aim to ensure compatibility * with older / less advanced devices. If you experiance unexplained flickering try setting this to true. */ - constructor(width, height, options = {}) + constructor(screenWidth, screenHeight, options = {}) { - super('WebGL', width, height, options); + super('WebGL', screenWidth, screenHeight, options); this.legacy = !!options.legacy; @@ -245,7 +245,7 @@ glCore._testingContext = gl; // setup the width/height properties and gl viewport - this.resize(this.width, this.height); + this.resize(this.screen.width, this.screen.height); } /** @@ -343,16 +343,16 @@ /** * Resizes the webGL view to the specified width and height. * - * @param {number} width - the new width of the webGL view - * @param {number} height - the new height of the webGL view + * @param {number} screenWidth - the new width of the screen + * @param {number} screenHeight - the new height of the screen */ - resize(width, height) + resize(screenWidth, screenHeight) { // if(width * this.resolution === this.width && height * this.resolution === this.height)return; - SystemRenderer.prototype.resize.call(this, width, height); + SystemRenderer.prototype.resize.call(this, screenWidth, screenHeight); - this.rootRenderTarget.resize(width, height); + this.rootRenderTarget.resize(screenWidth, screenHeight); if (this._activeRenderTarget === this.rootRenderTarget) { @@ -396,6 +396,26 @@ } /** + * Erases the render texture and fills the drawing area with a colour + * + * @param {PIXI.RenderTexture} renderTexture - The render texture to clear + * @param {number} [clearColor] - The colour + * @return {PIXI.WebGLRenderer} Returns itself. + */ + clearRenderTexture(renderTexture, clearColor) + { + const baseTexture = renderTexture.baseTexture; + const renderTarget = baseTexture._glRenderTargets[this.CONTEXT_UID]; + + if (renderTarget) + { + renderTarget.clear(clearColor); + } + + return this; + } + + /** * Binds a render texture for rendering * * @param {PIXI.RenderTexture} renderTexture - The render texture to render @@ -474,9 +494,10 @@ * Changes the current GLShader to the one given in parameter * * @param {PIXI.glCore.Shader} shader - the new glShader + * @param {boolean} [autoProject=true] - Whether automatically set the projection matrix * @return {PIXI.WebGLRenderer} Returns itself. */ - _bindGLShader(shader) + _bindGLShader(shader, autoProject) { // TODO cache if (this._activeShader !== shader) @@ -484,8 +505,14 @@ this._activeShader = shader; shader.bind(); - // automatically set the projection matrix - shader.uniforms.projectionMatrix = this._activeRenderTarget.projectionMatrix.toArray(true); + // `autoProject` normally would be a default parameter set to true + // but because of how Babel transpiles default parameters + // it hinders the performance of this method. + if (autoProject !== false) + { + // automatically set the projection matrix + shader.uniforms.projectionMatrix = this._activeRenderTarget.projectionMatrix.toArray(true); + } } return this; diff --git a/src/core/renderers/webgl/WebGLState.js b/src/core/renderers/webgl/WebGLState.js index ba6ecc5..6c846d5 100755 --- a/src/core/renderers/webgl/WebGLState.js +++ b/src/core/renderers/webgl/WebGLState.js @@ -82,18 +82,20 @@ push() { // next state.. - let state = this.stack[++this.stackIndex]; + let state = this.stack[this.stackIndex]; if (!state) { state = this.stack[this.stackIndex] = new Uint8Array(16); } + ++this.stackIndex; + // copy state.. // set active state so we can force overrides of gl state for (let i = 0; i < this.activeState.length; i++) { - this.activeState[i] = state[i]; + state[i] = this.activeState[i]; } } diff --git a/src/core/renderers/webgl/filters/Filter.js b/src/core/renderers/webgl/filters/Filter.js index 2c3eaad..4eb6128 100644 --- a/src/core/renderers/webgl/filters/Filter.js +++ b/src/core/renderers/webgl/filters/Filter.js @@ -9,7 +9,7 @@ * @memberof PIXI * @extends PIXI.Shader */ -class Filter extends Shader +export default class Filter extends Shader { /** * @param {string} [vertexSrc] - The source of the vertex shader. @@ -56,15 +56,16 @@ * @param {PIXI.RenderTarget} input - The input render target. * @param {PIXI.RenderTarget} output - The target to output to. * @param {boolean} clear - Should the output be cleared before rendering to it + * @param {object} [currentState] - It's current state of filter. + * There are some useful properties in the currentState : + * target, filters, sourceFrame, destinationFrame, renderTarget, resolution */ - apply(filterManager, input, output, clear) + apply(filterManager, input, output, clear, currentState) // eslint-disable-line no-unused-vars { // do as you please! - filterManager.applyFilter(this, input, output, clear); + filterManager.applyFilter(this, input, output, clear, currentState); // or just do a regular render.. } } - -export default Filter; diff --git a/src/core/renderers/webgl/filters/extractUniformsFromSrc.js b/src/core/renderers/webgl/filters/extractUniformsFromSrc.js index d78a0b1..4893214 100644 --- a/src/core/renderers/webgl/filters/extractUniformsFromSrc.js +++ b/src/core/renderers/webgl/filters/extractUniformsFromSrc.js @@ -12,7 +12,7 @@ function extractUniformsFromString(string) { - const maskRegex = new RegExp('^(projectionMatrix|uSampler|filterArea)$'); + const maskRegex = new RegExp('^(projectionMatrix|uSampler|filterArea|filterClamp)$'); const uniforms = {}; let nameSplit; diff --git a/src/core/renderers/webgl/managers/FilterManager.js b/src/core/renderers/webgl/managers/FilterManager.js index 43fc869..6c7422d 100644 --- a/src/core/renderers/webgl/managers/FilterManager.js +++ b/src/core/renderers/webgl/managers/FilterManager.js @@ -2,6 +2,7 @@ import RenderTarget from '../utils/RenderTarget'; import Quad from '../utils/Quad'; import { Rectangle } from '../../../math'; +import Shader from '../../../Shader'; import * as filterTransforms from '../filters/filterTransforms'; import bitTwiddle from 'bit-twiddle'; @@ -22,8 +23,6 @@ this.filters = []; this.target = null; this.resolution = 1; - - this.firstRun = true; } } @@ -155,7 +154,7 @@ if (filters.length === 1) { - filters[0].apply(this, currentState.renderTarget, lastState.renderTarget, false); + filters[0].apply(this, currentState.renderTarget, lastState.renderTarget, false, currentState); this.freePotRenderTarget(currentState.renderTarget); } else @@ -177,7 +176,7 @@ for (i = 0; i < filters.length - 1; ++i) { - filters[i].apply(this, flip, flop, true); + filters[i].apply(this, flip, flop, true, currentState); const t = flip; @@ -185,7 +184,7 @@ flop = t; } - filters[i].apply(this, flip, lastState.renderTarget, false); + filters[i].apply(this, flip, lastState.renderTarget, false, currentState); this.freePotRenderTarget(flip); this.freePotRenderTarget(flop); @@ -212,16 +211,28 @@ const renderer = this.renderer; const gl = renderer.gl; - renderer.bindShader(filter, true); + let shader = filter.glShaders[renderer.CONTEXT_UID]; - // TODO theres a better way! - // quad can be a mesh and we then should be able to pull this off without accessing the shader directly - const shader = renderer.shaderManager.getGLShader(); - - // TODO change quad to mesh.. - if (!this.firstRun) + // cacheing.. + if (!shader) { - this.firstRun = true; + if (filter.glShaderKey) + { + shader = this.shaderCache[filter.glShaderKey]; + + if (!shader) + { + shader = new Shader(this.gl, filter.vertexSrc, filter.fragmentSrc); + + filter.glShaders[renderer.CONTEXT_UID] = this.shaderCache[filter.glShaderKey] = shader; + } + } + else + { + shader = filter.glShaders[renderer.CONTEXT_UID] = new Shader(this.gl, filter.vertexSrc, filter.fragmentSrc); + } + + // TODO - this only needs to be done once? renderer.bindVao(null); this.quad.initVao(shader); @@ -244,58 +255,146 @@ renderer.maskManager.pushScissorMask(null, renderer.maskManager.scissorData); } - // renderer._bindGLShader(shader); + renderer.bindShader(shader); - let currentState; - const uniforms = filter.uniforms; + // free unit 0 for us, doesn't matter what was there + // don't try to restore it, because syncUniforms can upload it to another slot + // and it'll be a problem + const tex = this.renderer.emptyTextures[0]; - if (uniforms.filterArea) - { - currentState = this.filterData.stack[this.filterData.index]; - - const filterArea = uniforms.filterArea; - - filterArea[0] = currentState.renderTarget.size.width; - filterArea[1] = currentState.renderTarget.size.height; - filterArea[2] = currentState.sourceFrame.x; - filterArea[3] = currentState.sourceFrame.y; - - filter.uniforms.filterArea = filterArea; - } - - // use this to clamp displaced texture coords so they belong to filterArea - // see displacementFilter fragment shader for an example - if (uniforms.filterClamp) - { - currentState = this.filterData.stack[this.filterData.index]; - - const filterClamp = uniforms.filterClamp; - - filterClamp[0] = 0; - filterClamp[1] = 0; - filterClamp[2] = (currentState.sourceFrame.width - 1) / currentState.renderTarget.size.width; - filterClamp[3] = (currentState.sourceFrame.height - 1) / currentState.renderTarget.size.height; - - filter.uniforms.filterClamp = filterClamp; - } - - renderer.shaderManager.setUniforms(filter.uniforms); + this.renderer.boundTextures[0] = tex; + // this syncs the pixi filters uniforms with glsl uniforms + this.syncUniforms(shader, filter); renderer.state.setBlendMode(filter.blendMode); - // temporary bypass cache.. - const tex = this.renderer.boundTextures[0]; - gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, input.texture.texture); this.quad.vao.draw(this.renderer.gl.TRIANGLES, 6, 0); - // restore cache. gl.bindTexture(gl.TEXTURE_2D, tex._glTextures[this.renderer.CONTEXT_UID].texture); } /** + * Uploads the uniforms of the filter. + * + * @param {GLShader} shader - The underlying gl shader. + * @param {PIXI.Filter} filter - The filter we are synchronizing. + */ + syncUniforms(shader, filter) + { + const uniformData = filter.uniformData; + const uniforms = filter.uniforms; + + // 0 is reserved for the pixi texture so we start at 1! + let textureCount = 1; + let currentState; + + // filterArea and filterClamp that are handled by FilterManager directly + // they must not appear in uniformData + + if (shader.uniforms.filterArea) + { + currentState = this.filterData.stack[this.filterData.index]; + + const filterArea = shader.uniforms.filterArea; + + filterArea[0] = currentState.renderTarget.size.width; + filterArea[1] = currentState.renderTarget.size.height; + filterArea[2] = currentState.sourceFrame.x; + filterArea[3] = currentState.sourceFrame.y; + + shader.uniforms.filterArea = filterArea; + } + + // use this to clamp displaced texture coords so they belong to filterArea + // see displacementFilter fragment shader for an example + if (shader.uniforms.filterClamp) + { + currentState = currentState || this.filterData.stack[this.filterData.index]; + + const filterClamp = shader.uniforms.filterClamp; + + filterClamp[0] = 0; + filterClamp[1] = 0; + filterClamp[2] = (currentState.sourceFrame.width - 1) / currentState.renderTarget.size.width; + filterClamp[3] = (currentState.sourceFrame.height - 1) / currentState.renderTarget.size.height; + + shader.uniforms.filterClamp = filterClamp; + } + + // TODO Cacheing layer.. + for (const i in uniformData) + { + if (uniformData[i].type === 'sampler2D' && uniforms[i] !== 0) + { + if (uniforms[i].baseTexture) + { + shader.uniforms[i] = this.renderer.bindTexture(uniforms[i].baseTexture, textureCount); + } + else + { + shader.uniforms[i] = textureCount; + + // TODO + // this is helpful as renderTargets can also be set. + // Although thinking about it, we could probably + // make the filter texture cache return a RenderTexture + // rather than a renderTarget + const gl = this.renderer.gl; + + this.renderer.boundTextures[textureCount] = this.renderer.emptyTextures[textureCount]; + gl.activeTexture(gl.TEXTURE0 + textureCount); + + uniforms[i].texture.bind(); + } + + textureCount++; + } + else if (uniformData[i].type === 'mat3') + { + // check if its pixi matrix.. + if (uniforms[i].a !== undefined) + { + shader.uniforms[i] = uniforms[i].toArray(true); + } + else + { + shader.uniforms[i] = uniforms[i]; + } + } + else if (uniformData[i].type === 'vec2') + { + // check if its a point.. + if (uniforms[i].x !== undefined) + { + const val = shader.uniforms[i] || new Float32Array(2); + + val[0] = uniforms[i].x; + val[1] = uniforms[i].y; + shader.uniforms[i] = val; + } + else + { + shader.uniforms[i] = uniforms[i]; + } + } + else if (uniformData[i].type === 'float') + { + if (shader.uniforms.data[i].value !== uniformData[i]) + { + shader.uniforms[i] = uniforms[i]; + } + } + else + { + shader.uniforms[i] = uniforms[i]; + } + } + } + + /** * Gets a render target from the pool, or creates a new one. * * @param {boolean} clear - Should we clear the render texture when we get it? @@ -442,7 +541,6 @@ renderTarget.resolution = resolution; renderTarget.defaultFrame.width = renderTarget.size.width = minWidth / resolution; renderTarget.defaultFrame.height = renderTarget.size.height = minHeight / resolution; - renderTarget.attachStencilBuffer(); return renderTarget; } diff --git a/src/core/settings.js b/src/core/settings.js index bb08cb0..f4dd087 100644 --- a/src/core/settings.js +++ b/src/core/settings.js @@ -77,11 +77,11 @@ * * @static * @memberof PIXI.settings - * @type {RegExp|string} + * @type {RegExp} * @example `@2x` - * @default /@(.+)x/ + * @default /@([0-9\.]+)x/ */ - RETINA_PREFIX: /@(.+)x/, + RETINA_PREFIX: /@([0-9\.]+)x/, /** * The default render options if none are supplied to {@link PIXI.WebGLRenderer} @@ -175,14 +175,24 @@ SCALE_MODE: 0, /** - * Default specify float precision in shaders. + * Default specify float precision in vertex shader. + * + * @static + * @memberof PIXI.settings + * @type {PIXI.PRECISION} + * @default PIXI.PRECISION.HIGH + */ + PRECISION_VERTEX: 'highp', + + /** + * Default specify float precision in fragment shader. * * @static * @memberof PIXI.settings * @type {PIXI.PRECISION} * @default PIXI.PRECISION.MEDIUM */ - PRECISION: 'mediump', + PRECISION_FRAGMENT: 'mediump', /** * Can we upload the same buffer in a single frame? diff --git a/src/core/sprites/Sprite.js b/src/core/sprites/Sprite.js index 28b0b18..121bf58 100644 --- a/src/core/sprites/Sprite.js +++ b/src/core/sprites/Sprite.js @@ -212,11 +212,11 @@ } else { - w0 = orig.width * (1 - anchor._x); - w1 = orig.width * -anchor._x; + w1 = -anchor._x * orig.width; + w0 = w1 + orig.width; - h0 = orig.height * (1 - anchor._y); - h1 = orig.height * -anchor._y; + h1 = -anchor._y * orig.height; + h0 = h1 + orig.height; } // xy @@ -269,11 +269,11 @@ const tx = wt.tx; const ty = wt.ty; - const w0 = (orig.width) * (1 - anchor._x); - const w1 = (orig.width) * -anchor._x; + const w1 = -anchor._x * orig.width; + const w0 = w1 + orig.width; - const h0 = orig.height * (1 - anchor._y); - const h1 = orig.height * -anchor._y; + const h1 = -anchor._y * orig.height; + const h0 = h1 + orig.height; // xy vertexData[0] = (a * w1) + (c * h1) + tx; @@ -440,7 +440,7 @@ * * @static * @param {number|string|PIXI.BaseTexture|HTMLCanvasElement|HTMLVideoElement} source Source to create texture from - * @return {PIXI.Sprite} The newly created texture + * @return {PIXI.Sprite} The newly created sprite */ static from(source) { @@ -538,8 +538,8 @@ } /** - * The tint applied to the sprite. This is a hex value. A value of - * 0xFFFFFF will remove any tint effect. + * The tint applied to the sprite. This is a hex value. + * A value of 0xFFFFFF will remove any tint effect. * * @member {number} * @default 0xFFFFFF diff --git a/src/core/sprites/canvas/CanvasTinter.js b/src/core/sprites/canvas/CanvasTinter.js index 1cf316b..27e5e0c 100644 --- a/src/core/sprites/canvas/CanvasTinter.js +++ b/src/core/sprites/canvas/CanvasTinter.js @@ -75,8 +75,8 @@ crop.width *= resolution; crop.height *= resolution; - canvas.width = crop.width; - canvas.height = crop.height; + canvas.width = Math.ceil(crop.width); + canvas.height = Math.ceil(crop.height); context.fillStyle = `#${(`00000${(color | 0).toString(16)}`).substr(-6)}`; @@ -130,8 +130,8 @@ crop.width *= resolution; crop.height *= resolution; - canvas.width = crop.width; - canvas.height = crop.height; + canvas.width = Math.ceil(crop.width); + canvas.height = Math.ceil(crop.height); context.globalCompositeOperation = 'copy'; context.fillStyle = `#${(`00000${(color | 0).toString(16)}`).substr(-6)}`; @@ -172,8 +172,8 @@ crop.width *= resolution; crop.height *= resolution; - canvas.width = crop.width; - canvas.height = crop.height; + canvas.width = Math.ceil(crop.width); + canvas.height = Math.ceil(crop.height); context.globalCompositeOperation = 'copy'; context.drawImage( diff --git a/src/core/sprites/webgl/SpriteRenderer.js b/src/core/sprites/webgl/SpriteRenderer.js index 2affcd5..7c352e2 100644 --- a/src/core/sprites/webgl/SpriteRenderer.js +++ b/src/core/sprites/webgl/SpriteRenderer.js @@ -30,7 +30,7 @@ /** * Number of values sent in the vertex buffer. - * positionX, positionY, colorR, colorG, colorB = 5 + * aVertexPosition(2), aTextureCoord(1), aColor(1), aTextureId(1) = 5 * * @member {number} */ @@ -75,7 +75,6 @@ this.shader = null; this.currentIndex = 0; - TICK = 0; this.groups = []; for (let k = 0; k < this.size; k++) diff --git a/src/core/textures/BaseTexture.js b/src/core/textures/BaseTexture.js index 853de7e..aeccf39 100644 --- a/src/core/textures/BaseTexture.js +++ b/src/core/textures/BaseTexture.js @@ -232,16 +232,24 @@ this.realWidth = this.source.naturalWidth || this.source.videoWidth || this.source.width; this.realHeight = this.source.naturalHeight || this.source.videoHeight || this.source.height; - this.width = this.realWidth / this.resolution; - this.height = this.realHeight / this.resolution; - - this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); + this._updateDimensions(); } this.emit('update', this); } /** + * Update dimensions from real values + */ + _updateDimensions() + { + this.width = this.realWidth / this.resolution; + this.height = this.realHeight / this.resolution; + + this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); + } + + /** * Load a source. * * If the source is not-immediately-available, such as an image that needs to be @@ -525,11 +533,7 @@ this.realWidth = Math.round(svgWidth * this.sourceScale); this.realHeight = Math.round(svgHeight * this.sourceScale); - this.width = this.realWidth / this.resolution; - this.height = this.realHeight / this.resolution; - - // Check pow2 after scale - this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); + this._updateDimensions(); // Create a canvas element const canvas = document.createElement('canvas'); @@ -690,4 +694,52 @@ return baseTexture; } + + /** + * Helper function that creates a base texture based on the source you provide. + * The source can be - image url, image element, canvas element. + * + * @static + * @param {string|HTMLImageElement|HTMLCanvasElement} source - The source to create base texture from. + * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values + * @param {number} [sourceScale=(auto)] - Scale for the original image, used with Svg images. + * @return {PIXI.BaseTexture} The new base texture. + */ + static from(source, scaleMode, sourceScale) + { + if (typeof source === 'string') + { + return BaseTexture.fromImage(source, undefined, scaleMode, sourceScale); + } + else if (source instanceof HTMLImageElement) + { + const imageUrl = source.src; + let baseTexture = BaseTextureCache[imageUrl]; + + if (!baseTexture) + { + baseTexture = new BaseTexture(source, scaleMode); + baseTexture.imageUrl = imageUrl; + + if (sourceScale) + { + baseTexture.sourceScale = sourceScale; + } + + // if there is an @2x at the end of the url we are going to assume its a highres image + baseTexture.resolution = getResolutionOfUrl(imageUrl); + + BaseTextureCache[imageUrl] = baseTexture; + } + + return baseTexture; + } + else if (source instanceof HTMLCanvasElement) + { + return BaseTexture.fromCanvas(source, scaleMode); + } + + // lets assume its a base texture! + return source; + } } diff --git a/src/core/textures/Spritesheet.js b/src/core/textures/Spritesheet.js new file mode 100644 index 0000000..d469c62 --- /dev/null +++ b/src/core/textures/Spritesheet.js @@ -0,0 +1,274 @@ +import { Rectangle, Texture } from '../'; +import { getResolutionOfUrl, TextureCache } from '../utils'; + +/** + * Utility class for maintaining reference to a collection + * of Textures on a single Spritesheet. + * + * @class + * @memberof PIXI + */ +export default class Spritesheet +{ + /** + * The maximum number of Textures to build per process. + * + * @type {number} + * @default 1000 + */ + static get BATCH_SIZE() + { + return 1000; + } + + /** + * @param {PIXI.BaseTexture} baseTexture Reference to the source BaseTexture object. + * @param {Object} data - Spritesheet image data. + * @param {string} [resolutionFilename] - The filename to consider when determining + * the resolution of the spritesheet. If not provided, the imageUrl will + * be used on the BaseTexture. + */ + constructor(baseTexture, data, resolutionFilename = null) + { + /** + * Reference to ths source texture + * @type {PIXI.BaseTexture} + */ + this.baseTexture = baseTexture; + + /** + * Map of spritesheet textures. + * @type {Object} + */ + this.textures = {}; + + /** + * Reference to the original JSON data. + * @type {Object} + */ + this.data = data; + + /** + * The resolution of the spritesheet. + * @type {number} + */ + this.resolution = this._updateResolution( + resolutionFilename || this.baseTexture.imageUrl + ); + + /** + * Map of spritesheet frames. + * @type {Object} + * @private + */ + this._frames = this.data.frames; + + /** + * Collection of frame names. + * @type {string[]} + * @private + */ + this._frameKeys = Object.keys(this._frames); + + /** + * Current batch index being processed. + * @type {number} + * @private + */ + this._batchIndex = 0; + + /** + * Callback when parse is completed. + * @type {Function} + * @private + */ + this._callback = null; + } + + /** + * Generate the resolution from the filename or fallback + * to the meta.scale field of the JSON data. + * + * @private + * @param {string} resolutionFilename - The filename to use for resolving + * the default resolution. + * @return {number} Resolution to use for spritesheet. + */ + _updateResolution(resolutionFilename) + { + const scale = this.data.meta.scale; + + // Use a defaultValue of `null` to check if a url-based resolution is set + let resolution = getResolutionOfUrl(resolutionFilename, null); + + // No resolution found via URL + if (resolution === null) + { + // Use the scale value or default to 1 + resolution = scale !== undefined ? parseFloat(scale) : 1; + } + + // For non-1 resolutions, update baseTexture + if (resolution !== 1) + { + this.baseTexture.resolution = resolution; + this.baseTexture.update(); + } + + return resolution; + } + + /** + * Parser spritesheet from loaded data. This is done asynchronously + * to prevent creating too many Texture within a single process. + * + * @param {Function} callback - Callback when complete returns + * a map of the Textures for this spritesheet. + */ + parse(callback) + { + this._batchIndex = 0; + this._callback = callback; + + if (this._frameKeys.length <= Spritesheet.BATCH_SIZE) + { + this._processFrames(0); + this._parseComplete(); + } + else + { + this._nextBatch(); + } + } + + /** + * Process a batch of frames + * + * @private + * @param {number} initialFrameIndex - The index of frame to start. + */ + _processFrames(initialFrameIndex) + { + let frameIndex = initialFrameIndex; + const maxFrames = Spritesheet.BATCH_SIZE; + + while (frameIndex - initialFrameIndex < maxFrames && frameIndex < this._frameKeys.length) + { + const i = this._frameKeys[frameIndex]; + const rect = this._frames[i].frame; + + if (rect) + { + let frame = null; + let trim = null; + const orig = new Rectangle( + 0, + 0, + this._frames[i].sourceSize.w / this.resolution, + this._frames[i].sourceSize.h / this.resolution + ); + + if (this._frames[i].rotated) + { + frame = new Rectangle( + rect.x / this.resolution, + rect.y / this.resolution, + rect.h / this.resolution, + rect.w / this.resolution + ); + } + else + { + frame = new Rectangle( + rect.x / this.resolution, + rect.y / this.resolution, + rect.w / this.resolution, + rect.h / this.resolution + ); + } + + // Check to see if the sprite is trimmed + if (this._frames[i].trimmed) + { + trim = new Rectangle( + this._frames[i].spriteSourceSize.x / this.resolution, + this._frames[i].spriteSourceSize.y / this.resolution, + rect.w / this.resolution, + rect.h / this.resolution + ); + } + + this.textures[i] = new Texture( + this.baseTexture, + frame, + orig, + trim, + this._frames[i].rotated ? 2 : 0 + ); + + // lets also add the frame to pixi's global cache for fromFrame and fromImage functions + TextureCache[i] = this.textures[i]; + } + + frameIndex++; + } + } + + /** + * The parse has completed. + * + * @private + */ + _parseComplete() + { + const callback = this._callback; + + this._callback = null; + this._batchIndex = 0; + callback.call(this, this.textures); + } + + /** + * Begin the next batch of textures. + * + * @private + */ + _nextBatch() + { + this._processFrames(this._batchIndex * Spritesheet.BATCH_SIZE); + this._batchIndex++; + setTimeout(() => + { + if (this._batchIndex * Spritesheet.BATCH_SIZE < this._frameKeys.length) + { + this._nextBatch(); + } + else + { + this._parseComplete(); + } + }, 0); + } + + /** + * Destroy Spritesheet and don't use after this. + * + * @param {boolean} [destroyBase=false] Whether to destroy the base texture as well + */ + destroy(destroyBase = false) + { + for (const i in this.textures) + { + this.textures[i].destroy(); + } + this._frames = null; + this._frameKeys = null; + this.data = null; + this.textures = null; + if (destroyBase) + { + this.baseTexture.destroy(); + } + this.baseTexture = null; + } +} diff --git a/src/core/textures/Texture.js b/src/core/textures/Texture.js index 6403d4f..afe4254 100644 --- a/src/core/textures/Texture.js +++ b/src/core/textures/Texture.js @@ -3,7 +3,7 @@ import TextureUvs from './TextureUvs'; import EventEmitter from 'eventemitter3'; import { Rectangle } from '../math'; -import { TextureCache, BaseTextureCache } from '../utils'; +import { TextureCache, BaseTextureCache, getResolutionOfUrl } from '../utils'; /** * A texture stores the information that represents an image or part of an image. It cannot be added @@ -365,7 +365,8 @@ * The source can be - frame id, image url, video url, canvas element, video element, base texture * * @static - * @param {number|string|PIXI.BaseTexture|HTMLCanvasElement|HTMLVideoElement} source - Source to create texture from + * @param {number|string|HTMLImageElement|HTMLCanvasElement|HTMLVideoElement|PIXI.BaseTexture} + * source - Source to create texture from * @return {PIXI.Texture} The newly created texture */ static from(source) @@ -393,7 +394,7 @@ } else if (source instanceof HTMLImageElement) { - return new Texture(new BaseTexture(source)); + return new Texture(BaseTexture.from(source)); } else if (source instanceof HTMLCanvasElement) { @@ -413,6 +414,43 @@ } /** + * Create a texture from a source and add to the cache. + * + * @static + * @param {HTMLImageElement|HTMLCanvasElement} source - The input source. + * @param {String} imageUrl - File name of texture, for cache and resolving resolution. + * @param {String} [name] - Human readible name for the texture cache. If no name is + * specified, only `imageUrl` will be used as the cache ID. + * @return {PIXI.Texture} Output texture + */ + static fromLoader(source, imageUrl, name) + { + const baseTexture = new BaseTexture(source, null, getResolutionOfUrl(imageUrl)); + const texture = new Texture(baseTexture); + + baseTexture.imageUrl = imageUrl; + + // No name, use imageUrl instead + if (!name) + { + name = imageUrl; + } + + // lets also add the frame to pixi's global cache for fromFrame and fromImage fucntions + BaseTextureCache[name] = baseTexture; + TextureCache[name] = texture; + + // also add references by url if they are different. + if (name !== imageUrl) + { + BaseTextureCache[imageUrl] = baseTexture; + TextureCache[imageUrl] = texture; + } + + return texture; + } + + /** * Adds a texture to the global TextureCache. This cache is shared across the whole PIXI object. * * @static @@ -522,6 +560,29 @@ } } +function createWhiteTexture() +{ + const canvas = document.createElement('canvas'); + + canvas.width = 10; + canvas.height = 10; + + const context = canvas.getContext('2d'); + + context.fillStyle = 'white'; + context.fillRect(0, 0, 10, 10); + + return new Texture(new BaseTexture(canvas)); +} + +function removeAllHandlers(tex) +{ + tex.destroy = function _emptyDestroy() { /* empty */ }; + tex.on = function _emptyOn() { /* empty */ }; + tex.once = function _emptyOnce() { /* empty */ }; + tex.emit = function _emptyEmit() { /* empty */ }; +} + /** * An empty texture, used often to not have to create multiple empty textures. * Can not be destroyed. @@ -530,7 +591,14 @@ * @constant */ Texture.EMPTY = new Texture(new BaseTexture()); -Texture.EMPTY.destroy = function _emptyDestroy() { /* empty */ }; -Texture.EMPTY.on = function _emptyOn() { /* empty */ }; -Texture.EMPTY.once = function _emptyOnce() { /* empty */ }; -Texture.EMPTY.emit = function _emptyEmit() { /* empty */ }; +removeAllHandlers(Texture.EMPTY); + +/** + * A white texture of 10x10 size, used for graphics and other things + * Can not be destroyed. + * + * @static + * @constant + */ +Texture.WHITE = createWhiteTexture(); +removeAllHandlers(Texture.WHITE); diff --git a/src/core/ticker/Ticker.js b/src/core/ticker/Ticker.js index d98a1f0..17a1517 100644 --- a/src/core/ticker/Ticker.js +++ b/src/core/ticker/Ticker.js @@ -67,11 +67,12 @@ * is based, this value is neither capped nor scaled. * If the platform supports DOMHighResTimeStamp, * this value will have a precision of 1 µs. + * Defaults to target frame time * * @member {number} - * @default 1 / TARGET_FPMS + * @default 16.66 */ - this.elapsedMS = 1 / settings.TARGET_FPMS; // default to target frame time + this.elapsedMS = 1 / settings.TARGET_FPMS; /** * The last time {@link PIXI.ticker.Ticker#update} was invoked. diff --git a/src/core/utils/index.js b/src/core/utils/index.js index 6ff609f..be44750 100644 --- a/src/core/utils/index.js +++ b/src/core/utils/index.js @@ -2,6 +2,7 @@ import settings from '../settings'; import EventEmitter from 'eventemitter3'; import pluginTarget from './pluginTarget'; +import * as mixins from './mixin'; import * as isMobile from 'ismobilejs'; let nextUid = 0; @@ -33,6 +34,7 @@ * @type {mixin} */ pluginTarget, + mixins, }; /** @@ -93,7 +95,7 @@ */ export function rgb2hex(rgb) { - return (((rgb[0] * 255) << 16) + ((rgb[1] * 255) << 8) + (rgb[2] * 255)); + return (((rgb[0] * 255) << 16) + ((rgb[1] * 255) << 8) + (rgb[2] * 255 | 0)); } /** @@ -368,3 +370,43 @@ * @private */ export const ProgramCache = {}; + +/** + * Destroys all texture in the cache + * + * @memberof PIXI.utils + * @function destroyTextureCache + */ +export function destroyTextureCache() +{ + let key; + + for (key in TextureCache) + { + TextureCache[key].destroy(); + } + for (key in BaseTextureCache) + { + BaseTextureCache[key].destroy(); + } +} + +/** + * Removes all textures from cache, but does not destroy them + * + * @memberof PIXI.utils + * @function clearTextureCache + */ +export function clearTextureCache() +{ + let key; + + for (key in TextureCache) + { + delete TextureCache[key]; + } + for (key in BaseTextureCache) + { + delete BaseTextureCache[key]; + } +} diff --git a/src/core/utils/mixin.js b/src/core/utils/mixin.js new file mode 100644 index 0000000..114de2a --- /dev/null +++ b/src/core/utils/mixin.js @@ -0,0 +1,59 @@ +/** + * Mixes all enumerable properties and methods from a source object to a target object. + * + * @memberof PIXI.utils.mixins + * @function mixin + * @param {object} target The prototype or instance that properties and methods should be added to. + * @param {object} source The source of properties and methods to mix in. + */ +export function mixin(target, source) +{ + if (!target || !source) return; + // in ES8/ES2017, this would be really easy: + // Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); + + // get all the enumerable property keys + const keys = Object.keys(source); + + // loop through properties + for (let i = 0; i < keys.length; ++i) + { + const propertyName = keys[i]; + + // Set the property using the property descriptor - this works for accessors and normal value properties + Object.defineProperty(target, propertyName, Object.getOwnPropertyDescriptor(source, propertyName)); + } +} + +const mixins = []; + +/** + * Queues a mixin to be handled towards the end of the initialization of PIXI, so that deprecation + * can take effect. + * + * @memberof PIXI.utils.mixins + * @function delayMixin + * @private + * @param {object} target The prototype or instance that properties and methods should be added to. + * @param {object} source The source of properties and methods to mix in. + */ +export function delayMixin(target, source) +{ + mixins.push(target, source); +} + +/** + * Handles all mixins queued via delayMixin(). + * + * @memberof PIXI.utils.mixins + * @function performMixins + * @private + */ +export function performMixins() +{ + for (let i = 0; i < mixins.length; i += 2) + { + mixin(mixins[i], mixins[i + 1]); + } + mixins.length = 0; +} diff --git a/src/deprecation.js b/src/deprecation.js index 045a19f..b9c73b3 100644 --- a/src/deprecation.js +++ b/src/deprecation.js @@ -5,6 +5,7 @@ import * as filters from './filters'; import * as prepare from './prepare'; import * as loaders from './loaders'; +import * as interaction from './interaction'; // provide method to give a stack track for warnings // useful for tracking-down where deprecated methods/properties/classes @@ -534,7 +535,7 @@ { parent: 'GC_MODES', target: 'GC_MODE' }, { parent: 'WRAP_MODES', target: 'WRAP_MODE' }, { parent: 'SCALE_MODES', target: 'SCALE_MODE' }, - { parent: 'PRECISION', target: 'PRECISION' }, + { parent: 'PRECISION', target: 'PRECISION_FRAGMENT' }, ]; for (let i = 0; i < defaults.length; i++) @@ -558,6 +559,32 @@ }); } +Object.defineProperties(core.settings, { + + /** + * @static + * @name PRECISION + * @memberof PIXI.settings + * @see PIXI.PRECISION + * @deprecated since version 4.4.0 + */ + PRECISION: { + enumerable: true, + get() + { + warn('PIXI.settings.PRECISION has been deprecated, please use PIXI.settings.PRECISION_FRAGMENT'); + + return core.settings.PRECISION_FRAGMENT; + }, + set(value) + { + warn('PIXI.settings.PRECISION has been deprecated, please use PIXI.settings.PRECISION_FRAGMENT'); + + core.settings.PRECISION_FRAGMENT = value; + }, + }, +}); + Object.defineProperties(extras, { /** @@ -694,7 +721,7 @@ warn('determineFontProperties is now deprecated, please use the static calculateFontProperties method, ' + 'e.g : Text.calculateFontProperties(fontStyle);'); - return Text.calculateFontProperties(fontStyle); + return core.Text.calculateFontProperties(fontStyle); }; Object.defineProperties(core.TextStyle.prototype, { @@ -1003,3 +1030,69 @@ }, }, }); + +/** + * @name PIXI.interaction.interactiveTarget#defaultCursor + * @static + * @type {number} + * @see PIXI.interaction.interactiveTarget#cursor + * @deprecated since 4.3.0 + */ +Object.defineProperty(interaction.interactiveTarget, 'defaultCursor', { + set(value) + { + warn('Property defaultCursor has been replaced with \'cursor\'. '); + this.cursor = value; + }, + get() + { + warn('Property defaultCursor has been replaced with \'cursor\'. '); + + return this.cursor; + }, + enumerable: true, +}); + +/** + * @name PIXI.interaction.InteractionManager#defaultCursorStyle + * @static + * @type {string} + * @see PIXI.interaction.InteractionManager#cursorStyles + * @deprecated since 4.3.0 + */ +Object.defineProperty(interaction.InteractionManager, 'defaultCursorStyle', { + set(value) + { + warn('Property defaultCursorStyle has been replaced with \'cursorStyles.default\'. '); + this.cursorStyles.default = value; + }, + get() + { + warn('Property defaultCursorStyle has been replaced with \'cursorStyles.default\'. '); + + return this.cursorStyles.default; + }, +}); + +/** + * @name PIXI.interaction.InteractionManager#currentCursorStyle + * @static + * @type {string} + * @see PIXI.interaction.InteractionManager#cursorStyles + * @deprecated since 4.3.0 + */ +Object.defineProperty(interaction.InteractionManager, 'currentCursorStyle', { + set(value) + { + warn('Property currentCursorStyle has been removed.' + + 'See the currentCursorMode property, which works differently.'); + this.currentCursorMode = value; + }, + get() + { + warn('Property currentCursorStyle has been removed.' + + 'See the currentCursorMode property, which works differently.'); + + return this.currentCursorMode; + }, +}); diff --git a/src/extras/BitmapText.js b/src/extras/BitmapText.js index fb6c343..0a0e150 100644 --- a/src/extras/BitmapText.js +++ b/src/extras/BitmapText.js @@ -455,6 +455,73 @@ return this._textHeight; } + + /** + * Register a bitmap font with data and a texture. + * + * @static + * @param {XMLDocument} xml - The XML document data. + * @param {PIXI.Texture} texture - Texture with all symbols. + * @return {Object} Result font object with font, size, lineHeight and char fields. + */ + static registerFont(xml, texture) + { + const data = {}; + const info = xml.getElementsByTagName('info')[0]; + const common = xml.getElementsByTagName('common')[0]; + + data.font = info.getAttribute('face'); + data.size = parseInt(info.getAttribute('size'), 10); + data.lineHeight = parseInt(common.getAttribute('lineHeight'), 10); + data.chars = {}; + + // parse letters + const letters = xml.getElementsByTagName('char'); + + for (let i = 0; i < letters.length; i++) + { + const letter = letters[i]; + const charCode = parseInt(letter.getAttribute('id'), 10); + + const textureRect = new core.Rectangle( + parseInt(letter.getAttribute('x'), 10) + texture.frame.x, + parseInt(letter.getAttribute('y'), 10) + texture.frame.y, + parseInt(letter.getAttribute('width'), 10), + parseInt(letter.getAttribute('height'), 10) + ); + + data.chars[charCode] = { + xOffset: parseInt(letter.getAttribute('xoffset'), 10), + yOffset: parseInt(letter.getAttribute('yoffset'), 10), + xAdvance: parseInt(letter.getAttribute('xadvance'), 10), + kerning: {}, + texture: new core.Texture(texture.baseTexture, textureRect), + + }; + } + + // parse kernings + const kernings = xml.getElementsByTagName('kerning'); + + for (let i = 0; i < kernings.length; i++) + { + const kerning = kernings[i]; + const first = parseInt(kerning.getAttribute('first'), 10); + const second = parseInt(kerning.getAttribute('second'), 10); + const amount = parseInt(kerning.getAttribute('amount'), 10); + + if (data.chars[second]) + { + data.chars[second].kerning[first] = amount; + } + } + + // I'm leaving this as a temporary fix so we can test the bitmap fonts in v3 + // but it's very likely to change + BitmapText.fonts[data.font] = data; + + return data; + } } BitmapText.fonts = {}; diff --git a/src/index.js b/src/index.js index 59356cf..c553427 100644 --- a/src/index.js +++ b/src/index.js @@ -16,6 +16,10 @@ import * as particles from './particles'; import * as prepare from './prepare'; +// handle mixins now, after all code has been added, including deprecation +import { utils } from './core'; +utils.mixins.performMixins(); + export { accessibility, extract, diff --git a/src/interaction/InteractionManager.js b/src/interaction/InteractionManager.js index 7d51134..8baf13a 100644 --- a/src/interaction/InteractionManager.js +++ b/src/interaction/InteractionManager.js @@ -1,18 +1,20 @@ 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'; -// Mix interactiveTarget into core.DisplayObject.prototype -Object.assign( +// Mix interactiveTarget into core.DisplayObject.prototype, after deprecation has been handled +core.utils.mixins.delayMixin( core.DisplayObject.prototype, 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 +70,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 +101,6 @@ this.eventData = new InteractionEvent(); /** - * Tiny little interactiveData pool ! - * - * @member {PIXI.interaction.InteractionData[]} - */ - this.interactiveDataPool = []; - - /** * The DOM element to bind to. * * @private @@ -153,66 +155,12 @@ */ this.supportsPointerEvents = !!window.PointerEvent; - /** - * Are touch events being 'normalized' and converted into pointer events if pointer events are not supported - * For example, on a touch screen mobile device, a touchstart would also be emitted as a pointerdown - * - * @private - * @readonly - * @member {boolean} - */ - this.normalizeTouchEvents = !this.supportsPointerEvents && this.supportsTouchEvents; - - /** - * Are mouse events being 'normalized' and converted into pointer events if pointer events are not supported - * For example, on a desktop pc, a mousedown would also be emitted as a pointerdown - * - * @private - * @readonly - * @member {boolean} - */ - this.normalizeMouseEvents = !this.supportsPointerEvents && !MobileDevice.any; - // this will make it so that you don't have to call bind all the time /** * @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 +168,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,41 +199,32 @@ this.onPointerOver = this.onPointerOver.bind(this); /** - * @private - * @member {Function} + * Dictionary of how different cursor modes are handled. Strings are handled as CSS cursor + * values, objects are handled as dictionaries of CSS values for interactionDOMElement, + * and functions are called instead of changing the CSS. + * Default CSS cursor values are provided for 'default' and 'pointer' modes. + * @member {Object.)>} */ - this.onTouchStart = this.onTouchStart.bind(this); - this.processTouchStart = this.processTouchStart.bind(this); + this.cursorStyles = { + default: 'inherit', + pointer: 'pointer', + }; /** - * @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. - * - * @member {string} - * @default 'inherit' - */ - this.defaultCursorStyle = 'inherit'; - - /** - * The css style of the cursor that is being used. + * The mode of the cursor that is being used. + * The value of this is a key from the cursorStyles dictionary. * * @member {string} */ - this.currentCursorStyle = 'inherit'; + this.currentCursorMode = null; + + /** + * Internal cached let. + * + * @private + * @member {string} + */ + this.cursor = null; /** * Internal cached let. @@ -303,6 +249,7 @@ * object. * * @event mousedown + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -311,6 +258,7 @@ * on the display object. * * @event rightdown + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -319,6 +267,7 @@ * object. * * @event mouseup + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -327,6 +276,7 @@ * over the display object. * * @event rightup + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -335,6 +285,7 @@ * the display object. * * @event click + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -343,6 +294,7 @@ * and released on the display object. * * @event rightclick + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -352,6 +304,7 @@ * [mousedown]{@link PIXI.interaction.InteractionManager#event:mousedown}. * * @event mouseupoutside + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -361,6 +314,7 @@ * [rightdown]{@link PIXI.interaction.InteractionManager#event:rightdown}. * * @event rightupoutside + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -368,6 +322,7 @@ * Fired when a pointer device (usually a mouse) is moved while over the display object * * @event mousemove + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -375,6 +330,7 @@ * Fired when a pointer device (usually a mouse) is moved onto the display object * * @event mouseover + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -382,6 +338,7 @@ * Fired when a pointer device (usually a mouse) is moved off the display object * * @event mouseout + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -389,6 +346,7 @@ * Fired when a pointer device button is pressed on the display object. * * @event pointerdown + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -396,6 +354,14 @@ * Fired when a pointer device button is released over the display object. * * @event pointerup + * @type {PIXI.interaction.InteractionData} + * @memberof PIXI.interaction.InteractionManager# + */ + + /** + * Fired when the operating system cancels a pointer event + * + * @event pointercancel * @memberof PIXI.interaction.InteractionManager# */ @@ -403,6 +369,7 @@ * Fired when a pointer device button is pressed and released on the display object. * * @event pointertap + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -411,6 +378,7 @@ * registered a [pointerdown]{@link PIXI.interaction.InteractionManager#event:pointerdown}. * * @event pointerupoutside + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -418,6 +386,7 @@ * Fired when a pointer device is moved while over the display object * * @event pointermove + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -425,6 +394,7 @@ * Fired when a pointer device is moved onto the display object * * @event pointerover + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -432,6 +402,7 @@ * Fired when a pointer device is moved off the display object * * @event pointerout + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -439,6 +410,7 @@ * Fired when a touch point is placed on the display object. * * @event touchstart + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -446,6 +418,14 @@ * Fired when a touch point is removed from the display object. * * @event touchend + * @type {PIXI.interaction.InteractionData} + * @memberof PIXI.interaction.InteractionManager# + */ + + /** + * Fired when the operating system cancels a touch + * + * @event touchcancel * @memberof PIXI.interaction.InteractionManager# */ @@ -453,6 +433,7 @@ * Fired when a touch point is placed and removed from the display object. * * @event tap + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -461,6 +442,7 @@ * registered a [touchstart]{@link PIXI.interaction.InteractionManager#event:touchstart}. * * @event touchendoutside + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -468,6 +450,7 @@ * Fired when a touch point is moved along the display object. * * @event touchmove + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ } @@ -524,45 +507,30 @@ { window.document.addEventListener('pointermove', this.onPointerMove, true); this.interactionDOMElement.addEventListener('pointerdown', this.onPointerDown, true); - this.interactionDOMElement.addEventListener('pointerout', this.onPointerOut, true); + // pointerout is fired in addition to pointerup (for touch events) and pointercancel + // we already handle those, so for the purposes of what we do in onPointerOut, we only + // care about the pointerleave event + this.interactionDOMElement.addEventListener('pointerleave', 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; @@ -596,45 +564,26 @@ { window.document.removeEventListener('pointermove', this.onPointerMove, true); this.interactionDOMElement.removeEventListener('pointerdown', this.onPointerDown, true); - this.interactionDOMElement.removeEventListener('pointerout', this.onPointerOut, true); + this.interactionDOMElement.removeEventListener('pointerleave', 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) + 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.onPointerDown, true); + this.interactionDOMElement.removeEventListener('touchcancel', this.onPointerCancel, 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); - - 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 = null; @@ -664,7 +613,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; @@ -672,25 +621,80 @@ return; } - this.cursor = this.defaultCursorStyle; + this.cursor = null; // 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(); - - this.processInteractive(this.mouse.global, this.renderer._lastObjectRendered, this.processMouseOverOut, true); - - if (this.currentCursorStyle !== this.cursor) + for (const k in this.activeInteractionData) { - this.currentCursorStyle = this.cursor; - this.interactionDOMElement.style.cursor = this.cursor; + // eslint-disable-next-line no-prototype-builtins + if (this.activeInteractionData.hasOwnProperty(k)) + { + const interactionData = this.activeInteractionData[k]; + + if (interactionData.originalEvent && interactionData.pointerType !== 'touch') + { + const interactionEvent = this.configureInteractionEventForDOMEvent( + this.eventData, + interactionData.originalEvent, + interactionData + ); + + this.processInteractive( + interactionEvent, + this.renderer._lastObjectRendered, + this.processPointerOverOut, + true + ); + } + } } + this.setCursorMode(this.cursor); + // TODO } /** + * Sets the current cursor mode, handling any callbacks or CSS style changes. + * + * @param {string} mode - cursor mode, a key from the cursorStyles dictionary + */ + setCursorMode(mode) + { + mode = mode || 'default'; + // if the mode didn't actually change, bail early + if (this.currentCursorMode === mode) + { + return; + } + this.currentCursorMode = mode; + const style = this.cursorStyles[mode]; + + // only do things if there is a cursor style for it + if (style) + { + switch (typeof style) + { + case 'string': + // string styles are handled as cursor CSS + this.interactionDOMElement.style.cursor = style; + break; + case 'function': + // functions are just called, and passed the cursor mode + style(mode); + break; + case 'object': + // if it is an object, assume that it is a dictionary of CSS styles, + // apply it to the interactionDOMElement + Object.assign(this.interactionDOMElement.style, style); + break; + } + } + } + + /** * Dispatches an event on the display object that was interacted with * * @param {PIXI.Container|PIXI.Sprite|PIXI.extras.TilingSprite} displayObject - the display object in question @@ -748,22 +752,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 @@ -787,9 +795,8 @@ { interactiveParent = false; } - - // it has a mask! Then lets hit test that before continuing.. - if (hitTest && displayObject._mask) + // it has a mask! Then lets hit test that before continuing + else if (hitTest && displayObject._mask) { if (!displayObject._mask.containsPoint(point)) { @@ -797,14 +804,7 @@ } } - // it has a filterArea! Same as mask but easier, its a rectangle - if (hitTest && displayObject.filterArea) - { - if (!displayObject.filterArea.contains(point.x, point.y)) - { - hitTest = false; - } - } + let keepHitTestingAfterChildren = hitTest; // ** FREE TIP **! If an object is not interactive or has no buttons in it // (such as a game scene!) set interactiveChildren to false for that displayObject. @@ -818,7 +818,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 @@ -838,15 +838,20 @@ // This means we no longer need to hit test anything else. We still need to run // through all objects, but we don't need to perform any hit tests. - // { - hitTest = false; - // } + keepHitTestingAfterChildren = false; + + if (child.interactive) + { + hitTest = false; + } // we can break now as we have hit an object. } } } + hitTest = keepHitTestingAfterChildren; + // no point running this if the item is not interactive or does not have an interactive parent. if (interactive) { @@ -868,14 +873,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 +886,175 @@ } /** - * 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].isNormalized) { - 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); + if (!displayObject.trackedPointers[id]) + { + displayObject.trackedPointers[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.trackedPointers[id].rightDown = true; + } + else + { + displayObject.trackedPointers[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.trackedPointers[id] !== undefined) + { + delete displayObject.trackedPointers[id]; + this.dispatchEvent(displayObject, 'pointercancel', interactionEvent); + + if (e.type === 'touchcancel' || e.pointerType === 'touch') + { + this.dispatchEvent(displayObject, 'touchcancel', interactionEvent); + } } } @@ -1159,42 +1066,94 @@ */ 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) { - if (hit) - { - this.dispatchEvent(displayObject, 'pointerup', this.eventData); + const e = interactionEvent.data.originalEvent; - if (displayObject._pointerDown) + const id = interactionEvent.data.identifier; + + const trackingData = displayObject.trackedPointers[id]; + + const isTouch = (e.type === 'touchend' || e.pointerType === 'touch'); + + const isMouse = (e.type.indexOf('mouse') === 0 || e.pointerType === 'mouse'); + + // 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) { - displayObject._pointerDown = false; - this.dispatchEvent(displayObject, 'pointertap', this.eventData); + 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); + } + // update the down state of the tracking data + if (trackingData) + { + if (isRightButton) + { + trackingData.rightDown = hit; + } + else + { + trackingData.leftDown = hit; + } } } - else if (displayObject._pointerDown) + + // Pointers and Touches, and Mouse + if (hit) { - displayObject._pointerDown = false; - this.dispatchEvent(displayObject, 'pointerupoutside', this.eventData); + this.dispatchEvent(displayObject, 'pointerup', interactionEvent); + if (isTouch) this.dispatchEvent(displayObject, 'touchend', interactionEvent); + + if (trackingData) + { + this.dispatchEvent(displayObject, 'pointertap', interactionEvent); + if (isTouch) + { + this.dispatchEvent(displayObject, 'tap', interactionEvent); + // touches are no longer over (if they ever were) when we get the touchend + // so we should ensure that we don't keep pretending that they are + trackingData.over = false; + } + } + } + else if (trackingData) + { + this.dispatchEvent(displayObject, 'pointerupoutside', interactionEvent); + if (isTouch) this.dispatchEvent(displayObject, 'touchendoutside', interactionEvent); + } + // Only remove the tracking data if there is no over/down state still associated with it + if (trackingData && trackingData.none) + { + delete displayObject.trackedPointers[id]; } } @@ -1202,39 +1161,78 @@ * 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 = null; + } - 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') + { + this.setCursorMode(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 (isMouse) { - 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 +1240,94 @@ * 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.setCursorMode(null); + } - 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'); + + let trackingData = displayObject.trackedPointers[id]; + + // if we just moused over the display object, then we need to track that state + if (hit && !trackingData) + { + trackingData = displayObject.trackedPointers[id] = new InteractionTrackingData(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); + } + } + + // only change the cursor if it has not already been changed (by something deeper in the + // display tree) + if (isMouse && this.cursor === null) + { + this.cursor = displayObject.cursor; } } - 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); + } + // if there is no mouse down information for the pointer, then it is safe to delete + if (trackingData.none) + { + delete displayObject.trackedPointers[id]; + } } } @@ -1287,253 +1335,154 @@ * 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 && pointerEvent.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; + + // mark the touch as normalized, just so that we know we did it + touch.isNormalized = true; + + normalizedEvents.push(touch); + } } - else if (this.normalizeMouseEvents) + // apparently PointerEvent subclasses MouseEvent, so yay + else if (event instanceof MouseEvent && (!this.supportsPointerEvents || !(event instanceof window.PointerEvent))) { if (typeof event.isPrimary === 'undefined') event.isPrimary = true; if (typeof event.width === 'undefined') event.width = 1; @@ -1541,10 +1490,21 @@ 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; + + // mark the mouse event as normalized, just so that we know we did it + event.isNormalized = true; + + normalizedEvents.push(event); } + else + { + normalizedEvents.push(event); + } + + return normalizedEvents; } /** @@ -1563,30 +1523,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 +1542,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..6a1f69e --- /dev/null +++ b/src/interaction/InteractionTrackingData.js @@ -0,0 +1,147 @@ +/** + * 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 inactive (not over or down)? + * + * @member {number} + * @memberof PIXI.interaction.InteractionTrackingData# + */ + get none() + { + return this._flags === this.constructor.FLAGS.NONE; + } + + /** + * 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..d252fba 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 * ); */ @@ -38,68 +38,56 @@ hitArea: null, /** - * If enabled, the mouse cursor will change when hovered over the displayObject if it is interactive + * If enabled, the mouse cursor use the pointer behavior when hovered over the displayObject if it is interactive + * Setting this changes the 'cursor' property to `'pointer'`. * - * @inner {boolean} + * @member {boolean} + * @memberof PIXI.interaction.interactiveTarget# */ - buttonMode: false, + get buttonMode() + { + return this.cursor === 'pointer'; + }, + set buttonMode(value) + { + if (value) + { + this.cursor = 'pointer'; + } + else if (this.cursor === 'pointer') + { + this.cursor = null; + } + }, /** - * If buttonMode is enabled, this defines what CSS cursor property is used when the mouse cursor - * is hovered over the displayObject + * This defines what cursor mode is used when the mouse cursor + * is hovered over the displayObject. * * @see https://developer.mozilla.org/en/docs/Web/CSS/cursor * * @inner {string} */ - defaultCursor: 'pointer', - - // some internal checks.. - /** - * Internal check to detect if the mouse cursor is hovered over the displayObject - * - * @inner {boolean} - * @private - */ - _over: false, + cursor: null, /** - * Internal check to detect if the left mouse button is pressed on the displayObject + * Internal set of all active pointers, by identifier * - * @inner {boolean} + * @member {Map} + * @memberof PIXI.interaction.interactiveTarget# * @private */ - _isLeftDown: false, + get trackedPointers() + { + if (this._trackedPointers === undefined) this._trackedPointers = {}; + + return this._trackedPointers; + }, /** - * Internal check to detect if the right mouse button is pressed on the displayObject + * Map of all tracked pointers, by identifier. Use trackedPointers to access. * - * @inner {boolean} - * @private + * @private {Map} */ - _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, + _trackedPointers: undefined, }; diff --git a/src/loaders/bitmapFontParser.js b/src/loaders/bitmapFontParser.js index cc2618c..f4cf996 100644 --- a/src/loaders/bitmapFontParser.js +++ b/src/loaders/bitmapFontParser.js @@ -1,63 +1,19 @@ import * as path from 'path'; -import { Rectangle, Texture, utils } from '../core'; +import { utils } from '../core'; import { Resource } from 'resource-loader'; import { BitmapText } from '../extras'; +/** + * Register a BitmapText font from loader resource. + * + * @function parseBitmapFontData + * @memberof PIXI.loaders + * @param {PIXI.loaders.Resource} resource - Loader resource. + * @param {PIXI.Texture} texture - Reference to texture. + */ export function parse(resource, texture) { - const data = {}; - const info = resource.data.getElementsByTagName('info')[0]; - const common = resource.data.getElementsByTagName('common')[0]; - - data.font = info.getAttribute('face'); - data.size = parseInt(info.getAttribute('size'), 10); - data.lineHeight = parseInt(common.getAttribute('lineHeight'), 10); - data.chars = {}; - - // parse letters - const letters = resource.data.getElementsByTagName('char'); - - for (let i = 0; i < letters.length; i++) - { - const charCode = parseInt(letters[i].getAttribute('id'), 10); - - const textureRect = new Rectangle( - parseInt(letters[i].getAttribute('x'), 10) + texture.frame.x, - parseInt(letters[i].getAttribute('y'), 10) + texture.frame.y, - parseInt(letters[i].getAttribute('width'), 10), - parseInt(letters[i].getAttribute('height'), 10) - ); - - data.chars[charCode] = { - xOffset: parseInt(letters[i].getAttribute('xoffset'), 10), - yOffset: parseInt(letters[i].getAttribute('yoffset'), 10), - xAdvance: parseInt(letters[i].getAttribute('xadvance'), 10), - kerning: {}, - texture: new Texture(texture.baseTexture, textureRect), - - }; - } - - // parse kernings - const kernings = resource.data.getElementsByTagName('kerning'); - - for (let i = 0; i < kernings.length; i++) - { - const first = parseInt(kernings[i].getAttribute('first'), 10); - const second = parseInt(kernings[i].getAttribute('second'), 10); - const amount = parseInt(kernings[i].getAttribute('amount'), 10); - - if (data.chars[second]) - { - data.chars[second].kerning[first] = amount; - } - } - - resource.bitmapFont = data; - - // I'm leaving this as a temporary fix so we can test the bitmap fonts in v3 - // but it's very likely to change - BitmapText.fonts[data.font] = data; + resource.bitmapFont = BitmapText.registerFont(resource.data, texture); } export default function () diff --git a/src/loaders/index.js b/src/loaders/index.js index 8942436..5189793 100644 --- a/src/loaders/index.js +++ b/src/loaders/index.js @@ -5,4 +5,11 @@ export { default as bitmapFontParser, parse as parseBitmapFontData } from './bitmapFontParser'; export { default as spritesheetParser } from './spritesheetParser'; export { default as textureParser } from './textureParser'; + +/** + * Reference to **resource-loader**'s Resource class. + * See https://github.com/englercj/resource-loader + * @class Resource + * @memberof PIXI.loaders + */ export { Resource } from 'resource-loader'; diff --git a/src/loaders/loader.js b/src/loaders/loader.js index 0a670cb..767a411 100644 --- a/src/loaders/loader.js +++ b/src/loaders/loader.js @@ -7,20 +7,47 @@ /** * - * The new loader, extends Resource Loader by Chad Engler : https://github.com/englercj/resource-loader + * The new loader, extends Resource Loader by Chad Engler: https://github.com/englercj/resource-loader * * ```js - * let loader = PIXI.loader; // pixi exposes a premade instance for you to use. + * const loader = PIXI.loader; // pixi exposes a premade instance for you to use. * //or - * let loader = new PIXI.loaders.Loader(); // you can also create your own if you want + * const loader = new PIXI.loaders.Loader(); // you can also create your own if you want * - * loader.add('bunny', 'data/bunny.png'); - * loader.add('spaceship', 'assets/spritesheet.json'); + * const sprites = {}; + * + * // Chainable `add` to enqueue a resource + * loader.add('bunny', 'data/bunny.png') + * .add('spaceship', 'assets/spritesheet.json'); * loader.add('scoreFont', 'assets/score.fnt'); * - * loader.once('complete',onAssetsLoaded); + * // Chainable `pre` to add a middleware that runs for each resource, *before* loading that resource. + * // This is useful to implement custom caching modules (using filesystem, indexeddb, memory, etc). + * loader.pre(cachingMiddleware); * - * loader.load(); + * // Chainable `use` to add a middleware that runs for each resource, *after* loading that resource. + * // This is useful to implement custom parsing modules (like spritesheet parsers, spine parser, etc). + * loader.use(parsingMiddleware); + * + * // The `load` method loads the queue of resources, and calls the passed in callback called once all + * // resources have loaded. + * loader.load((loader, resources) => { + * // resources is an object where the key is the name of the resource loaded and the value is the resource object. + * // They have a couple default properties: + * // - `url`: The URL that the resource was loaded from + * // - `error`: The error that happened when trying to load (if any) + * // - `data`: The raw data that was loaded + * // also may contain other properties based on the middleware that runs. + * sprites.bunny = new PIXI.TilingSprite(resources.bunny.texture); + * sprites.spaceship = new PIXI.TilingSprite(resources.spaceship.texture); + * sprites.scoreFont = new PIXI.TilingSprite(resources.scoreFont.texture); + * }); + * + * // throughout the process multiple signals can be dispatched. + * loader.onProgress.add(() => {}); // called once per loaded/errored file + * loader.onError.add(() => {}); // called once per errored file + * loader.onLoad.add(() => {}); // called once per loaded file + * loader.onComplete.add(() => {}); // called once when the queued resources all load. * ``` * * @see https://github.com/englercj/resource-loader diff --git a/src/loaders/spritesheetParser.js b/src/loaders/spritesheetParser.js index 7570332..e57dfb2 100644 --- a/src/loaders/spritesheetParser.js +++ b/src/loaders/spritesheetParser.js @@ -1,8 +1,6 @@ import { Resource } from 'resource-loader'; import path from 'path'; -import * as core from '../core'; - -const BATCH_SIZE = 1000; +import { Spritesheet } from '../core'; export default function () { @@ -43,134 +41,18 @@ // load the image for this sheet this.add(imageResourceName, resourcePath, loadOptions, function onImageLoad(res) { - resource.textures = {}; + const spritesheet = new Spritesheet( + res.texture.baseTexture, + resource.data, + resource.url + ); - const frames = resource.data.frames; - const frameKeys = Object.keys(frames); - const baseTexture = res.texture.baseTexture; - const scale = resource.data.meta.scale; - - // Use a defaultValue of `null` to check if a url-based resolution is set - let resolution = core.utils.getResolutionOfUrl(resource.url, null); - - // No resolution found via URL - if (resolution === null) + spritesheet.parse(() => { - // Use the scale value or default to 1 - resolution = scale !== undefined ? scale : 1; - } - - // For non-1 resolutions, update baseTexture - if (resolution !== 1) - { - baseTexture.resolution = resolution; - baseTexture.update(); - } - - let batchIndex = 0; - - function processFrames(initialFrameIndex, maxFrames) - { - let frameIndex = initialFrameIndex; - - while (frameIndex - initialFrameIndex < maxFrames && frameIndex < frameKeys.length) - { - const i = frameKeys[frameIndex]; - const rect = frames[i].frame; - - if (rect) - { - let frame = null; - let trim = null; - const orig = new core.Rectangle( - 0, - 0, - frames[i].sourceSize.w / resolution, - frames[i].sourceSize.h / resolution - ); - - if (frames[i].rotated) - { - frame = new core.Rectangle( - rect.x / resolution, - rect.y / resolution, - rect.h / resolution, - rect.w / resolution - ); - } - else - { - frame = new core.Rectangle( - rect.x / resolution, - rect.y / resolution, - rect.w / resolution, - rect.h / resolution - ); - } - - // Check to see if the sprite is trimmed - if (frames[i].trimmed) - { - trim = new core.Rectangle( - frames[i].spriteSourceSize.x / resolution, - frames[i].spriteSourceSize.y / resolution, - rect.w / resolution, - rect.h / resolution - ); - } - - resource.textures[i] = new core.Texture( - baseTexture, - frame, - orig, - trim, - frames[i].rotated ? 2 : 0 - ); - - // lets also add the frame to pixi's global cache for fromFrame and fromImage functions - core.utils.TextureCache[i] = resource.textures[i]; - } - - frameIndex++; - } - } - - function shouldProcessNextBatch() - { - return batchIndex * BATCH_SIZE < frameKeys.length; - } - - function processNextBatch(done) - { - processFrames(batchIndex * BATCH_SIZE, BATCH_SIZE); - batchIndex++; - setTimeout(done, 0); - } - - function iteration() - { - processNextBatch(() => - { - if (shouldProcessNextBatch()) - { - iteration(); - } - else - { - next(); - } - }); - } - - if (frameKeys.length <= BATCH_SIZE) - { - processFrames(0, BATCH_SIZE); + resource.spritesheet = spritesheet; + resource.textures = spritesheet.textures; next(); - } - else - { - iteration(); - } + }); }); }; } diff --git a/src/loaders/textureParser.js b/src/loaders/textureParser.js index 5398a7f..480edc1 100644 --- a/src/loaders/textureParser.js +++ b/src/loaders/textureParser.js @@ -1,5 +1,5 @@ -import * as core from '../core'; import { Resource } from 'resource-loader'; +import Texture from '../core/textures/Texture'; export default function () { @@ -8,23 +8,12 @@ // create a new texture if the data is an Image object if (resource.data && resource.type === Resource.TYPE.IMAGE) { - const baseTexture = new core.BaseTexture(resource.data, null, core.utils.getResolutionOfUrl(resource.url)); - - baseTexture.imageUrl = resource.url; - resource.texture = new core.Texture(baseTexture); - - // lets also add the frame to pixi's global cache for fromFrame and fromImage fucntions - core.utils.BaseTextureCache[resource.name] = baseTexture; - core.utils.TextureCache[resource.name] = resource.texture; - - // also add references by url if they are different. - if (resource.name !== resource.url) - { - core.utils.BaseTextureCache[resource.url] = baseTexture; - core.utils.TextureCache[resource.url] = resource.texture; - } + resource.texture = Texture.fromLoader( + resource.data, + resource.url, + resource.name + ); } - next(); }; } diff --git a/package.json b/package.json index 6a366ba..71ade15 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pixi.js", - "version": "4.3.4", + "version": "4.3.5", "description": "Pixi.js is a fast lightweight 2D library that works across all devices.", "author": "Mat Groves", "contributors": [ @@ -61,7 +61,7 @@ "ismobilejs": "^0.4.0", "object-assign": "^4.0.1", "pixi-gl-core": "^1.0.3", - "resource-loader": "^2.0.4" + "resource-loader": "^2.0.6" }, "devDependencies": { "babel-cli": "^6.18.0", @@ -70,9 +70,9 @@ "babel-preset-es2015": "^6.14.0", "babelify": "^7.3.0", "del": "^2.2.0", - "electron-prebuilt": "^1.3.2", + "electron": "^1.4.15", "eslint": "^3.5.0", - "floss": "^1.2.0", + "floss": "^2.0.1", "gh-pages": "^0.11.0", "jaguarjs-jsdoc": "^1.0.1", "js-md5": "^0.4.1", diff --git a/src/accessibility/AccessibilityManager.js b/src/accessibility/AccessibilityManager.js index df2716a..f16bcb1 100644 --- a/src/accessibility/AccessibilityManager.js +++ b/src/accessibility/AccessibilityManager.js @@ -3,7 +3,7 @@ import accessibleTarget from './accessibleTarget'; // add some extra variables to the container.. -Object.assign( +core.utils.mixins.delayMixin( core.DisplayObject.prototype, accessibleTarget ); diff --git a/src/core/Application.js b/src/core/Application.js index ea0f058..5340578 100644 --- a/src/core/Application.js +++ b/src/core/Application.js @@ -1,6 +1,6 @@ import { autoDetectRenderer } from './autoDetectRenderer'; import Container from './display/Container'; -import Ticker from './ticker/Ticker'; +import { shared, Ticker } from './ticker'; /** * Convenience class to create a new PIXI application. @@ -35,9 +35,9 @@ * @param {boolean} [noWebGL=false] - prevents selection of WebGL renderer, even if such is present * @param {boolean} [options.legacy=false] - If true Pixi will aim to ensure compatibility * with older / less advanced devices. If you experience unexplained flickering try setting this to true. - + * @param {boolean} [useSharedTicker=false] - `true` to use PIXI.ticker.shared, `false` to create new ticker. */ - constructor(width, height, options, noWebGL) + constructor(width, height, options, noWebGL, useSharedTicker = false) { /** * WebGL renderer if available, otherwise CanvasRenderer @@ -46,23 +46,46 @@ this.renderer = autoDetectRenderer(width, height, options, noWebGL); /** - * The root display container that's renderered. + * The root display container that's rendered. * @member {PIXI.Container} */ this.stage = new Container(); /** + * Internal reference to the ticker + * @member {PIXI.ticker.Ticker} + * @private + */ + this._ticker = null; + + /** * Ticker for doing render updates. * @member {PIXI.ticker.Ticker} + * @default PIXI.ticker.shared */ - this.ticker = new Ticker(); - - this.ticker.add(this.render, this); + this.ticker = useSharedTicker ? shared : new Ticker(); // Start the rendering this.start(); } + set ticker(ticker) // eslint-disable-line require-jsdoc + { + if (this._ticker) + { + this._ticker.remove(this.render, this); + } + this._ticker = ticker; + if (ticker) + { + ticker.add(this.render, this); + } + } + get ticker() // eslint-disable-line require-jsdoc + { + return this._ticker; + } + /** * Render the current stage. */ @@ -76,7 +99,7 @@ */ stop() { - this.ticker.stop(); + this._ticker.stop(); } /** @@ -84,7 +107,7 @@ */ start() { - this.ticker.start(); + this._ticker.start(); } /** @@ -98,13 +121,22 @@ } /** + * Reference to the renderer's screen rectangle. Its safe to use as filterArea or hitArea for whole screen + * @member {PIXI.Rectangle} + * @readonly + */ + get screen() + { + return this.renderer.screen; + } + + /** * Destroy and don't use after this. * @param {Boolean} [removeView=false] Automatically remove canvas from DOM. */ destroy(removeView) { this.stop(); - this.ticker.remove(this.render, this); this.ticker = null; this.stage.destroy(); diff --git a/src/core/Shader.js b/src/core/Shader.js new file mode 100644 index 0000000..78b17e6 --- /dev/null +++ b/src/core/Shader.js @@ -0,0 +1,46 @@ +import { GLShader } from 'pixi-gl-core'; +import settings from './settings'; + +function checkPrecision(src, def) +{ + if (src instanceof Array) + { + if (src[0].substring(0, 9) !== 'precision') + { + const copy = src.slice(0); + + copy.unshift(`precision ${def} float;`); + + return copy; + } + } + else if (src.substring(0, 9) !== 'precision') + { + return `precision ${def} float;\n${src}`; + } + + return src; +} + +/** + * Wrapper class, webGL Shader for Pixi. + * Adds precision string if vertexSrc or fragmentSrc have no mention of it. + * + * @class + * @extends GLShader + * @memberof PIXI + */ +export default class Shader extends GLShader +{ + /** + * + * @param {WebGLRenderingContext} gl - The current WebGL rendering context + * @param {string|string[]} vertexSrc - The vertex shader source as an array of strings. + * @param {string|string[]} fragmentSrc - The fragment shader source as an array of strings. + */ + constructor(gl, vertexSrc, fragmentSrc) + { + super(gl, checkPrecision(vertexSrc, settings.PRECISION_VERTEX), + checkPrecision(fragmentSrc, settings.PRECISION_FRAGMENT)); + } +} diff --git a/src/core/const.js b/src/core/const.js index ef6f662..3de0efe 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -188,7 +188,7 @@ * The gc modes that are supported by pixi. * * The {@link PIXI.settings.GC_MODE} Garbage Collection mode for pixi textures is AUTO - * If set to GC_MODE, the renderer will occasianally check textures usage. If they are not + * If set to GC_MODE, the renderer will occasionally check textures usage. If they are not * used for a specified period of time they will be removed from the GPU. They will of course * be uploaded again when they are required. This is a silent behind the scenes process that * should ensure that the GPU does not get filled up. diff --git a/src/core/display/Container.js b/src/core/display/Container.js index ed8cd5f..17fde8d 100644 --- a/src/core/display/Container.js +++ b/src/core/display/Container.js @@ -73,11 +73,12 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.push(child); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -108,11 +109,12 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.splice(index, 0, child); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -225,10 +227,11 @@ if (index === -1) return null; child.parent = null; + // ensure child transform will be recalculated + child.transform._parentID = -1; removeItems(this.children, index, 1); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -249,11 +252,12 @@ { const child = this.getChildAt(index); + // ensure child transform will be recalculated.. child.parent = null; + child.transform._parentID = -1; removeItems(this.children, index, 1); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -284,13 +288,12 @@ for (let i = 0; i < removed.length; ++i) { removed[i].parent = null; + if (removed[i].transform) + { + removed[i].transform._parentID = -1; + } } - // ensure a transform will be recalculated.. - if (this.transform) - { - this.transform._parentID = -1; - } this._boundsID++; this.onChildrenChange(beginIndex); diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index f37f2f5..be4c4fb 100644 --- a/src/core/graphics/Graphics.js +++ b/src/core/graphics/Graphics.js @@ -28,8 +28,9 @@ { /** * + * @param {boolean} [nativeLines=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP */ - constructor() + constructor(nativeLines = false) { super(); @@ -50,6 +51,13 @@ this.lineWidth = 0; /** + * If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * + * @member {boolean} + */ + this.nativeLines = nativeLines; + + /** * The color of any lines drawn. * * @member {string} @@ -757,28 +765,14 @@ if (!this._spriteRect) { - if (!Graphics._SPRITE_TEXTURE) - { - Graphics._SPRITE_TEXTURE = RenderTexture.create(10, 10); - - const canvas = document.createElement('canvas'); - - canvas.width = 10; - canvas.height = 10; - - const context = canvas.getContext('2d'); - - context.fillStyle = 'white'; - context.fillRect(0, 0, 10, 10); - - Graphics._SPRITE_TEXTURE = Texture.fromCanvas(canvas); - } - - this._spriteRect = new Sprite(Graphics._SPRITE_TEXTURE); + this._spriteRect = new Sprite(new Texture(Texture.WHITE)); } + + const sprite = this._spriteRect; + if (this.tint === 0xffffff) { - this._spriteRect.tint = this.graphicsData[0].fillColor; + sprite.tint = this.graphicsData[0].fillColor; } else { @@ -792,20 +786,21 @@ t1[1] *= t2[1]; t1[2] *= t2[2]; - this._spriteRect.tint = rgb2hex(t1); + sprite.tint = rgb2hex(t1); } - this._spriteRect.alpha = this.graphicsData[0].fillAlpha; - this._spriteRect.worldAlpha = this.worldAlpha * this._spriteRect.alpha; + sprite.alpha = this.graphicsData[0].fillAlpha; + sprite.worldAlpha = this.worldAlpha * sprite.alpha; + sprite.blendMode = this.blendMode; - Graphics._SPRITE_TEXTURE._frame.width = rect.width; - Graphics._SPRITE_TEXTURE._frame.height = rect.height; + sprite.texture._frame.width = rect.width; + sprite.texture._frame.height = rect.height; - this._spriteRect.transform.worldTransform = this.transform.worldTransform; + sprite.transform.worldTransform = this.transform.worldTransform; - this._spriteRect.anchor.set(-rect.x / rect.width, -rect.y / rect.height); - this._spriteRect._onAnchorUpdate(); + sprite.anchor.set(-rect.x / rect.width, -rect.y / rect.height); + sprite._onAnchorUpdate(); - this._spriteRect._renderWebGL(renderer); + sprite._renderWebGL(renderer); } /** @@ -1031,6 +1026,7 @@ this.fillColor, this.fillAlpha, this.filling, + this.nativeLines, shape ); diff --git a/src/core/graphics/GraphicsData.js b/src/core/graphics/GraphicsData.js index f5a87ae..b6f03c0 100644 --- a/src/core/graphics/GraphicsData.js +++ b/src/core/graphics/GraphicsData.js @@ -14,14 +14,19 @@ * @param {number} fillColor - the color of the fill * @param {number} fillAlpha - the alpha of the fill * @param {boolean} fill - whether or not the shape is filled with a colour + * @param {boolean} nativeLines - the method for drawing lines * @param {PIXI.Circle|PIXI.Rectangle|PIXI.Ellipse|PIXI.Polygon} shape - The shape object to draw. */ - constructor(lineWidth, lineColor, lineAlpha, fillColor, fillAlpha, fill, shape) + constructor(lineWidth, lineColor, lineAlpha, fillColor, fillAlpha, fill, nativeLines, shape) { /** * @member {number} the width of the line to draw */ this.lineWidth = lineWidth; + /** + * @member {boolean} if true the liens will be draw using LINES instead of TRIANGLE_STRIP + */ + this.nativeLines = nativeLines; /** * @member {number} the color of the line to draw @@ -85,6 +90,7 @@ this.fillColor, this.fillAlpha, this.fill, + this.nativeLines, this.shape ); } diff --git a/src/core/graphics/webgl/GraphicsRenderer.js b/src/core/graphics/webgl/GraphicsRenderer.js index 14083fe..67f5e92 100644 --- a/src/core/graphics/webgl/GraphicsRenderer.js +++ b/src/core/graphics/webgl/GraphicsRenderer.js @@ -102,7 +102,15 @@ shaderTemp.uniforms.alpha = graphics.worldAlpha; renderer.bindVao(webGLData.vao); - webGLData.vao.draw(gl.TRIANGLE_STRIP, webGLData.indices.length); + + if (graphics.nativeLines) + { + gl.drawArrays(gl.LINES, 0, webGLData.points.length / 6); + } + else + { + webGLData.vao.draw(gl.TRIANGLE_STRIP, webGLData.indices.length); + } } } diff --git a/src/core/graphics/webgl/utils/buildLine.js b/src/core/graphics/webgl/utils/buildLine.js index ecf43f7..4d6862e 100644 --- a/src/core/graphics/webgl/utils/buildLine.js +++ b/src/core/graphics/webgl/utils/buildLine.js @@ -11,7 +11,29 @@ * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties * @param {object} webGLData - an object containing all the webGL-specific information to create this shape */ -export default function buildLine(graphicsData, webGLData) +export default function (graphicsData, webGLData) +{ + if (graphicsData.nativeLines) + { + buildNativeLine(graphicsData, webGLData); + } + else + { + buildLine(graphicsData, webGLData); + } +} + +/** + * Builds a line to draw using the poligon method. + * + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @private + * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties + * @param {object} webGLData - an object containing all the webGL-specific information to create this shape + */ +function buildLine(graphicsData, webGLData) { // TODO OPTIMISE! let points = graphicsData.points; @@ -224,3 +246,46 @@ indices.push(indexStart - 1); } + +/** + * Builds a line to draw using the gl.drawArrays(gl.LINES) method + * + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @private + * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties + * @param {object} webGLData - an object containing all the webGL-specific information to create this shape + */ +function buildNativeLine(graphicsData, webGLData) +{ + let i = 0; + const points = graphicsData.points; + + if (points.length === 0) return; + + const verts = webGLData.points; + const length = points.length / 2; + + // sort color + const color = hex2rgb(graphicsData.lineColor); + const alpha = graphicsData.lineAlpha; + const r = color[0] * alpha; + const g = color[1] * alpha; + const b = color[2] * alpha; + + for (i = 1; i < length; i++) + { + const p1x = points[(i - 1) * 2]; + const p1y = points[((i - 1) * 2) + 1]; + + const p2x = points[i * 2]; + const p2y = points[(i * 2) + 1]; + + verts.push(p1x, p1y); + verts.push(r, g, b, alpha); + + verts.push(p2x, p2y); + verts.push(r, g, b, alpha); + } +} diff --git a/src/core/graphics/webgl/utils/buildRoundedRectangle.js b/src/core/graphics/webgl/utils/buildRoundedRectangle.js index 9abc807..a6c8461 100644 --- a/src/core/graphics/webgl/utils/buildRoundedRectangle.js +++ b/src/core/graphics/webgl/utils/buildRoundedRectangle.js @@ -77,6 +77,26 @@ } /** + * Calculate a single point for a quadratic bezier curve. + * Utility function used by quadraticBezierCurve. + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @private + * @param {number} n1 - first number + * @param {number} n2 - second number + * @param {number} perc - percentage + * @return {number} the result + * + */ +function getPt(n1, n2, perc) +{ + const diff = n2 - n1; + + return n1 + (diff * perc); +} + +/** * Calculate the points for a quadratic bezier curve. (helper function..) * Based on: https://stackoverflow.com/questions/785097/how-do-i-implement-a-bezier-curve-in-c * @@ -105,13 +125,6 @@ let x = 0; let y = 0; - function getPt(n1, n2, perc) - { - const diff = n2 - n1; - - return n1 + (diff * perc); - } - for (let i = 0, j = 0; i <= n; ++i) { j = i / n; diff --git a/src/core/index.js b/src/core/index.js index f7a817c..4f414e8 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -30,6 +30,7 @@ export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; export { default as CanvasGraphicsRenderer } from './graphics/canvas/CanvasGraphicsRenderer'; +export { default as Spritesheet } from './textures/Spritesheet'; export { default as Texture } from './textures/Texture'; export { default as BaseTexture } from './textures/BaseTexture'; export { default as RenderTexture } from './textures/RenderTexture'; diff --git a/src/core/math/Matrix.js b/src/core/math/Matrix.js index 0b77477..3679860 100644 --- a/src/core/math/Matrix.js +++ b/src/core/math/Matrix.js @@ -13,45 +13,50 @@ export default class Matrix { /** - * + * @param {number} [a=1] - x scale + * @param {number} [b=0] - y skew + * @param {number} [c=0] - x skew + * @param {number} [d=1] - y scale + * @param {number} [tx=0] - x translation + * @param {number} [ty=0] - y translation */ - constructor() + constructor(a = 1, b = 0, c = 0, d = 1, tx = 0, ty = 0) { /** * @member {number} * @default 1 */ - this.a = 1; + this.a = a; /** * @member {number} * @default 0 */ - this.b = 0; + this.b = b; /** * @member {number} * @default 0 */ - this.c = 0; + this.c = c; /** * @member {number} * @default 1 */ - this.d = 1; + this.d = d; /** * @member {number} * @default 0 */ - this.tx = 0; + this.tx = tx; /** * @member {number} * @default 0 */ - this.ty = 0; + this.ty = ty; this.array = null; } diff --git a/src/core/renderers/SystemRenderer.js b/src/core/renderers/SystemRenderer.js index fa116a6..a24c775 100644 --- a/src/core/renderers/SystemRenderer.js +++ b/src/core/renderers/SystemRenderer.js @@ -1,5 +1,5 @@ import { sayHello, hex2string, hex2rgb } from '../utils'; -import { Matrix } from '../math'; +import { Matrix, Rectangle } from '../math'; import { RENDERER_TYPE } from '../const'; import settings from '../settings'; import Container from '../display/Container'; @@ -21,8 +21,8 @@ { /** * @param {string} system - The name of the system this renderer is for. - * @param {number} [width=800] - the width of the canvas view - * @param {number} [height=600] - the height of the canvas view + * @param {number} [screenWidth=800] - the width of the screen + * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false @@ -37,7 +37,7 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(system, width, height, options) + constructor(system, screenWidth, screenHeight, options) { super(); @@ -69,20 +69,13 @@ this.type = RENDERER_TYPE.UNKNOWN; /** - * The width of the canvas view + * Measurements of the screen. (0, 0, screenWidth, screenHeight) * - * @member {number} - * @default 800 - */ - this.width = width || 800; - - /** - * The height of the canvas view + * Its safe to use as filterArea or hitArea for whole stage * - * @member {number} - * @default 600 + * @member {PIXI.Rectangle} */ - this.height = height || 600; + this.screen = new Rectangle(0, 0, screenWidth || 800, screenHeight || 600); /** * The canvas element that everything is drawn to @@ -107,7 +100,7 @@ this.transparent = options.transparent; /** - * Whether the render view should be resized automatically + * Whether css dimensions of canvas view should be resized to screen dimensions automatically * * @member {boolean} */ @@ -192,23 +185,48 @@ } /** - * Resizes the canvas view to the specified width and height + * Same as view.width, actual number of pixels in the canvas by horizontal * - * @param {number} width - the new width of the canvas view - * @param {number} height - the new height of the canvas view + * @member {number} + * @readonly + * @default 800 */ - resize(width, height) + get width() { - this.width = width * this.resolution; - this.height = height * this.resolution; + return this.view.width; + } - this.view.width = this.width; - this.view.height = this.height; + /** + * Same as view.height, actual number of pixels in the canvas by vertical + * + * @member {number} + * @readonly + * @default 600 + */ + get height() + { + return this.view.height; + } + + /** + * Resizes the screen and canvas to the specified width and height + * Canvas dimensions are multiplied by resolution + * + * @param {number} screenWidth - the new width of the screen + * @param {number} screenHeight - the new height of the screen + */ + resize(screenWidth, screenHeight) + { + this.screen.width = screenWidth; + this.screen.height = screenHeight; + + this.view.width = screenWidth * this.resolution; + this.view.height = screenHeight * this.resolution; if (this.autoResize) { - this.view.style.width = `${this.width / this.resolution}px`; - this.view.style.height = `${this.height / this.resolution}px`; + this.view.style.width = `${screenWidth}px`; + this.view.style.height = `${screenHeight}px`; } } @@ -249,11 +267,10 @@ this.type = RENDERER_TYPE.UNKNOWN; - this.width = 0; - this.height = 0; - this.view = null; + this.screen = null; + this.resolution = 0; this.transparent = false; diff --git a/src/core/renderers/canvas/CanvasRenderer.js b/src/core/renderers/canvas/CanvasRenderer.js index 1bd2d5e..e84e49c 100644 --- a/src/core/renderers/canvas/CanvasRenderer.js +++ b/src/core/renderers/canvas/CanvasRenderer.js @@ -18,8 +18,8 @@ export default class CanvasRenderer extends SystemRenderer { /** - * @param {number} [width=800] - the width of the canvas view - * @param {number} [height=600] - the height of the canvas view + * @param {number} [screenWidth=800] - the width of the screen + * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false @@ -34,9 +34,9 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(width, height, options = {}) + constructor(screenWidth, screenHeight, options = {}) { - super('Canvas', width, height, options); + super('Canvas', screenWidth, screenHeight, options); this.type = RENDERER_TYPE.CANVAS; @@ -96,7 +96,7 @@ this.context = null; this.renderingToScreen = false; - this.resize(width, height); + this.resize(screenWidth, screenHeight); } /** @@ -283,12 +283,12 @@ * * @extends PIXI.SystemRenderer#resize * - * @param {number} width - The new width of the canvas view - * @param {number} height - The new height of the canvas view + * @param {number} screenWidth - the new width of the screen + * @param {number} screenHeight - the new height of the screen */ - resize(width, height) + resize(screenWidth, screenHeight) { - super.resize(width, height); + super.resize(screenWidth, screenHeight); // reset the scale mode.. oddly this seems to be reset when the canvas is resized. // surely a browser bug?? Let pixi fix that for you.. diff --git a/src/core/renderers/webgl/TextureManager.js b/src/core/renderers/webgl/TextureManager.js index b685c1a..8227289 100644 --- a/src/core/renderers/webgl/TextureManager.js +++ b/src/core/renderers/webgl/TextureManager.js @@ -61,7 +61,7 @@ * Updates and/or Creates a WebGL texture for the renderer's context. * * @param {PIXI.BaseTexture|PIXI.Texture} texture - the texture to update - * @param {Number} location - the location the texture will be bound to. + * @param {number} location - the location the texture will be bound to. * @return {GLTexture} The gl texture. */ updateTexture(texture, location) diff --git a/src/core/renderers/webgl/WebGLRenderer.js b/src/core/renderers/webgl/WebGLRenderer.js index e4cd8a0..830b382 100644 --- a/src/core/renderers/webgl/WebGLRenderer.js +++ b/src/core/renderers/webgl/WebGLRenderer.js @@ -31,8 +31,8 @@ { /** * - * @param {number} [width=0] - the width of the canvas view - * @param {number} [height=0] - the height of the canvas view + * @param {number} [screenWidth=800] - the width of the screen + * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false @@ -53,9 +53,9 @@ * @param {boolean} [options.legacy=false] - If true Pixi will aim to ensure compatibility * with older / less advanced devices. If you experiance unexplained flickering try setting this to true. */ - constructor(width, height, options = {}) + constructor(screenWidth, screenHeight, options = {}) { - super('WebGL', width, height, options); + super('WebGL', screenWidth, screenHeight, options); this.legacy = !!options.legacy; @@ -245,7 +245,7 @@ glCore._testingContext = gl; // setup the width/height properties and gl viewport - this.resize(this.width, this.height); + this.resize(this.screen.width, this.screen.height); } /** @@ -343,16 +343,16 @@ /** * Resizes the webGL view to the specified width and height. * - * @param {number} width - the new width of the webGL view - * @param {number} height - the new height of the webGL view + * @param {number} screenWidth - the new width of the screen + * @param {number} screenHeight - the new height of the screen */ - resize(width, height) + resize(screenWidth, screenHeight) { // if(width * this.resolution === this.width && height * this.resolution === this.height)return; - SystemRenderer.prototype.resize.call(this, width, height); + SystemRenderer.prototype.resize.call(this, screenWidth, screenHeight); - this.rootRenderTarget.resize(width, height); + this.rootRenderTarget.resize(screenWidth, screenHeight); if (this._activeRenderTarget === this.rootRenderTarget) { @@ -396,6 +396,26 @@ } /** + * Erases the render texture and fills the drawing area with a colour + * + * @param {PIXI.RenderTexture} renderTexture - The render texture to clear + * @param {number} [clearColor] - The colour + * @return {PIXI.WebGLRenderer} Returns itself. + */ + clearRenderTexture(renderTexture, clearColor) + { + const baseTexture = renderTexture.baseTexture; + const renderTarget = baseTexture._glRenderTargets[this.CONTEXT_UID]; + + if (renderTarget) + { + renderTarget.clear(clearColor); + } + + return this; + } + + /** * Binds a render texture for rendering * * @param {PIXI.RenderTexture} renderTexture - The render texture to render @@ -474,9 +494,10 @@ * Changes the current GLShader to the one given in parameter * * @param {PIXI.glCore.Shader} shader - the new glShader + * @param {boolean} [autoProject=true] - Whether automatically set the projection matrix * @return {PIXI.WebGLRenderer} Returns itself. */ - _bindGLShader(shader) + _bindGLShader(shader, autoProject) { // TODO cache if (this._activeShader !== shader) @@ -484,8 +505,14 @@ this._activeShader = shader; shader.bind(); - // automatically set the projection matrix - shader.uniforms.projectionMatrix = this._activeRenderTarget.projectionMatrix.toArray(true); + // `autoProject` normally would be a default parameter set to true + // but because of how Babel transpiles default parameters + // it hinders the performance of this method. + if (autoProject !== false) + { + // automatically set the projection matrix + shader.uniforms.projectionMatrix = this._activeRenderTarget.projectionMatrix.toArray(true); + } } return this; diff --git a/src/core/renderers/webgl/WebGLState.js b/src/core/renderers/webgl/WebGLState.js index ba6ecc5..6c846d5 100755 --- a/src/core/renderers/webgl/WebGLState.js +++ b/src/core/renderers/webgl/WebGLState.js @@ -82,18 +82,20 @@ push() { // next state.. - let state = this.stack[++this.stackIndex]; + let state = this.stack[this.stackIndex]; if (!state) { state = this.stack[this.stackIndex] = new Uint8Array(16); } + ++this.stackIndex; + // copy state.. // set active state so we can force overrides of gl state for (let i = 0; i < this.activeState.length; i++) { - this.activeState[i] = state[i]; + state[i] = this.activeState[i]; } } diff --git a/src/core/renderers/webgl/filters/Filter.js b/src/core/renderers/webgl/filters/Filter.js index 2c3eaad..4eb6128 100644 --- a/src/core/renderers/webgl/filters/Filter.js +++ b/src/core/renderers/webgl/filters/Filter.js @@ -9,7 +9,7 @@ * @memberof PIXI * @extends PIXI.Shader */ -class Filter extends Shader +export default class Filter extends Shader { /** * @param {string} [vertexSrc] - The source of the vertex shader. @@ -56,15 +56,16 @@ * @param {PIXI.RenderTarget} input - The input render target. * @param {PIXI.RenderTarget} output - The target to output to. * @param {boolean} clear - Should the output be cleared before rendering to it + * @param {object} [currentState] - It's current state of filter. + * There are some useful properties in the currentState : + * target, filters, sourceFrame, destinationFrame, renderTarget, resolution */ - apply(filterManager, input, output, clear) + apply(filterManager, input, output, clear, currentState) // eslint-disable-line no-unused-vars { // do as you please! - filterManager.applyFilter(this, input, output, clear); + filterManager.applyFilter(this, input, output, clear, currentState); // or just do a regular render.. } } - -export default Filter; diff --git a/src/core/renderers/webgl/filters/extractUniformsFromSrc.js b/src/core/renderers/webgl/filters/extractUniformsFromSrc.js index d78a0b1..4893214 100644 --- a/src/core/renderers/webgl/filters/extractUniformsFromSrc.js +++ b/src/core/renderers/webgl/filters/extractUniformsFromSrc.js @@ -12,7 +12,7 @@ function extractUniformsFromString(string) { - const maskRegex = new RegExp('^(projectionMatrix|uSampler|filterArea)$'); + const maskRegex = new RegExp('^(projectionMatrix|uSampler|filterArea|filterClamp)$'); const uniforms = {}; let nameSplit; diff --git a/src/core/renderers/webgl/managers/FilterManager.js b/src/core/renderers/webgl/managers/FilterManager.js index 43fc869..6c7422d 100644 --- a/src/core/renderers/webgl/managers/FilterManager.js +++ b/src/core/renderers/webgl/managers/FilterManager.js @@ -2,6 +2,7 @@ import RenderTarget from '../utils/RenderTarget'; import Quad from '../utils/Quad'; import { Rectangle } from '../../../math'; +import Shader from '../../../Shader'; import * as filterTransforms from '../filters/filterTransforms'; import bitTwiddle from 'bit-twiddle'; @@ -22,8 +23,6 @@ this.filters = []; this.target = null; this.resolution = 1; - - this.firstRun = true; } } @@ -155,7 +154,7 @@ if (filters.length === 1) { - filters[0].apply(this, currentState.renderTarget, lastState.renderTarget, false); + filters[0].apply(this, currentState.renderTarget, lastState.renderTarget, false, currentState); this.freePotRenderTarget(currentState.renderTarget); } else @@ -177,7 +176,7 @@ for (i = 0; i < filters.length - 1; ++i) { - filters[i].apply(this, flip, flop, true); + filters[i].apply(this, flip, flop, true, currentState); const t = flip; @@ -185,7 +184,7 @@ flop = t; } - filters[i].apply(this, flip, lastState.renderTarget, false); + filters[i].apply(this, flip, lastState.renderTarget, false, currentState); this.freePotRenderTarget(flip); this.freePotRenderTarget(flop); @@ -212,16 +211,28 @@ const renderer = this.renderer; const gl = renderer.gl; - renderer.bindShader(filter, true); + let shader = filter.glShaders[renderer.CONTEXT_UID]; - // TODO theres a better way! - // quad can be a mesh and we then should be able to pull this off without accessing the shader directly - const shader = renderer.shaderManager.getGLShader(); - - // TODO change quad to mesh.. - if (!this.firstRun) + // cacheing.. + if (!shader) { - this.firstRun = true; + if (filter.glShaderKey) + { + shader = this.shaderCache[filter.glShaderKey]; + + if (!shader) + { + shader = new Shader(this.gl, filter.vertexSrc, filter.fragmentSrc); + + filter.glShaders[renderer.CONTEXT_UID] = this.shaderCache[filter.glShaderKey] = shader; + } + } + else + { + shader = filter.glShaders[renderer.CONTEXT_UID] = new Shader(this.gl, filter.vertexSrc, filter.fragmentSrc); + } + + // TODO - this only needs to be done once? renderer.bindVao(null); this.quad.initVao(shader); @@ -244,58 +255,146 @@ renderer.maskManager.pushScissorMask(null, renderer.maskManager.scissorData); } - // renderer._bindGLShader(shader); + renderer.bindShader(shader); - let currentState; - const uniforms = filter.uniforms; + // free unit 0 for us, doesn't matter what was there + // don't try to restore it, because syncUniforms can upload it to another slot + // and it'll be a problem + const tex = this.renderer.emptyTextures[0]; - if (uniforms.filterArea) - { - currentState = this.filterData.stack[this.filterData.index]; - - const filterArea = uniforms.filterArea; - - filterArea[0] = currentState.renderTarget.size.width; - filterArea[1] = currentState.renderTarget.size.height; - filterArea[2] = currentState.sourceFrame.x; - filterArea[3] = currentState.sourceFrame.y; - - filter.uniforms.filterArea = filterArea; - } - - // use this to clamp displaced texture coords so they belong to filterArea - // see displacementFilter fragment shader for an example - if (uniforms.filterClamp) - { - currentState = this.filterData.stack[this.filterData.index]; - - const filterClamp = uniforms.filterClamp; - - filterClamp[0] = 0; - filterClamp[1] = 0; - filterClamp[2] = (currentState.sourceFrame.width - 1) / currentState.renderTarget.size.width; - filterClamp[3] = (currentState.sourceFrame.height - 1) / currentState.renderTarget.size.height; - - filter.uniforms.filterClamp = filterClamp; - } - - renderer.shaderManager.setUniforms(filter.uniforms); + this.renderer.boundTextures[0] = tex; + // this syncs the pixi filters uniforms with glsl uniforms + this.syncUniforms(shader, filter); renderer.state.setBlendMode(filter.blendMode); - // temporary bypass cache.. - const tex = this.renderer.boundTextures[0]; - gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, input.texture.texture); this.quad.vao.draw(this.renderer.gl.TRIANGLES, 6, 0); - // restore cache. gl.bindTexture(gl.TEXTURE_2D, tex._glTextures[this.renderer.CONTEXT_UID].texture); } /** + * Uploads the uniforms of the filter. + * + * @param {GLShader} shader - The underlying gl shader. + * @param {PIXI.Filter} filter - The filter we are synchronizing. + */ + syncUniforms(shader, filter) + { + const uniformData = filter.uniformData; + const uniforms = filter.uniforms; + + // 0 is reserved for the pixi texture so we start at 1! + let textureCount = 1; + let currentState; + + // filterArea and filterClamp that are handled by FilterManager directly + // they must not appear in uniformData + + if (shader.uniforms.filterArea) + { + currentState = this.filterData.stack[this.filterData.index]; + + const filterArea = shader.uniforms.filterArea; + + filterArea[0] = currentState.renderTarget.size.width; + filterArea[1] = currentState.renderTarget.size.height; + filterArea[2] = currentState.sourceFrame.x; + filterArea[3] = currentState.sourceFrame.y; + + shader.uniforms.filterArea = filterArea; + } + + // use this to clamp displaced texture coords so they belong to filterArea + // see displacementFilter fragment shader for an example + if (shader.uniforms.filterClamp) + { + currentState = currentState || this.filterData.stack[this.filterData.index]; + + const filterClamp = shader.uniforms.filterClamp; + + filterClamp[0] = 0; + filterClamp[1] = 0; + filterClamp[2] = (currentState.sourceFrame.width - 1) / currentState.renderTarget.size.width; + filterClamp[3] = (currentState.sourceFrame.height - 1) / currentState.renderTarget.size.height; + + shader.uniforms.filterClamp = filterClamp; + } + + // TODO Cacheing layer.. + for (const i in uniformData) + { + if (uniformData[i].type === 'sampler2D' && uniforms[i] !== 0) + { + if (uniforms[i].baseTexture) + { + shader.uniforms[i] = this.renderer.bindTexture(uniforms[i].baseTexture, textureCount); + } + else + { + shader.uniforms[i] = textureCount; + + // TODO + // this is helpful as renderTargets can also be set. + // Although thinking about it, we could probably + // make the filter texture cache return a RenderTexture + // rather than a renderTarget + const gl = this.renderer.gl; + + this.renderer.boundTextures[textureCount] = this.renderer.emptyTextures[textureCount]; + gl.activeTexture(gl.TEXTURE0 + textureCount); + + uniforms[i].texture.bind(); + } + + textureCount++; + } + else if (uniformData[i].type === 'mat3') + { + // check if its pixi matrix.. + if (uniforms[i].a !== undefined) + { + shader.uniforms[i] = uniforms[i].toArray(true); + } + else + { + shader.uniforms[i] = uniforms[i]; + } + } + else if (uniformData[i].type === 'vec2') + { + // check if its a point.. + if (uniforms[i].x !== undefined) + { + const val = shader.uniforms[i] || new Float32Array(2); + + val[0] = uniforms[i].x; + val[1] = uniforms[i].y; + shader.uniforms[i] = val; + } + else + { + shader.uniforms[i] = uniforms[i]; + } + } + else if (uniformData[i].type === 'float') + { + if (shader.uniforms.data[i].value !== uniformData[i]) + { + shader.uniforms[i] = uniforms[i]; + } + } + else + { + shader.uniforms[i] = uniforms[i]; + } + } + } + + /** * Gets a render target from the pool, or creates a new one. * * @param {boolean} clear - Should we clear the render texture when we get it? @@ -442,7 +541,6 @@ renderTarget.resolution = resolution; renderTarget.defaultFrame.width = renderTarget.size.width = minWidth / resolution; renderTarget.defaultFrame.height = renderTarget.size.height = minHeight / resolution; - renderTarget.attachStencilBuffer(); return renderTarget; } diff --git a/src/core/settings.js b/src/core/settings.js index bb08cb0..f4dd087 100644 --- a/src/core/settings.js +++ b/src/core/settings.js @@ -77,11 +77,11 @@ * * @static * @memberof PIXI.settings - * @type {RegExp|string} + * @type {RegExp} * @example `@2x` - * @default /@(.+)x/ + * @default /@([0-9\.]+)x/ */ - RETINA_PREFIX: /@(.+)x/, + RETINA_PREFIX: /@([0-9\.]+)x/, /** * The default render options if none are supplied to {@link PIXI.WebGLRenderer} @@ -175,14 +175,24 @@ SCALE_MODE: 0, /** - * Default specify float precision in shaders. + * Default specify float precision in vertex shader. + * + * @static + * @memberof PIXI.settings + * @type {PIXI.PRECISION} + * @default PIXI.PRECISION.HIGH + */ + PRECISION_VERTEX: 'highp', + + /** + * Default specify float precision in fragment shader. * * @static * @memberof PIXI.settings * @type {PIXI.PRECISION} * @default PIXI.PRECISION.MEDIUM */ - PRECISION: 'mediump', + PRECISION_FRAGMENT: 'mediump', /** * Can we upload the same buffer in a single frame? diff --git a/src/core/sprites/Sprite.js b/src/core/sprites/Sprite.js index 28b0b18..121bf58 100644 --- a/src/core/sprites/Sprite.js +++ b/src/core/sprites/Sprite.js @@ -212,11 +212,11 @@ } else { - w0 = orig.width * (1 - anchor._x); - w1 = orig.width * -anchor._x; + w1 = -anchor._x * orig.width; + w0 = w1 + orig.width; - h0 = orig.height * (1 - anchor._y); - h1 = orig.height * -anchor._y; + h1 = -anchor._y * orig.height; + h0 = h1 + orig.height; } // xy @@ -269,11 +269,11 @@ const tx = wt.tx; const ty = wt.ty; - const w0 = (orig.width) * (1 - anchor._x); - const w1 = (orig.width) * -anchor._x; + const w1 = -anchor._x * orig.width; + const w0 = w1 + orig.width; - const h0 = orig.height * (1 - anchor._y); - const h1 = orig.height * -anchor._y; + const h1 = -anchor._y * orig.height; + const h0 = h1 + orig.height; // xy vertexData[0] = (a * w1) + (c * h1) + tx; @@ -440,7 +440,7 @@ * * @static * @param {number|string|PIXI.BaseTexture|HTMLCanvasElement|HTMLVideoElement} source Source to create texture from - * @return {PIXI.Sprite} The newly created texture + * @return {PIXI.Sprite} The newly created sprite */ static from(source) { @@ -538,8 +538,8 @@ } /** - * The tint applied to the sprite. This is a hex value. A value of - * 0xFFFFFF will remove any tint effect. + * The tint applied to the sprite. This is a hex value. + * A value of 0xFFFFFF will remove any tint effect. * * @member {number} * @default 0xFFFFFF diff --git a/src/core/sprites/canvas/CanvasTinter.js b/src/core/sprites/canvas/CanvasTinter.js index 1cf316b..27e5e0c 100644 --- a/src/core/sprites/canvas/CanvasTinter.js +++ b/src/core/sprites/canvas/CanvasTinter.js @@ -75,8 +75,8 @@ crop.width *= resolution; crop.height *= resolution; - canvas.width = crop.width; - canvas.height = crop.height; + canvas.width = Math.ceil(crop.width); + canvas.height = Math.ceil(crop.height); context.fillStyle = `#${(`00000${(color | 0).toString(16)}`).substr(-6)}`; @@ -130,8 +130,8 @@ crop.width *= resolution; crop.height *= resolution; - canvas.width = crop.width; - canvas.height = crop.height; + canvas.width = Math.ceil(crop.width); + canvas.height = Math.ceil(crop.height); context.globalCompositeOperation = 'copy'; context.fillStyle = `#${(`00000${(color | 0).toString(16)}`).substr(-6)}`; @@ -172,8 +172,8 @@ crop.width *= resolution; crop.height *= resolution; - canvas.width = crop.width; - canvas.height = crop.height; + canvas.width = Math.ceil(crop.width); + canvas.height = Math.ceil(crop.height); context.globalCompositeOperation = 'copy'; context.drawImage( diff --git a/src/core/sprites/webgl/SpriteRenderer.js b/src/core/sprites/webgl/SpriteRenderer.js index 2affcd5..7c352e2 100644 --- a/src/core/sprites/webgl/SpriteRenderer.js +++ b/src/core/sprites/webgl/SpriteRenderer.js @@ -30,7 +30,7 @@ /** * Number of values sent in the vertex buffer. - * positionX, positionY, colorR, colorG, colorB = 5 + * aVertexPosition(2), aTextureCoord(1), aColor(1), aTextureId(1) = 5 * * @member {number} */ @@ -75,7 +75,6 @@ this.shader = null; this.currentIndex = 0; - TICK = 0; this.groups = []; for (let k = 0; k < this.size; k++) diff --git a/src/core/textures/BaseTexture.js b/src/core/textures/BaseTexture.js index 853de7e..aeccf39 100644 --- a/src/core/textures/BaseTexture.js +++ b/src/core/textures/BaseTexture.js @@ -232,16 +232,24 @@ this.realWidth = this.source.naturalWidth || this.source.videoWidth || this.source.width; this.realHeight = this.source.naturalHeight || this.source.videoHeight || this.source.height; - this.width = this.realWidth / this.resolution; - this.height = this.realHeight / this.resolution; - - this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); + this._updateDimensions(); } this.emit('update', this); } /** + * Update dimensions from real values + */ + _updateDimensions() + { + this.width = this.realWidth / this.resolution; + this.height = this.realHeight / this.resolution; + + this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); + } + + /** * Load a source. * * If the source is not-immediately-available, such as an image that needs to be @@ -525,11 +533,7 @@ this.realWidth = Math.round(svgWidth * this.sourceScale); this.realHeight = Math.round(svgHeight * this.sourceScale); - this.width = this.realWidth / this.resolution; - this.height = this.realHeight / this.resolution; - - // Check pow2 after scale - this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); + this._updateDimensions(); // Create a canvas element const canvas = document.createElement('canvas'); @@ -690,4 +694,52 @@ return baseTexture; } + + /** + * Helper function that creates a base texture based on the source you provide. + * The source can be - image url, image element, canvas element. + * + * @static + * @param {string|HTMLImageElement|HTMLCanvasElement} source - The source to create base texture from. + * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values + * @param {number} [sourceScale=(auto)] - Scale for the original image, used with Svg images. + * @return {PIXI.BaseTexture} The new base texture. + */ + static from(source, scaleMode, sourceScale) + { + if (typeof source === 'string') + { + return BaseTexture.fromImage(source, undefined, scaleMode, sourceScale); + } + else if (source instanceof HTMLImageElement) + { + const imageUrl = source.src; + let baseTexture = BaseTextureCache[imageUrl]; + + if (!baseTexture) + { + baseTexture = new BaseTexture(source, scaleMode); + baseTexture.imageUrl = imageUrl; + + if (sourceScale) + { + baseTexture.sourceScale = sourceScale; + } + + // if there is an @2x at the end of the url we are going to assume its a highres image + baseTexture.resolution = getResolutionOfUrl(imageUrl); + + BaseTextureCache[imageUrl] = baseTexture; + } + + return baseTexture; + } + else if (source instanceof HTMLCanvasElement) + { + return BaseTexture.fromCanvas(source, scaleMode); + } + + // lets assume its a base texture! + return source; + } } diff --git a/src/core/textures/Spritesheet.js b/src/core/textures/Spritesheet.js new file mode 100644 index 0000000..d469c62 --- /dev/null +++ b/src/core/textures/Spritesheet.js @@ -0,0 +1,274 @@ +import { Rectangle, Texture } from '../'; +import { getResolutionOfUrl, TextureCache } from '../utils'; + +/** + * Utility class for maintaining reference to a collection + * of Textures on a single Spritesheet. + * + * @class + * @memberof PIXI + */ +export default class Spritesheet +{ + /** + * The maximum number of Textures to build per process. + * + * @type {number} + * @default 1000 + */ + static get BATCH_SIZE() + { + return 1000; + } + + /** + * @param {PIXI.BaseTexture} baseTexture Reference to the source BaseTexture object. + * @param {Object} data - Spritesheet image data. + * @param {string} [resolutionFilename] - The filename to consider when determining + * the resolution of the spritesheet. If not provided, the imageUrl will + * be used on the BaseTexture. + */ + constructor(baseTexture, data, resolutionFilename = null) + { + /** + * Reference to ths source texture + * @type {PIXI.BaseTexture} + */ + this.baseTexture = baseTexture; + + /** + * Map of spritesheet textures. + * @type {Object} + */ + this.textures = {}; + + /** + * Reference to the original JSON data. + * @type {Object} + */ + this.data = data; + + /** + * The resolution of the spritesheet. + * @type {number} + */ + this.resolution = this._updateResolution( + resolutionFilename || this.baseTexture.imageUrl + ); + + /** + * Map of spritesheet frames. + * @type {Object} + * @private + */ + this._frames = this.data.frames; + + /** + * Collection of frame names. + * @type {string[]} + * @private + */ + this._frameKeys = Object.keys(this._frames); + + /** + * Current batch index being processed. + * @type {number} + * @private + */ + this._batchIndex = 0; + + /** + * Callback when parse is completed. + * @type {Function} + * @private + */ + this._callback = null; + } + + /** + * Generate the resolution from the filename or fallback + * to the meta.scale field of the JSON data. + * + * @private + * @param {string} resolutionFilename - The filename to use for resolving + * the default resolution. + * @return {number} Resolution to use for spritesheet. + */ + _updateResolution(resolutionFilename) + { + const scale = this.data.meta.scale; + + // Use a defaultValue of `null` to check if a url-based resolution is set + let resolution = getResolutionOfUrl(resolutionFilename, null); + + // No resolution found via URL + if (resolution === null) + { + // Use the scale value or default to 1 + resolution = scale !== undefined ? parseFloat(scale) : 1; + } + + // For non-1 resolutions, update baseTexture + if (resolution !== 1) + { + this.baseTexture.resolution = resolution; + this.baseTexture.update(); + } + + return resolution; + } + + /** + * Parser spritesheet from loaded data. This is done asynchronously + * to prevent creating too many Texture within a single process. + * + * @param {Function} callback - Callback when complete returns + * a map of the Textures for this spritesheet. + */ + parse(callback) + { + this._batchIndex = 0; + this._callback = callback; + + if (this._frameKeys.length <= Spritesheet.BATCH_SIZE) + { + this._processFrames(0); + this._parseComplete(); + } + else + { + this._nextBatch(); + } + } + + /** + * Process a batch of frames + * + * @private + * @param {number} initialFrameIndex - The index of frame to start. + */ + _processFrames(initialFrameIndex) + { + let frameIndex = initialFrameIndex; + const maxFrames = Spritesheet.BATCH_SIZE; + + while (frameIndex - initialFrameIndex < maxFrames && frameIndex < this._frameKeys.length) + { + const i = this._frameKeys[frameIndex]; + const rect = this._frames[i].frame; + + if (rect) + { + let frame = null; + let trim = null; + const orig = new Rectangle( + 0, + 0, + this._frames[i].sourceSize.w / this.resolution, + this._frames[i].sourceSize.h / this.resolution + ); + + if (this._frames[i].rotated) + { + frame = new Rectangle( + rect.x / this.resolution, + rect.y / this.resolution, + rect.h / this.resolution, + rect.w / this.resolution + ); + } + else + { + frame = new Rectangle( + rect.x / this.resolution, + rect.y / this.resolution, + rect.w / this.resolution, + rect.h / this.resolution + ); + } + + // Check to see if the sprite is trimmed + if (this._frames[i].trimmed) + { + trim = new Rectangle( + this._frames[i].spriteSourceSize.x / this.resolution, + this._frames[i].spriteSourceSize.y / this.resolution, + rect.w / this.resolution, + rect.h / this.resolution + ); + } + + this.textures[i] = new Texture( + this.baseTexture, + frame, + orig, + trim, + this._frames[i].rotated ? 2 : 0 + ); + + // lets also add the frame to pixi's global cache for fromFrame and fromImage functions + TextureCache[i] = this.textures[i]; + } + + frameIndex++; + } + } + + /** + * The parse has completed. + * + * @private + */ + _parseComplete() + { + const callback = this._callback; + + this._callback = null; + this._batchIndex = 0; + callback.call(this, this.textures); + } + + /** + * Begin the next batch of textures. + * + * @private + */ + _nextBatch() + { + this._processFrames(this._batchIndex * Spritesheet.BATCH_SIZE); + this._batchIndex++; + setTimeout(() => + { + if (this._batchIndex * Spritesheet.BATCH_SIZE < this._frameKeys.length) + { + this._nextBatch(); + } + else + { + this._parseComplete(); + } + }, 0); + } + + /** + * Destroy Spritesheet and don't use after this. + * + * @param {boolean} [destroyBase=false] Whether to destroy the base texture as well + */ + destroy(destroyBase = false) + { + for (const i in this.textures) + { + this.textures[i].destroy(); + } + this._frames = null; + this._frameKeys = null; + this.data = null; + this.textures = null; + if (destroyBase) + { + this.baseTexture.destroy(); + } + this.baseTexture = null; + } +} diff --git a/src/core/textures/Texture.js b/src/core/textures/Texture.js index 6403d4f..afe4254 100644 --- a/src/core/textures/Texture.js +++ b/src/core/textures/Texture.js @@ -3,7 +3,7 @@ import TextureUvs from './TextureUvs'; import EventEmitter from 'eventemitter3'; import { Rectangle } from '../math'; -import { TextureCache, BaseTextureCache } from '../utils'; +import { TextureCache, BaseTextureCache, getResolutionOfUrl } from '../utils'; /** * A texture stores the information that represents an image or part of an image. It cannot be added @@ -365,7 +365,8 @@ * The source can be - frame id, image url, video url, canvas element, video element, base texture * * @static - * @param {number|string|PIXI.BaseTexture|HTMLCanvasElement|HTMLVideoElement} source - Source to create texture from + * @param {number|string|HTMLImageElement|HTMLCanvasElement|HTMLVideoElement|PIXI.BaseTexture} + * source - Source to create texture from * @return {PIXI.Texture} The newly created texture */ static from(source) @@ -393,7 +394,7 @@ } else if (source instanceof HTMLImageElement) { - return new Texture(new BaseTexture(source)); + return new Texture(BaseTexture.from(source)); } else if (source instanceof HTMLCanvasElement) { @@ -413,6 +414,43 @@ } /** + * Create a texture from a source and add to the cache. + * + * @static + * @param {HTMLImageElement|HTMLCanvasElement} source - The input source. + * @param {String} imageUrl - File name of texture, for cache and resolving resolution. + * @param {String} [name] - Human readible name for the texture cache. If no name is + * specified, only `imageUrl` will be used as the cache ID. + * @return {PIXI.Texture} Output texture + */ + static fromLoader(source, imageUrl, name) + { + const baseTexture = new BaseTexture(source, null, getResolutionOfUrl(imageUrl)); + const texture = new Texture(baseTexture); + + baseTexture.imageUrl = imageUrl; + + // No name, use imageUrl instead + if (!name) + { + name = imageUrl; + } + + // lets also add the frame to pixi's global cache for fromFrame and fromImage fucntions + BaseTextureCache[name] = baseTexture; + TextureCache[name] = texture; + + // also add references by url if they are different. + if (name !== imageUrl) + { + BaseTextureCache[imageUrl] = baseTexture; + TextureCache[imageUrl] = texture; + } + + return texture; + } + + /** * Adds a texture to the global TextureCache. This cache is shared across the whole PIXI object. * * @static @@ -522,6 +560,29 @@ } } +function createWhiteTexture() +{ + const canvas = document.createElement('canvas'); + + canvas.width = 10; + canvas.height = 10; + + const context = canvas.getContext('2d'); + + context.fillStyle = 'white'; + context.fillRect(0, 0, 10, 10); + + return new Texture(new BaseTexture(canvas)); +} + +function removeAllHandlers(tex) +{ + tex.destroy = function _emptyDestroy() { /* empty */ }; + tex.on = function _emptyOn() { /* empty */ }; + tex.once = function _emptyOnce() { /* empty */ }; + tex.emit = function _emptyEmit() { /* empty */ }; +} + /** * An empty texture, used often to not have to create multiple empty textures. * Can not be destroyed. @@ -530,7 +591,14 @@ * @constant */ Texture.EMPTY = new Texture(new BaseTexture()); -Texture.EMPTY.destroy = function _emptyDestroy() { /* empty */ }; -Texture.EMPTY.on = function _emptyOn() { /* empty */ }; -Texture.EMPTY.once = function _emptyOnce() { /* empty */ }; -Texture.EMPTY.emit = function _emptyEmit() { /* empty */ }; +removeAllHandlers(Texture.EMPTY); + +/** + * A white texture of 10x10 size, used for graphics and other things + * Can not be destroyed. + * + * @static + * @constant + */ +Texture.WHITE = createWhiteTexture(); +removeAllHandlers(Texture.WHITE); diff --git a/src/core/ticker/Ticker.js b/src/core/ticker/Ticker.js index d98a1f0..17a1517 100644 --- a/src/core/ticker/Ticker.js +++ b/src/core/ticker/Ticker.js @@ -67,11 +67,12 @@ * is based, this value is neither capped nor scaled. * If the platform supports DOMHighResTimeStamp, * this value will have a precision of 1 µs. + * Defaults to target frame time * * @member {number} - * @default 1 / TARGET_FPMS + * @default 16.66 */ - this.elapsedMS = 1 / settings.TARGET_FPMS; // default to target frame time + this.elapsedMS = 1 / settings.TARGET_FPMS; /** * The last time {@link PIXI.ticker.Ticker#update} was invoked. diff --git a/src/core/utils/index.js b/src/core/utils/index.js index 6ff609f..be44750 100644 --- a/src/core/utils/index.js +++ b/src/core/utils/index.js @@ -2,6 +2,7 @@ import settings from '../settings'; import EventEmitter from 'eventemitter3'; import pluginTarget from './pluginTarget'; +import * as mixins from './mixin'; import * as isMobile from 'ismobilejs'; let nextUid = 0; @@ -33,6 +34,7 @@ * @type {mixin} */ pluginTarget, + mixins, }; /** @@ -93,7 +95,7 @@ */ export function rgb2hex(rgb) { - return (((rgb[0] * 255) << 16) + ((rgb[1] * 255) << 8) + (rgb[2] * 255)); + return (((rgb[0] * 255) << 16) + ((rgb[1] * 255) << 8) + (rgb[2] * 255 | 0)); } /** @@ -368,3 +370,43 @@ * @private */ export const ProgramCache = {}; + +/** + * Destroys all texture in the cache + * + * @memberof PIXI.utils + * @function destroyTextureCache + */ +export function destroyTextureCache() +{ + let key; + + for (key in TextureCache) + { + TextureCache[key].destroy(); + } + for (key in BaseTextureCache) + { + BaseTextureCache[key].destroy(); + } +} + +/** + * Removes all textures from cache, but does not destroy them + * + * @memberof PIXI.utils + * @function clearTextureCache + */ +export function clearTextureCache() +{ + let key; + + for (key in TextureCache) + { + delete TextureCache[key]; + } + for (key in BaseTextureCache) + { + delete BaseTextureCache[key]; + } +} diff --git a/src/core/utils/mixin.js b/src/core/utils/mixin.js new file mode 100644 index 0000000..114de2a --- /dev/null +++ b/src/core/utils/mixin.js @@ -0,0 +1,59 @@ +/** + * Mixes all enumerable properties and methods from a source object to a target object. + * + * @memberof PIXI.utils.mixins + * @function mixin + * @param {object} target The prototype or instance that properties and methods should be added to. + * @param {object} source The source of properties and methods to mix in. + */ +export function mixin(target, source) +{ + if (!target || !source) return; + // in ES8/ES2017, this would be really easy: + // Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); + + // get all the enumerable property keys + const keys = Object.keys(source); + + // loop through properties + for (let i = 0; i < keys.length; ++i) + { + const propertyName = keys[i]; + + // Set the property using the property descriptor - this works for accessors and normal value properties + Object.defineProperty(target, propertyName, Object.getOwnPropertyDescriptor(source, propertyName)); + } +} + +const mixins = []; + +/** + * Queues a mixin to be handled towards the end of the initialization of PIXI, so that deprecation + * can take effect. + * + * @memberof PIXI.utils.mixins + * @function delayMixin + * @private + * @param {object} target The prototype or instance that properties and methods should be added to. + * @param {object} source The source of properties and methods to mix in. + */ +export function delayMixin(target, source) +{ + mixins.push(target, source); +} + +/** + * Handles all mixins queued via delayMixin(). + * + * @memberof PIXI.utils.mixins + * @function performMixins + * @private + */ +export function performMixins() +{ + for (let i = 0; i < mixins.length; i += 2) + { + mixin(mixins[i], mixins[i + 1]); + } + mixins.length = 0; +} diff --git a/src/deprecation.js b/src/deprecation.js index 045a19f..b9c73b3 100644 --- a/src/deprecation.js +++ b/src/deprecation.js @@ -5,6 +5,7 @@ import * as filters from './filters'; import * as prepare from './prepare'; import * as loaders from './loaders'; +import * as interaction from './interaction'; // provide method to give a stack track for warnings // useful for tracking-down where deprecated methods/properties/classes @@ -534,7 +535,7 @@ { parent: 'GC_MODES', target: 'GC_MODE' }, { parent: 'WRAP_MODES', target: 'WRAP_MODE' }, { parent: 'SCALE_MODES', target: 'SCALE_MODE' }, - { parent: 'PRECISION', target: 'PRECISION' }, + { parent: 'PRECISION', target: 'PRECISION_FRAGMENT' }, ]; for (let i = 0; i < defaults.length; i++) @@ -558,6 +559,32 @@ }); } +Object.defineProperties(core.settings, { + + /** + * @static + * @name PRECISION + * @memberof PIXI.settings + * @see PIXI.PRECISION + * @deprecated since version 4.4.0 + */ + PRECISION: { + enumerable: true, + get() + { + warn('PIXI.settings.PRECISION has been deprecated, please use PIXI.settings.PRECISION_FRAGMENT'); + + return core.settings.PRECISION_FRAGMENT; + }, + set(value) + { + warn('PIXI.settings.PRECISION has been deprecated, please use PIXI.settings.PRECISION_FRAGMENT'); + + core.settings.PRECISION_FRAGMENT = value; + }, + }, +}); + Object.defineProperties(extras, { /** @@ -694,7 +721,7 @@ warn('determineFontProperties is now deprecated, please use the static calculateFontProperties method, ' + 'e.g : Text.calculateFontProperties(fontStyle);'); - return Text.calculateFontProperties(fontStyle); + return core.Text.calculateFontProperties(fontStyle); }; Object.defineProperties(core.TextStyle.prototype, { @@ -1003,3 +1030,69 @@ }, }, }); + +/** + * @name PIXI.interaction.interactiveTarget#defaultCursor + * @static + * @type {number} + * @see PIXI.interaction.interactiveTarget#cursor + * @deprecated since 4.3.0 + */ +Object.defineProperty(interaction.interactiveTarget, 'defaultCursor', { + set(value) + { + warn('Property defaultCursor has been replaced with \'cursor\'. '); + this.cursor = value; + }, + get() + { + warn('Property defaultCursor has been replaced with \'cursor\'. '); + + return this.cursor; + }, + enumerable: true, +}); + +/** + * @name PIXI.interaction.InteractionManager#defaultCursorStyle + * @static + * @type {string} + * @see PIXI.interaction.InteractionManager#cursorStyles + * @deprecated since 4.3.0 + */ +Object.defineProperty(interaction.InteractionManager, 'defaultCursorStyle', { + set(value) + { + warn('Property defaultCursorStyle has been replaced with \'cursorStyles.default\'. '); + this.cursorStyles.default = value; + }, + get() + { + warn('Property defaultCursorStyle has been replaced with \'cursorStyles.default\'. '); + + return this.cursorStyles.default; + }, +}); + +/** + * @name PIXI.interaction.InteractionManager#currentCursorStyle + * @static + * @type {string} + * @see PIXI.interaction.InteractionManager#cursorStyles + * @deprecated since 4.3.0 + */ +Object.defineProperty(interaction.InteractionManager, 'currentCursorStyle', { + set(value) + { + warn('Property currentCursorStyle has been removed.' + + 'See the currentCursorMode property, which works differently.'); + this.currentCursorMode = value; + }, + get() + { + warn('Property currentCursorStyle has been removed.' + + 'See the currentCursorMode property, which works differently.'); + + return this.currentCursorMode; + }, +}); diff --git a/src/extras/BitmapText.js b/src/extras/BitmapText.js index fb6c343..0a0e150 100644 --- a/src/extras/BitmapText.js +++ b/src/extras/BitmapText.js @@ -455,6 +455,73 @@ return this._textHeight; } + + /** + * Register a bitmap font with data and a texture. + * + * @static + * @param {XMLDocument} xml - The XML document data. + * @param {PIXI.Texture} texture - Texture with all symbols. + * @return {Object} Result font object with font, size, lineHeight and char fields. + */ + static registerFont(xml, texture) + { + const data = {}; + const info = xml.getElementsByTagName('info')[0]; + const common = xml.getElementsByTagName('common')[0]; + + data.font = info.getAttribute('face'); + data.size = parseInt(info.getAttribute('size'), 10); + data.lineHeight = parseInt(common.getAttribute('lineHeight'), 10); + data.chars = {}; + + // parse letters + const letters = xml.getElementsByTagName('char'); + + for (let i = 0; i < letters.length; i++) + { + const letter = letters[i]; + const charCode = parseInt(letter.getAttribute('id'), 10); + + const textureRect = new core.Rectangle( + parseInt(letter.getAttribute('x'), 10) + texture.frame.x, + parseInt(letter.getAttribute('y'), 10) + texture.frame.y, + parseInt(letter.getAttribute('width'), 10), + parseInt(letter.getAttribute('height'), 10) + ); + + data.chars[charCode] = { + xOffset: parseInt(letter.getAttribute('xoffset'), 10), + yOffset: parseInt(letter.getAttribute('yoffset'), 10), + xAdvance: parseInt(letter.getAttribute('xadvance'), 10), + kerning: {}, + texture: new core.Texture(texture.baseTexture, textureRect), + + }; + } + + // parse kernings + const kernings = xml.getElementsByTagName('kerning'); + + for (let i = 0; i < kernings.length; i++) + { + const kerning = kernings[i]; + const first = parseInt(kerning.getAttribute('first'), 10); + const second = parseInt(kerning.getAttribute('second'), 10); + const amount = parseInt(kerning.getAttribute('amount'), 10); + + if (data.chars[second]) + { + data.chars[second].kerning[first] = amount; + } + } + + // I'm leaving this as a temporary fix so we can test the bitmap fonts in v3 + // but it's very likely to change + BitmapText.fonts[data.font] = data; + + return data; + } } BitmapText.fonts = {}; diff --git a/src/index.js b/src/index.js index 59356cf..c553427 100644 --- a/src/index.js +++ b/src/index.js @@ -16,6 +16,10 @@ import * as particles from './particles'; import * as prepare from './prepare'; +// handle mixins now, after all code has been added, including deprecation +import { utils } from './core'; +utils.mixins.performMixins(); + export { accessibility, extract, diff --git a/src/interaction/InteractionManager.js b/src/interaction/InteractionManager.js index 7d51134..8baf13a 100644 --- a/src/interaction/InteractionManager.js +++ b/src/interaction/InteractionManager.js @@ -1,18 +1,20 @@ 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'; -// Mix interactiveTarget into core.DisplayObject.prototype -Object.assign( +// Mix interactiveTarget into core.DisplayObject.prototype, after deprecation has been handled +core.utils.mixins.delayMixin( core.DisplayObject.prototype, 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 +70,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 +101,6 @@ this.eventData = new InteractionEvent(); /** - * Tiny little interactiveData pool ! - * - * @member {PIXI.interaction.InteractionData[]} - */ - this.interactiveDataPool = []; - - /** * The DOM element to bind to. * * @private @@ -153,66 +155,12 @@ */ this.supportsPointerEvents = !!window.PointerEvent; - /** - * Are touch events being 'normalized' and converted into pointer events if pointer events are not supported - * For example, on a touch screen mobile device, a touchstart would also be emitted as a pointerdown - * - * @private - * @readonly - * @member {boolean} - */ - this.normalizeTouchEvents = !this.supportsPointerEvents && this.supportsTouchEvents; - - /** - * Are mouse events being 'normalized' and converted into pointer events if pointer events are not supported - * For example, on a desktop pc, a mousedown would also be emitted as a pointerdown - * - * @private - * @readonly - * @member {boolean} - */ - this.normalizeMouseEvents = !this.supportsPointerEvents && !MobileDevice.any; - // this will make it so that you don't have to call bind all the time /** * @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 +168,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,41 +199,32 @@ this.onPointerOver = this.onPointerOver.bind(this); /** - * @private - * @member {Function} + * Dictionary of how different cursor modes are handled. Strings are handled as CSS cursor + * values, objects are handled as dictionaries of CSS values for interactionDOMElement, + * and functions are called instead of changing the CSS. + * Default CSS cursor values are provided for 'default' and 'pointer' modes. + * @member {Object.)>} */ - this.onTouchStart = this.onTouchStart.bind(this); - this.processTouchStart = this.processTouchStart.bind(this); + this.cursorStyles = { + default: 'inherit', + pointer: 'pointer', + }; /** - * @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. - * - * @member {string} - * @default 'inherit' - */ - this.defaultCursorStyle = 'inherit'; - - /** - * The css style of the cursor that is being used. + * The mode of the cursor that is being used. + * The value of this is a key from the cursorStyles dictionary. * * @member {string} */ - this.currentCursorStyle = 'inherit'; + this.currentCursorMode = null; + + /** + * Internal cached let. + * + * @private + * @member {string} + */ + this.cursor = null; /** * Internal cached let. @@ -303,6 +249,7 @@ * object. * * @event mousedown + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -311,6 +258,7 @@ * on the display object. * * @event rightdown + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -319,6 +267,7 @@ * object. * * @event mouseup + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -327,6 +276,7 @@ * over the display object. * * @event rightup + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -335,6 +285,7 @@ * the display object. * * @event click + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -343,6 +294,7 @@ * and released on the display object. * * @event rightclick + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -352,6 +304,7 @@ * [mousedown]{@link PIXI.interaction.InteractionManager#event:mousedown}. * * @event mouseupoutside + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -361,6 +314,7 @@ * [rightdown]{@link PIXI.interaction.InteractionManager#event:rightdown}. * * @event rightupoutside + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -368,6 +322,7 @@ * Fired when a pointer device (usually a mouse) is moved while over the display object * * @event mousemove + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -375,6 +330,7 @@ * Fired when a pointer device (usually a mouse) is moved onto the display object * * @event mouseover + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -382,6 +338,7 @@ * Fired when a pointer device (usually a mouse) is moved off the display object * * @event mouseout + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -389,6 +346,7 @@ * Fired when a pointer device button is pressed on the display object. * * @event pointerdown + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -396,6 +354,14 @@ * Fired when a pointer device button is released over the display object. * * @event pointerup + * @type {PIXI.interaction.InteractionData} + * @memberof PIXI.interaction.InteractionManager# + */ + + /** + * Fired when the operating system cancels a pointer event + * + * @event pointercancel * @memberof PIXI.interaction.InteractionManager# */ @@ -403,6 +369,7 @@ * Fired when a pointer device button is pressed and released on the display object. * * @event pointertap + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -411,6 +378,7 @@ * registered a [pointerdown]{@link PIXI.interaction.InteractionManager#event:pointerdown}. * * @event pointerupoutside + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -418,6 +386,7 @@ * Fired when a pointer device is moved while over the display object * * @event pointermove + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -425,6 +394,7 @@ * Fired when a pointer device is moved onto the display object * * @event pointerover + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -432,6 +402,7 @@ * Fired when a pointer device is moved off the display object * * @event pointerout + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -439,6 +410,7 @@ * Fired when a touch point is placed on the display object. * * @event touchstart + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -446,6 +418,14 @@ * Fired when a touch point is removed from the display object. * * @event touchend + * @type {PIXI.interaction.InteractionData} + * @memberof PIXI.interaction.InteractionManager# + */ + + /** + * Fired when the operating system cancels a touch + * + * @event touchcancel * @memberof PIXI.interaction.InteractionManager# */ @@ -453,6 +433,7 @@ * Fired when a touch point is placed and removed from the display object. * * @event tap + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -461,6 +442,7 @@ * registered a [touchstart]{@link PIXI.interaction.InteractionManager#event:touchstart}. * * @event touchendoutside + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -468,6 +450,7 @@ * Fired when a touch point is moved along the display object. * * @event touchmove + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ } @@ -524,45 +507,30 @@ { window.document.addEventListener('pointermove', this.onPointerMove, true); this.interactionDOMElement.addEventListener('pointerdown', this.onPointerDown, true); - this.interactionDOMElement.addEventListener('pointerout', this.onPointerOut, true); + // pointerout is fired in addition to pointerup (for touch events) and pointercancel + // we already handle those, so for the purposes of what we do in onPointerOut, we only + // care about the pointerleave event + this.interactionDOMElement.addEventListener('pointerleave', 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; @@ -596,45 +564,26 @@ { window.document.removeEventListener('pointermove', this.onPointerMove, true); this.interactionDOMElement.removeEventListener('pointerdown', this.onPointerDown, true); - this.interactionDOMElement.removeEventListener('pointerout', this.onPointerOut, true); + this.interactionDOMElement.removeEventListener('pointerleave', 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) + 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.onPointerDown, true); + this.interactionDOMElement.removeEventListener('touchcancel', this.onPointerCancel, 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); - - 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 = null; @@ -664,7 +613,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; @@ -672,25 +621,80 @@ return; } - this.cursor = this.defaultCursorStyle; + this.cursor = null; // 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(); - - this.processInteractive(this.mouse.global, this.renderer._lastObjectRendered, this.processMouseOverOut, true); - - if (this.currentCursorStyle !== this.cursor) + for (const k in this.activeInteractionData) { - this.currentCursorStyle = this.cursor; - this.interactionDOMElement.style.cursor = this.cursor; + // eslint-disable-next-line no-prototype-builtins + if (this.activeInteractionData.hasOwnProperty(k)) + { + const interactionData = this.activeInteractionData[k]; + + if (interactionData.originalEvent && interactionData.pointerType !== 'touch') + { + const interactionEvent = this.configureInteractionEventForDOMEvent( + this.eventData, + interactionData.originalEvent, + interactionData + ); + + this.processInteractive( + interactionEvent, + this.renderer._lastObjectRendered, + this.processPointerOverOut, + true + ); + } + } } + this.setCursorMode(this.cursor); + // TODO } /** + * Sets the current cursor mode, handling any callbacks or CSS style changes. + * + * @param {string} mode - cursor mode, a key from the cursorStyles dictionary + */ + setCursorMode(mode) + { + mode = mode || 'default'; + // if the mode didn't actually change, bail early + if (this.currentCursorMode === mode) + { + return; + } + this.currentCursorMode = mode; + const style = this.cursorStyles[mode]; + + // only do things if there is a cursor style for it + if (style) + { + switch (typeof style) + { + case 'string': + // string styles are handled as cursor CSS + this.interactionDOMElement.style.cursor = style; + break; + case 'function': + // functions are just called, and passed the cursor mode + style(mode); + break; + case 'object': + // if it is an object, assume that it is a dictionary of CSS styles, + // apply it to the interactionDOMElement + Object.assign(this.interactionDOMElement.style, style); + break; + } + } + } + + /** * Dispatches an event on the display object that was interacted with * * @param {PIXI.Container|PIXI.Sprite|PIXI.extras.TilingSprite} displayObject - the display object in question @@ -748,22 +752,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 @@ -787,9 +795,8 @@ { interactiveParent = false; } - - // it has a mask! Then lets hit test that before continuing.. - if (hitTest && displayObject._mask) + // it has a mask! Then lets hit test that before continuing + else if (hitTest && displayObject._mask) { if (!displayObject._mask.containsPoint(point)) { @@ -797,14 +804,7 @@ } } - // it has a filterArea! Same as mask but easier, its a rectangle - if (hitTest && displayObject.filterArea) - { - if (!displayObject.filterArea.contains(point.x, point.y)) - { - hitTest = false; - } - } + let keepHitTestingAfterChildren = hitTest; // ** FREE TIP **! If an object is not interactive or has no buttons in it // (such as a game scene!) set interactiveChildren to false for that displayObject. @@ -818,7 +818,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 @@ -838,15 +838,20 @@ // This means we no longer need to hit test anything else. We still need to run // through all objects, but we don't need to perform any hit tests. - // { - hitTest = false; - // } + keepHitTestingAfterChildren = false; + + if (child.interactive) + { + hitTest = false; + } // we can break now as we have hit an object. } } } + hitTest = keepHitTestingAfterChildren; + // no point running this if the item is not interactive or does not have an interactive parent. if (interactive) { @@ -868,14 +873,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 +886,175 @@ } /** - * 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].isNormalized) { - 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); + if (!displayObject.trackedPointers[id]) + { + displayObject.trackedPointers[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.trackedPointers[id].rightDown = true; + } + else + { + displayObject.trackedPointers[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.trackedPointers[id] !== undefined) + { + delete displayObject.trackedPointers[id]; + this.dispatchEvent(displayObject, 'pointercancel', interactionEvent); + + if (e.type === 'touchcancel' || e.pointerType === 'touch') + { + this.dispatchEvent(displayObject, 'touchcancel', interactionEvent); + } } } @@ -1159,42 +1066,94 @@ */ 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) { - if (hit) - { - this.dispatchEvent(displayObject, 'pointerup', this.eventData); + const e = interactionEvent.data.originalEvent; - if (displayObject._pointerDown) + const id = interactionEvent.data.identifier; + + const trackingData = displayObject.trackedPointers[id]; + + const isTouch = (e.type === 'touchend' || e.pointerType === 'touch'); + + const isMouse = (e.type.indexOf('mouse') === 0 || e.pointerType === 'mouse'); + + // 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) { - displayObject._pointerDown = false; - this.dispatchEvent(displayObject, 'pointertap', this.eventData); + 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); + } + // update the down state of the tracking data + if (trackingData) + { + if (isRightButton) + { + trackingData.rightDown = hit; + } + else + { + trackingData.leftDown = hit; + } } } - else if (displayObject._pointerDown) + + // Pointers and Touches, and Mouse + if (hit) { - displayObject._pointerDown = false; - this.dispatchEvent(displayObject, 'pointerupoutside', this.eventData); + this.dispatchEvent(displayObject, 'pointerup', interactionEvent); + if (isTouch) this.dispatchEvent(displayObject, 'touchend', interactionEvent); + + if (trackingData) + { + this.dispatchEvent(displayObject, 'pointertap', interactionEvent); + if (isTouch) + { + this.dispatchEvent(displayObject, 'tap', interactionEvent); + // touches are no longer over (if they ever were) when we get the touchend + // so we should ensure that we don't keep pretending that they are + trackingData.over = false; + } + } + } + else if (trackingData) + { + this.dispatchEvent(displayObject, 'pointerupoutside', interactionEvent); + if (isTouch) this.dispatchEvent(displayObject, 'touchendoutside', interactionEvent); + } + // Only remove the tracking data if there is no over/down state still associated with it + if (trackingData && trackingData.none) + { + delete displayObject.trackedPointers[id]; } } @@ -1202,39 +1161,78 @@ * 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 = null; + } - 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') + { + this.setCursorMode(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 (isMouse) { - 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 +1240,94 @@ * 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.setCursorMode(null); + } - 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'); + + let trackingData = displayObject.trackedPointers[id]; + + // if we just moused over the display object, then we need to track that state + if (hit && !trackingData) + { + trackingData = displayObject.trackedPointers[id] = new InteractionTrackingData(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); + } + } + + // only change the cursor if it has not already been changed (by something deeper in the + // display tree) + if (isMouse && this.cursor === null) + { + this.cursor = displayObject.cursor; } } - 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); + } + // if there is no mouse down information for the pointer, then it is safe to delete + if (trackingData.none) + { + delete displayObject.trackedPointers[id]; + } } } @@ -1287,253 +1335,154 @@ * 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 && pointerEvent.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; + + // mark the touch as normalized, just so that we know we did it + touch.isNormalized = true; + + normalizedEvents.push(touch); + } } - else if (this.normalizeMouseEvents) + // apparently PointerEvent subclasses MouseEvent, so yay + else if (event instanceof MouseEvent && (!this.supportsPointerEvents || !(event instanceof window.PointerEvent))) { if (typeof event.isPrimary === 'undefined') event.isPrimary = true; if (typeof event.width === 'undefined') event.width = 1; @@ -1541,10 +1490,21 @@ 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; + + // mark the mouse event as normalized, just so that we know we did it + event.isNormalized = true; + + normalizedEvents.push(event); } + else + { + normalizedEvents.push(event); + } + + return normalizedEvents; } /** @@ -1563,30 +1523,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 +1542,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..6a1f69e --- /dev/null +++ b/src/interaction/InteractionTrackingData.js @@ -0,0 +1,147 @@ +/** + * 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 inactive (not over or down)? + * + * @member {number} + * @memberof PIXI.interaction.InteractionTrackingData# + */ + get none() + { + return this._flags === this.constructor.FLAGS.NONE; + } + + /** + * 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..d252fba 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 * ); */ @@ -38,68 +38,56 @@ hitArea: null, /** - * If enabled, the mouse cursor will change when hovered over the displayObject if it is interactive + * If enabled, the mouse cursor use the pointer behavior when hovered over the displayObject if it is interactive + * Setting this changes the 'cursor' property to `'pointer'`. * - * @inner {boolean} + * @member {boolean} + * @memberof PIXI.interaction.interactiveTarget# */ - buttonMode: false, + get buttonMode() + { + return this.cursor === 'pointer'; + }, + set buttonMode(value) + { + if (value) + { + this.cursor = 'pointer'; + } + else if (this.cursor === 'pointer') + { + this.cursor = null; + } + }, /** - * If buttonMode is enabled, this defines what CSS cursor property is used when the mouse cursor - * is hovered over the displayObject + * This defines what cursor mode is used when the mouse cursor + * is hovered over the displayObject. * * @see https://developer.mozilla.org/en/docs/Web/CSS/cursor * * @inner {string} */ - defaultCursor: 'pointer', - - // some internal checks.. - /** - * Internal check to detect if the mouse cursor is hovered over the displayObject - * - * @inner {boolean} - * @private - */ - _over: false, + cursor: null, /** - * Internal check to detect if the left mouse button is pressed on the displayObject + * Internal set of all active pointers, by identifier * - * @inner {boolean} + * @member {Map} + * @memberof PIXI.interaction.interactiveTarget# * @private */ - _isLeftDown: false, + get trackedPointers() + { + if (this._trackedPointers === undefined) this._trackedPointers = {}; + + return this._trackedPointers; + }, /** - * Internal check to detect if the right mouse button is pressed on the displayObject + * Map of all tracked pointers, by identifier. Use trackedPointers to access. * - * @inner {boolean} - * @private + * @private {Map} */ - _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, + _trackedPointers: undefined, }; diff --git a/src/loaders/bitmapFontParser.js b/src/loaders/bitmapFontParser.js index cc2618c..f4cf996 100644 --- a/src/loaders/bitmapFontParser.js +++ b/src/loaders/bitmapFontParser.js @@ -1,63 +1,19 @@ import * as path from 'path'; -import { Rectangle, Texture, utils } from '../core'; +import { utils } from '../core'; import { Resource } from 'resource-loader'; import { BitmapText } from '../extras'; +/** + * Register a BitmapText font from loader resource. + * + * @function parseBitmapFontData + * @memberof PIXI.loaders + * @param {PIXI.loaders.Resource} resource - Loader resource. + * @param {PIXI.Texture} texture - Reference to texture. + */ export function parse(resource, texture) { - const data = {}; - const info = resource.data.getElementsByTagName('info')[0]; - const common = resource.data.getElementsByTagName('common')[0]; - - data.font = info.getAttribute('face'); - data.size = parseInt(info.getAttribute('size'), 10); - data.lineHeight = parseInt(common.getAttribute('lineHeight'), 10); - data.chars = {}; - - // parse letters - const letters = resource.data.getElementsByTagName('char'); - - for (let i = 0; i < letters.length; i++) - { - const charCode = parseInt(letters[i].getAttribute('id'), 10); - - const textureRect = new Rectangle( - parseInt(letters[i].getAttribute('x'), 10) + texture.frame.x, - parseInt(letters[i].getAttribute('y'), 10) + texture.frame.y, - parseInt(letters[i].getAttribute('width'), 10), - parseInt(letters[i].getAttribute('height'), 10) - ); - - data.chars[charCode] = { - xOffset: parseInt(letters[i].getAttribute('xoffset'), 10), - yOffset: parseInt(letters[i].getAttribute('yoffset'), 10), - xAdvance: parseInt(letters[i].getAttribute('xadvance'), 10), - kerning: {}, - texture: new Texture(texture.baseTexture, textureRect), - - }; - } - - // parse kernings - const kernings = resource.data.getElementsByTagName('kerning'); - - for (let i = 0; i < kernings.length; i++) - { - const first = parseInt(kernings[i].getAttribute('first'), 10); - const second = parseInt(kernings[i].getAttribute('second'), 10); - const amount = parseInt(kernings[i].getAttribute('amount'), 10); - - if (data.chars[second]) - { - data.chars[second].kerning[first] = amount; - } - } - - resource.bitmapFont = data; - - // I'm leaving this as a temporary fix so we can test the bitmap fonts in v3 - // but it's very likely to change - BitmapText.fonts[data.font] = data; + resource.bitmapFont = BitmapText.registerFont(resource.data, texture); } export default function () diff --git a/src/loaders/index.js b/src/loaders/index.js index 8942436..5189793 100644 --- a/src/loaders/index.js +++ b/src/loaders/index.js @@ -5,4 +5,11 @@ export { default as bitmapFontParser, parse as parseBitmapFontData } from './bitmapFontParser'; export { default as spritesheetParser } from './spritesheetParser'; export { default as textureParser } from './textureParser'; + +/** + * Reference to **resource-loader**'s Resource class. + * See https://github.com/englercj/resource-loader + * @class Resource + * @memberof PIXI.loaders + */ export { Resource } from 'resource-loader'; diff --git a/src/loaders/loader.js b/src/loaders/loader.js index 0a670cb..767a411 100644 --- a/src/loaders/loader.js +++ b/src/loaders/loader.js @@ -7,20 +7,47 @@ /** * - * The new loader, extends Resource Loader by Chad Engler : https://github.com/englercj/resource-loader + * The new loader, extends Resource Loader by Chad Engler: https://github.com/englercj/resource-loader * * ```js - * let loader = PIXI.loader; // pixi exposes a premade instance for you to use. + * const loader = PIXI.loader; // pixi exposes a premade instance for you to use. * //or - * let loader = new PIXI.loaders.Loader(); // you can also create your own if you want + * const loader = new PIXI.loaders.Loader(); // you can also create your own if you want * - * loader.add('bunny', 'data/bunny.png'); - * loader.add('spaceship', 'assets/spritesheet.json'); + * const sprites = {}; + * + * // Chainable `add` to enqueue a resource + * loader.add('bunny', 'data/bunny.png') + * .add('spaceship', 'assets/spritesheet.json'); * loader.add('scoreFont', 'assets/score.fnt'); * - * loader.once('complete',onAssetsLoaded); + * // Chainable `pre` to add a middleware that runs for each resource, *before* loading that resource. + * // This is useful to implement custom caching modules (using filesystem, indexeddb, memory, etc). + * loader.pre(cachingMiddleware); * - * loader.load(); + * // Chainable `use` to add a middleware that runs for each resource, *after* loading that resource. + * // This is useful to implement custom parsing modules (like spritesheet parsers, spine parser, etc). + * loader.use(parsingMiddleware); + * + * // The `load` method loads the queue of resources, and calls the passed in callback called once all + * // resources have loaded. + * loader.load((loader, resources) => { + * // resources is an object where the key is the name of the resource loaded and the value is the resource object. + * // They have a couple default properties: + * // - `url`: The URL that the resource was loaded from + * // - `error`: The error that happened when trying to load (if any) + * // - `data`: The raw data that was loaded + * // also may contain other properties based on the middleware that runs. + * sprites.bunny = new PIXI.TilingSprite(resources.bunny.texture); + * sprites.spaceship = new PIXI.TilingSprite(resources.spaceship.texture); + * sprites.scoreFont = new PIXI.TilingSprite(resources.scoreFont.texture); + * }); + * + * // throughout the process multiple signals can be dispatched. + * loader.onProgress.add(() => {}); // called once per loaded/errored file + * loader.onError.add(() => {}); // called once per errored file + * loader.onLoad.add(() => {}); // called once per loaded file + * loader.onComplete.add(() => {}); // called once when the queued resources all load. * ``` * * @see https://github.com/englercj/resource-loader diff --git a/src/loaders/spritesheetParser.js b/src/loaders/spritesheetParser.js index 7570332..e57dfb2 100644 --- a/src/loaders/spritesheetParser.js +++ b/src/loaders/spritesheetParser.js @@ -1,8 +1,6 @@ import { Resource } from 'resource-loader'; import path from 'path'; -import * as core from '../core'; - -const BATCH_SIZE = 1000; +import { Spritesheet } from '../core'; export default function () { @@ -43,134 +41,18 @@ // load the image for this sheet this.add(imageResourceName, resourcePath, loadOptions, function onImageLoad(res) { - resource.textures = {}; + const spritesheet = new Spritesheet( + res.texture.baseTexture, + resource.data, + resource.url + ); - const frames = resource.data.frames; - const frameKeys = Object.keys(frames); - const baseTexture = res.texture.baseTexture; - const scale = resource.data.meta.scale; - - // Use a defaultValue of `null` to check if a url-based resolution is set - let resolution = core.utils.getResolutionOfUrl(resource.url, null); - - // No resolution found via URL - if (resolution === null) + spritesheet.parse(() => { - // Use the scale value or default to 1 - resolution = scale !== undefined ? scale : 1; - } - - // For non-1 resolutions, update baseTexture - if (resolution !== 1) - { - baseTexture.resolution = resolution; - baseTexture.update(); - } - - let batchIndex = 0; - - function processFrames(initialFrameIndex, maxFrames) - { - let frameIndex = initialFrameIndex; - - while (frameIndex - initialFrameIndex < maxFrames && frameIndex < frameKeys.length) - { - const i = frameKeys[frameIndex]; - const rect = frames[i].frame; - - if (rect) - { - let frame = null; - let trim = null; - const orig = new core.Rectangle( - 0, - 0, - frames[i].sourceSize.w / resolution, - frames[i].sourceSize.h / resolution - ); - - if (frames[i].rotated) - { - frame = new core.Rectangle( - rect.x / resolution, - rect.y / resolution, - rect.h / resolution, - rect.w / resolution - ); - } - else - { - frame = new core.Rectangle( - rect.x / resolution, - rect.y / resolution, - rect.w / resolution, - rect.h / resolution - ); - } - - // Check to see if the sprite is trimmed - if (frames[i].trimmed) - { - trim = new core.Rectangle( - frames[i].spriteSourceSize.x / resolution, - frames[i].spriteSourceSize.y / resolution, - rect.w / resolution, - rect.h / resolution - ); - } - - resource.textures[i] = new core.Texture( - baseTexture, - frame, - orig, - trim, - frames[i].rotated ? 2 : 0 - ); - - // lets also add the frame to pixi's global cache for fromFrame and fromImage functions - core.utils.TextureCache[i] = resource.textures[i]; - } - - frameIndex++; - } - } - - function shouldProcessNextBatch() - { - return batchIndex * BATCH_SIZE < frameKeys.length; - } - - function processNextBatch(done) - { - processFrames(batchIndex * BATCH_SIZE, BATCH_SIZE); - batchIndex++; - setTimeout(done, 0); - } - - function iteration() - { - processNextBatch(() => - { - if (shouldProcessNextBatch()) - { - iteration(); - } - else - { - next(); - } - }); - } - - if (frameKeys.length <= BATCH_SIZE) - { - processFrames(0, BATCH_SIZE); + resource.spritesheet = spritesheet; + resource.textures = spritesheet.textures; next(); - } - else - { - iteration(); - } + }); }); }; } diff --git a/src/loaders/textureParser.js b/src/loaders/textureParser.js index 5398a7f..480edc1 100644 --- a/src/loaders/textureParser.js +++ b/src/loaders/textureParser.js @@ -1,5 +1,5 @@ -import * as core from '../core'; import { Resource } from 'resource-loader'; +import Texture from '../core/textures/Texture'; export default function () { @@ -8,23 +8,12 @@ // create a new texture if the data is an Image object if (resource.data && resource.type === Resource.TYPE.IMAGE) { - const baseTexture = new core.BaseTexture(resource.data, null, core.utils.getResolutionOfUrl(resource.url)); - - baseTexture.imageUrl = resource.url; - resource.texture = new core.Texture(baseTexture); - - // lets also add the frame to pixi's global cache for fromFrame and fromImage fucntions - core.utils.BaseTextureCache[resource.name] = baseTexture; - core.utils.TextureCache[resource.name] = resource.texture; - - // also add references by url if they are different. - if (resource.name !== resource.url) - { - core.utils.BaseTextureCache[resource.url] = baseTexture; - core.utils.TextureCache[resource.url] = resource.texture; - } + resource.texture = Texture.fromLoader( + resource.data, + resource.url, + resource.name + ); } - next(); }; } diff --git a/test/core/Container.js b/test/core/Container.js index 8a10b40..d11be33 100644 --- a/test/core/Container.js +++ b/test/core/Container.js @@ -1,5 +1,33 @@ 'use strict'; +function testAddChild(fn) +{ + fn(function (container, obj) + { + container.addChild(obj); + }); + fn(function (container, obj) + { + container.addChildAt(obj); + }); +} + +function testRemoveChild(fn) +{ + fn(function (container, obj) + { + container.removeChild(obj); + }); + fn(function (container, obj) + { + container.removeChildAt(container.children.indexOf(obj)); + }); + fn(function (container, obj) + { + container.removeChildren(container.children.indexOf(obj), container.children.indexOf(obj) + 1); + }); +} + describe('PIXI.Container', function () { describe('parent', function () @@ -70,6 +98,49 @@ expect(spy).to.have.been.called; expect(spy).to.have.been.calledWith(0); }); + + it('should flag child transform and container bounds for recalculation', testAddChild(function (mockAddChild) + { + const container = new PIXI.Container(); + const child = new PIXI.Container(); + + container.getBounds(); + child.getBounds(); + + const boundsID = container._boundsID; + const childParentID = child.transform._parentID; + + mockAddChild(container, child); + + expect(boundsID).to.not.be.equals(container._boundsID); + expect(childParentID).to.not.be.equals(child.transform._parentID); + })); + + it('should recalculate added child correctly', testAddChild(function (mockAddChild) + { + const parent = new PIXI.Container(); + const container = new PIXI.Container(); + const graphics = new PIXI.Graphics(); + + parent.addChild(container); + + graphics.drawRect(0, 0, 10, 10); + container.position.set(100, 200); + container.updateTransform(); + + graphics.getBounds(); + // Oops, that can happen sometimes! + graphics.transform._parentID = container.transform._worldID + 1; + + mockAddChild(container, graphics); + + const bounds = graphics.getBounds(); + + expect(bounds.x).to.be.equal(100); + expect(bounds.y).to.be.equal(200); + expect(bounds.width).to.be.equal(10); + expect(bounds.height).to.be.equal(10); + })); }); describe('removeChildAt', function () @@ -202,6 +273,44 @@ expect(spy).to.have.been.called; expect(spy).to.have.been.calledWith(0); }); + + it('should flag transform for recalculation', testRemoveChild(function (mockRemoveChild) + { + const container = new PIXI.Container(); + const child = new PIXI.Container(); + + container.addChild(child); + container.getBounds(); + + const childParentID = child.transform._parentID; + const boundsID = container._boundsID; + + mockRemoveChild(container, child); + + expect(childParentID).to.not.be.equals(child.transform._parentID); + expect(boundsID).to.not.be.equals(container._boundsID); + })); + + it('should recalculate removed child correctly', testRemoveChild(function (mockRemoveChild) + { + const parent = new PIXI.Container(); + const container = new PIXI.Container(); + const graphics = new PIXI.Graphics(); + + parent.addChild(container); + + graphics.drawRect(0, 0, 10, 10); + container.position.set(100, 200); + container.addChild(graphics); + graphics.getBounds(); + + mockRemoveChild(container, graphics); + + const bounds = graphics.getBounds(); + + expect(bounds.x).to.be.equal(0); + expect(bounds.y).to.be.equal(0); + })); }); describe('getChildIndex', function () diff --git a/package.json b/package.json index 6a366ba..71ade15 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pixi.js", - "version": "4.3.4", + "version": "4.3.5", "description": "Pixi.js is a fast lightweight 2D library that works across all devices.", "author": "Mat Groves", "contributors": [ @@ -61,7 +61,7 @@ "ismobilejs": "^0.4.0", "object-assign": "^4.0.1", "pixi-gl-core": "^1.0.3", - "resource-loader": "^2.0.4" + "resource-loader": "^2.0.6" }, "devDependencies": { "babel-cli": "^6.18.0", @@ -70,9 +70,9 @@ "babel-preset-es2015": "^6.14.0", "babelify": "^7.3.0", "del": "^2.2.0", - "electron-prebuilt": "^1.3.2", + "electron": "^1.4.15", "eslint": "^3.5.0", - "floss": "^1.2.0", + "floss": "^2.0.1", "gh-pages": "^0.11.0", "jaguarjs-jsdoc": "^1.0.1", "js-md5": "^0.4.1", diff --git a/src/accessibility/AccessibilityManager.js b/src/accessibility/AccessibilityManager.js index df2716a..f16bcb1 100644 --- a/src/accessibility/AccessibilityManager.js +++ b/src/accessibility/AccessibilityManager.js @@ -3,7 +3,7 @@ import accessibleTarget from './accessibleTarget'; // add some extra variables to the container.. -Object.assign( +core.utils.mixins.delayMixin( core.DisplayObject.prototype, accessibleTarget ); diff --git a/src/core/Application.js b/src/core/Application.js index ea0f058..5340578 100644 --- a/src/core/Application.js +++ b/src/core/Application.js @@ -1,6 +1,6 @@ import { autoDetectRenderer } from './autoDetectRenderer'; import Container from './display/Container'; -import Ticker from './ticker/Ticker'; +import { shared, Ticker } from './ticker'; /** * Convenience class to create a new PIXI application. @@ -35,9 +35,9 @@ * @param {boolean} [noWebGL=false] - prevents selection of WebGL renderer, even if such is present * @param {boolean} [options.legacy=false] - If true Pixi will aim to ensure compatibility * with older / less advanced devices. If you experience unexplained flickering try setting this to true. - + * @param {boolean} [useSharedTicker=false] - `true` to use PIXI.ticker.shared, `false` to create new ticker. */ - constructor(width, height, options, noWebGL) + constructor(width, height, options, noWebGL, useSharedTicker = false) { /** * WebGL renderer if available, otherwise CanvasRenderer @@ -46,23 +46,46 @@ this.renderer = autoDetectRenderer(width, height, options, noWebGL); /** - * The root display container that's renderered. + * The root display container that's rendered. * @member {PIXI.Container} */ this.stage = new Container(); /** + * Internal reference to the ticker + * @member {PIXI.ticker.Ticker} + * @private + */ + this._ticker = null; + + /** * Ticker for doing render updates. * @member {PIXI.ticker.Ticker} + * @default PIXI.ticker.shared */ - this.ticker = new Ticker(); - - this.ticker.add(this.render, this); + this.ticker = useSharedTicker ? shared : new Ticker(); // Start the rendering this.start(); } + set ticker(ticker) // eslint-disable-line require-jsdoc + { + if (this._ticker) + { + this._ticker.remove(this.render, this); + } + this._ticker = ticker; + if (ticker) + { + ticker.add(this.render, this); + } + } + get ticker() // eslint-disable-line require-jsdoc + { + return this._ticker; + } + /** * Render the current stage. */ @@ -76,7 +99,7 @@ */ stop() { - this.ticker.stop(); + this._ticker.stop(); } /** @@ -84,7 +107,7 @@ */ start() { - this.ticker.start(); + this._ticker.start(); } /** @@ -98,13 +121,22 @@ } /** + * Reference to the renderer's screen rectangle. Its safe to use as filterArea or hitArea for whole screen + * @member {PIXI.Rectangle} + * @readonly + */ + get screen() + { + return this.renderer.screen; + } + + /** * Destroy and don't use after this. * @param {Boolean} [removeView=false] Automatically remove canvas from DOM. */ destroy(removeView) { this.stop(); - this.ticker.remove(this.render, this); this.ticker = null; this.stage.destroy(); diff --git a/src/core/Shader.js b/src/core/Shader.js new file mode 100644 index 0000000..78b17e6 --- /dev/null +++ b/src/core/Shader.js @@ -0,0 +1,46 @@ +import { GLShader } from 'pixi-gl-core'; +import settings from './settings'; + +function checkPrecision(src, def) +{ + if (src instanceof Array) + { + if (src[0].substring(0, 9) !== 'precision') + { + const copy = src.slice(0); + + copy.unshift(`precision ${def} float;`); + + return copy; + } + } + else if (src.substring(0, 9) !== 'precision') + { + return `precision ${def} float;\n${src}`; + } + + return src; +} + +/** + * Wrapper class, webGL Shader for Pixi. + * Adds precision string if vertexSrc or fragmentSrc have no mention of it. + * + * @class + * @extends GLShader + * @memberof PIXI + */ +export default class Shader extends GLShader +{ + /** + * + * @param {WebGLRenderingContext} gl - The current WebGL rendering context + * @param {string|string[]} vertexSrc - The vertex shader source as an array of strings. + * @param {string|string[]} fragmentSrc - The fragment shader source as an array of strings. + */ + constructor(gl, vertexSrc, fragmentSrc) + { + super(gl, checkPrecision(vertexSrc, settings.PRECISION_VERTEX), + checkPrecision(fragmentSrc, settings.PRECISION_FRAGMENT)); + } +} diff --git a/src/core/const.js b/src/core/const.js index ef6f662..3de0efe 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -188,7 +188,7 @@ * The gc modes that are supported by pixi. * * The {@link PIXI.settings.GC_MODE} Garbage Collection mode for pixi textures is AUTO - * If set to GC_MODE, the renderer will occasianally check textures usage. If they are not + * If set to GC_MODE, the renderer will occasionally check textures usage. If they are not * used for a specified period of time they will be removed from the GPU. They will of course * be uploaded again when they are required. This is a silent behind the scenes process that * should ensure that the GPU does not get filled up. diff --git a/src/core/display/Container.js b/src/core/display/Container.js index ed8cd5f..17fde8d 100644 --- a/src/core/display/Container.js +++ b/src/core/display/Container.js @@ -73,11 +73,12 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.push(child); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -108,11 +109,12 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.splice(index, 0, child); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -225,10 +227,11 @@ if (index === -1) return null; child.parent = null; + // ensure child transform will be recalculated + child.transform._parentID = -1; removeItems(this.children, index, 1); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -249,11 +252,12 @@ { const child = this.getChildAt(index); + // ensure child transform will be recalculated.. child.parent = null; + child.transform._parentID = -1; removeItems(this.children, index, 1); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -284,13 +288,12 @@ for (let i = 0; i < removed.length; ++i) { removed[i].parent = null; + if (removed[i].transform) + { + removed[i].transform._parentID = -1; + } } - // ensure a transform will be recalculated.. - if (this.transform) - { - this.transform._parentID = -1; - } this._boundsID++; this.onChildrenChange(beginIndex); diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index f37f2f5..be4c4fb 100644 --- a/src/core/graphics/Graphics.js +++ b/src/core/graphics/Graphics.js @@ -28,8 +28,9 @@ { /** * + * @param {boolean} [nativeLines=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP */ - constructor() + constructor(nativeLines = false) { super(); @@ -50,6 +51,13 @@ this.lineWidth = 0; /** + * If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * + * @member {boolean} + */ + this.nativeLines = nativeLines; + + /** * The color of any lines drawn. * * @member {string} @@ -757,28 +765,14 @@ if (!this._spriteRect) { - if (!Graphics._SPRITE_TEXTURE) - { - Graphics._SPRITE_TEXTURE = RenderTexture.create(10, 10); - - const canvas = document.createElement('canvas'); - - canvas.width = 10; - canvas.height = 10; - - const context = canvas.getContext('2d'); - - context.fillStyle = 'white'; - context.fillRect(0, 0, 10, 10); - - Graphics._SPRITE_TEXTURE = Texture.fromCanvas(canvas); - } - - this._spriteRect = new Sprite(Graphics._SPRITE_TEXTURE); + this._spriteRect = new Sprite(new Texture(Texture.WHITE)); } + + const sprite = this._spriteRect; + if (this.tint === 0xffffff) { - this._spriteRect.tint = this.graphicsData[0].fillColor; + sprite.tint = this.graphicsData[0].fillColor; } else { @@ -792,20 +786,21 @@ t1[1] *= t2[1]; t1[2] *= t2[2]; - this._spriteRect.tint = rgb2hex(t1); + sprite.tint = rgb2hex(t1); } - this._spriteRect.alpha = this.graphicsData[0].fillAlpha; - this._spriteRect.worldAlpha = this.worldAlpha * this._spriteRect.alpha; + sprite.alpha = this.graphicsData[0].fillAlpha; + sprite.worldAlpha = this.worldAlpha * sprite.alpha; + sprite.blendMode = this.blendMode; - Graphics._SPRITE_TEXTURE._frame.width = rect.width; - Graphics._SPRITE_TEXTURE._frame.height = rect.height; + sprite.texture._frame.width = rect.width; + sprite.texture._frame.height = rect.height; - this._spriteRect.transform.worldTransform = this.transform.worldTransform; + sprite.transform.worldTransform = this.transform.worldTransform; - this._spriteRect.anchor.set(-rect.x / rect.width, -rect.y / rect.height); - this._spriteRect._onAnchorUpdate(); + sprite.anchor.set(-rect.x / rect.width, -rect.y / rect.height); + sprite._onAnchorUpdate(); - this._spriteRect._renderWebGL(renderer); + sprite._renderWebGL(renderer); } /** @@ -1031,6 +1026,7 @@ this.fillColor, this.fillAlpha, this.filling, + this.nativeLines, shape ); diff --git a/src/core/graphics/GraphicsData.js b/src/core/graphics/GraphicsData.js index f5a87ae..b6f03c0 100644 --- a/src/core/graphics/GraphicsData.js +++ b/src/core/graphics/GraphicsData.js @@ -14,14 +14,19 @@ * @param {number} fillColor - the color of the fill * @param {number} fillAlpha - the alpha of the fill * @param {boolean} fill - whether or not the shape is filled with a colour + * @param {boolean} nativeLines - the method for drawing lines * @param {PIXI.Circle|PIXI.Rectangle|PIXI.Ellipse|PIXI.Polygon} shape - The shape object to draw. */ - constructor(lineWidth, lineColor, lineAlpha, fillColor, fillAlpha, fill, shape) + constructor(lineWidth, lineColor, lineAlpha, fillColor, fillAlpha, fill, nativeLines, shape) { /** * @member {number} the width of the line to draw */ this.lineWidth = lineWidth; + /** + * @member {boolean} if true the liens will be draw using LINES instead of TRIANGLE_STRIP + */ + this.nativeLines = nativeLines; /** * @member {number} the color of the line to draw @@ -85,6 +90,7 @@ this.fillColor, this.fillAlpha, this.fill, + this.nativeLines, this.shape ); } diff --git a/src/core/graphics/webgl/GraphicsRenderer.js b/src/core/graphics/webgl/GraphicsRenderer.js index 14083fe..67f5e92 100644 --- a/src/core/graphics/webgl/GraphicsRenderer.js +++ b/src/core/graphics/webgl/GraphicsRenderer.js @@ -102,7 +102,15 @@ shaderTemp.uniforms.alpha = graphics.worldAlpha; renderer.bindVao(webGLData.vao); - webGLData.vao.draw(gl.TRIANGLE_STRIP, webGLData.indices.length); + + if (graphics.nativeLines) + { + gl.drawArrays(gl.LINES, 0, webGLData.points.length / 6); + } + else + { + webGLData.vao.draw(gl.TRIANGLE_STRIP, webGLData.indices.length); + } } } diff --git a/src/core/graphics/webgl/utils/buildLine.js b/src/core/graphics/webgl/utils/buildLine.js index ecf43f7..4d6862e 100644 --- a/src/core/graphics/webgl/utils/buildLine.js +++ b/src/core/graphics/webgl/utils/buildLine.js @@ -11,7 +11,29 @@ * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties * @param {object} webGLData - an object containing all the webGL-specific information to create this shape */ -export default function buildLine(graphicsData, webGLData) +export default function (graphicsData, webGLData) +{ + if (graphicsData.nativeLines) + { + buildNativeLine(graphicsData, webGLData); + } + else + { + buildLine(graphicsData, webGLData); + } +} + +/** + * Builds a line to draw using the poligon method. + * + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @private + * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties + * @param {object} webGLData - an object containing all the webGL-specific information to create this shape + */ +function buildLine(graphicsData, webGLData) { // TODO OPTIMISE! let points = graphicsData.points; @@ -224,3 +246,46 @@ indices.push(indexStart - 1); } + +/** + * Builds a line to draw using the gl.drawArrays(gl.LINES) method + * + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @private + * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties + * @param {object} webGLData - an object containing all the webGL-specific information to create this shape + */ +function buildNativeLine(graphicsData, webGLData) +{ + let i = 0; + const points = graphicsData.points; + + if (points.length === 0) return; + + const verts = webGLData.points; + const length = points.length / 2; + + // sort color + const color = hex2rgb(graphicsData.lineColor); + const alpha = graphicsData.lineAlpha; + const r = color[0] * alpha; + const g = color[1] * alpha; + const b = color[2] * alpha; + + for (i = 1; i < length; i++) + { + const p1x = points[(i - 1) * 2]; + const p1y = points[((i - 1) * 2) + 1]; + + const p2x = points[i * 2]; + const p2y = points[(i * 2) + 1]; + + verts.push(p1x, p1y); + verts.push(r, g, b, alpha); + + verts.push(p2x, p2y); + verts.push(r, g, b, alpha); + } +} diff --git a/src/core/graphics/webgl/utils/buildRoundedRectangle.js b/src/core/graphics/webgl/utils/buildRoundedRectangle.js index 9abc807..a6c8461 100644 --- a/src/core/graphics/webgl/utils/buildRoundedRectangle.js +++ b/src/core/graphics/webgl/utils/buildRoundedRectangle.js @@ -77,6 +77,26 @@ } /** + * Calculate a single point for a quadratic bezier curve. + * Utility function used by quadraticBezierCurve. + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @private + * @param {number} n1 - first number + * @param {number} n2 - second number + * @param {number} perc - percentage + * @return {number} the result + * + */ +function getPt(n1, n2, perc) +{ + const diff = n2 - n1; + + return n1 + (diff * perc); +} + +/** * Calculate the points for a quadratic bezier curve. (helper function..) * Based on: https://stackoverflow.com/questions/785097/how-do-i-implement-a-bezier-curve-in-c * @@ -105,13 +125,6 @@ let x = 0; let y = 0; - function getPt(n1, n2, perc) - { - const diff = n2 - n1; - - return n1 + (diff * perc); - } - for (let i = 0, j = 0; i <= n; ++i) { j = i / n; diff --git a/src/core/index.js b/src/core/index.js index f7a817c..4f414e8 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -30,6 +30,7 @@ export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; export { default as CanvasGraphicsRenderer } from './graphics/canvas/CanvasGraphicsRenderer'; +export { default as Spritesheet } from './textures/Spritesheet'; export { default as Texture } from './textures/Texture'; export { default as BaseTexture } from './textures/BaseTexture'; export { default as RenderTexture } from './textures/RenderTexture'; diff --git a/src/core/math/Matrix.js b/src/core/math/Matrix.js index 0b77477..3679860 100644 --- a/src/core/math/Matrix.js +++ b/src/core/math/Matrix.js @@ -13,45 +13,50 @@ export default class Matrix { /** - * + * @param {number} [a=1] - x scale + * @param {number} [b=0] - y skew + * @param {number} [c=0] - x skew + * @param {number} [d=1] - y scale + * @param {number} [tx=0] - x translation + * @param {number} [ty=0] - y translation */ - constructor() + constructor(a = 1, b = 0, c = 0, d = 1, tx = 0, ty = 0) { /** * @member {number} * @default 1 */ - this.a = 1; + this.a = a; /** * @member {number} * @default 0 */ - this.b = 0; + this.b = b; /** * @member {number} * @default 0 */ - this.c = 0; + this.c = c; /** * @member {number} * @default 1 */ - this.d = 1; + this.d = d; /** * @member {number} * @default 0 */ - this.tx = 0; + this.tx = tx; /** * @member {number} * @default 0 */ - this.ty = 0; + this.ty = ty; this.array = null; } diff --git a/src/core/renderers/SystemRenderer.js b/src/core/renderers/SystemRenderer.js index fa116a6..a24c775 100644 --- a/src/core/renderers/SystemRenderer.js +++ b/src/core/renderers/SystemRenderer.js @@ -1,5 +1,5 @@ import { sayHello, hex2string, hex2rgb } from '../utils'; -import { Matrix } from '../math'; +import { Matrix, Rectangle } from '../math'; import { RENDERER_TYPE } from '../const'; import settings from '../settings'; import Container from '../display/Container'; @@ -21,8 +21,8 @@ { /** * @param {string} system - The name of the system this renderer is for. - * @param {number} [width=800] - the width of the canvas view - * @param {number} [height=600] - the height of the canvas view + * @param {number} [screenWidth=800] - the width of the screen + * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false @@ -37,7 +37,7 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(system, width, height, options) + constructor(system, screenWidth, screenHeight, options) { super(); @@ -69,20 +69,13 @@ this.type = RENDERER_TYPE.UNKNOWN; /** - * The width of the canvas view + * Measurements of the screen. (0, 0, screenWidth, screenHeight) * - * @member {number} - * @default 800 - */ - this.width = width || 800; - - /** - * The height of the canvas view + * Its safe to use as filterArea or hitArea for whole stage * - * @member {number} - * @default 600 + * @member {PIXI.Rectangle} */ - this.height = height || 600; + this.screen = new Rectangle(0, 0, screenWidth || 800, screenHeight || 600); /** * The canvas element that everything is drawn to @@ -107,7 +100,7 @@ this.transparent = options.transparent; /** - * Whether the render view should be resized automatically + * Whether css dimensions of canvas view should be resized to screen dimensions automatically * * @member {boolean} */ @@ -192,23 +185,48 @@ } /** - * Resizes the canvas view to the specified width and height + * Same as view.width, actual number of pixels in the canvas by horizontal * - * @param {number} width - the new width of the canvas view - * @param {number} height - the new height of the canvas view + * @member {number} + * @readonly + * @default 800 */ - resize(width, height) + get width() { - this.width = width * this.resolution; - this.height = height * this.resolution; + return this.view.width; + } - this.view.width = this.width; - this.view.height = this.height; + /** + * Same as view.height, actual number of pixels in the canvas by vertical + * + * @member {number} + * @readonly + * @default 600 + */ + get height() + { + return this.view.height; + } + + /** + * Resizes the screen and canvas to the specified width and height + * Canvas dimensions are multiplied by resolution + * + * @param {number} screenWidth - the new width of the screen + * @param {number} screenHeight - the new height of the screen + */ + resize(screenWidth, screenHeight) + { + this.screen.width = screenWidth; + this.screen.height = screenHeight; + + this.view.width = screenWidth * this.resolution; + this.view.height = screenHeight * this.resolution; if (this.autoResize) { - this.view.style.width = `${this.width / this.resolution}px`; - this.view.style.height = `${this.height / this.resolution}px`; + this.view.style.width = `${screenWidth}px`; + this.view.style.height = `${screenHeight}px`; } } @@ -249,11 +267,10 @@ this.type = RENDERER_TYPE.UNKNOWN; - this.width = 0; - this.height = 0; - this.view = null; + this.screen = null; + this.resolution = 0; this.transparent = false; diff --git a/src/core/renderers/canvas/CanvasRenderer.js b/src/core/renderers/canvas/CanvasRenderer.js index 1bd2d5e..e84e49c 100644 --- a/src/core/renderers/canvas/CanvasRenderer.js +++ b/src/core/renderers/canvas/CanvasRenderer.js @@ -18,8 +18,8 @@ export default class CanvasRenderer extends SystemRenderer { /** - * @param {number} [width=800] - the width of the canvas view - * @param {number} [height=600] - the height of the canvas view + * @param {number} [screenWidth=800] - the width of the screen + * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false @@ -34,9 +34,9 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(width, height, options = {}) + constructor(screenWidth, screenHeight, options = {}) { - super('Canvas', width, height, options); + super('Canvas', screenWidth, screenHeight, options); this.type = RENDERER_TYPE.CANVAS; @@ -96,7 +96,7 @@ this.context = null; this.renderingToScreen = false; - this.resize(width, height); + this.resize(screenWidth, screenHeight); } /** @@ -283,12 +283,12 @@ * * @extends PIXI.SystemRenderer#resize * - * @param {number} width - The new width of the canvas view - * @param {number} height - The new height of the canvas view + * @param {number} screenWidth - the new width of the screen + * @param {number} screenHeight - the new height of the screen */ - resize(width, height) + resize(screenWidth, screenHeight) { - super.resize(width, height); + super.resize(screenWidth, screenHeight); // reset the scale mode.. oddly this seems to be reset when the canvas is resized. // surely a browser bug?? Let pixi fix that for you.. diff --git a/src/core/renderers/webgl/TextureManager.js b/src/core/renderers/webgl/TextureManager.js index b685c1a..8227289 100644 --- a/src/core/renderers/webgl/TextureManager.js +++ b/src/core/renderers/webgl/TextureManager.js @@ -61,7 +61,7 @@ * Updates and/or Creates a WebGL texture for the renderer's context. * * @param {PIXI.BaseTexture|PIXI.Texture} texture - the texture to update - * @param {Number} location - the location the texture will be bound to. + * @param {number} location - the location the texture will be bound to. * @return {GLTexture} The gl texture. */ updateTexture(texture, location) diff --git a/src/core/renderers/webgl/WebGLRenderer.js b/src/core/renderers/webgl/WebGLRenderer.js index e4cd8a0..830b382 100644 --- a/src/core/renderers/webgl/WebGLRenderer.js +++ b/src/core/renderers/webgl/WebGLRenderer.js @@ -31,8 +31,8 @@ { /** * - * @param {number} [width=0] - the width of the canvas view - * @param {number} [height=0] - the height of the canvas view + * @param {number} [screenWidth=800] - the width of the screen + * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false @@ -53,9 +53,9 @@ * @param {boolean} [options.legacy=false] - If true Pixi will aim to ensure compatibility * with older / less advanced devices. If you experiance unexplained flickering try setting this to true. */ - constructor(width, height, options = {}) + constructor(screenWidth, screenHeight, options = {}) { - super('WebGL', width, height, options); + super('WebGL', screenWidth, screenHeight, options); this.legacy = !!options.legacy; @@ -245,7 +245,7 @@ glCore._testingContext = gl; // setup the width/height properties and gl viewport - this.resize(this.width, this.height); + this.resize(this.screen.width, this.screen.height); } /** @@ -343,16 +343,16 @@ /** * Resizes the webGL view to the specified width and height. * - * @param {number} width - the new width of the webGL view - * @param {number} height - the new height of the webGL view + * @param {number} screenWidth - the new width of the screen + * @param {number} screenHeight - the new height of the screen */ - resize(width, height) + resize(screenWidth, screenHeight) { // if(width * this.resolution === this.width && height * this.resolution === this.height)return; - SystemRenderer.prototype.resize.call(this, width, height); + SystemRenderer.prototype.resize.call(this, screenWidth, screenHeight); - this.rootRenderTarget.resize(width, height); + this.rootRenderTarget.resize(screenWidth, screenHeight); if (this._activeRenderTarget === this.rootRenderTarget) { @@ -396,6 +396,26 @@ } /** + * Erases the render texture and fills the drawing area with a colour + * + * @param {PIXI.RenderTexture} renderTexture - The render texture to clear + * @param {number} [clearColor] - The colour + * @return {PIXI.WebGLRenderer} Returns itself. + */ + clearRenderTexture(renderTexture, clearColor) + { + const baseTexture = renderTexture.baseTexture; + const renderTarget = baseTexture._glRenderTargets[this.CONTEXT_UID]; + + if (renderTarget) + { + renderTarget.clear(clearColor); + } + + return this; + } + + /** * Binds a render texture for rendering * * @param {PIXI.RenderTexture} renderTexture - The render texture to render @@ -474,9 +494,10 @@ * Changes the current GLShader to the one given in parameter * * @param {PIXI.glCore.Shader} shader - the new glShader + * @param {boolean} [autoProject=true] - Whether automatically set the projection matrix * @return {PIXI.WebGLRenderer} Returns itself. */ - _bindGLShader(shader) + _bindGLShader(shader, autoProject) { // TODO cache if (this._activeShader !== shader) @@ -484,8 +505,14 @@ this._activeShader = shader; shader.bind(); - // automatically set the projection matrix - shader.uniforms.projectionMatrix = this._activeRenderTarget.projectionMatrix.toArray(true); + // `autoProject` normally would be a default parameter set to true + // but because of how Babel transpiles default parameters + // it hinders the performance of this method. + if (autoProject !== false) + { + // automatically set the projection matrix + shader.uniforms.projectionMatrix = this._activeRenderTarget.projectionMatrix.toArray(true); + } } return this; diff --git a/src/core/renderers/webgl/WebGLState.js b/src/core/renderers/webgl/WebGLState.js index ba6ecc5..6c846d5 100755 --- a/src/core/renderers/webgl/WebGLState.js +++ b/src/core/renderers/webgl/WebGLState.js @@ -82,18 +82,20 @@ push() { // next state.. - let state = this.stack[++this.stackIndex]; + let state = this.stack[this.stackIndex]; if (!state) { state = this.stack[this.stackIndex] = new Uint8Array(16); } + ++this.stackIndex; + // copy state.. // set active state so we can force overrides of gl state for (let i = 0; i < this.activeState.length; i++) { - this.activeState[i] = state[i]; + state[i] = this.activeState[i]; } } diff --git a/src/core/renderers/webgl/filters/Filter.js b/src/core/renderers/webgl/filters/Filter.js index 2c3eaad..4eb6128 100644 --- a/src/core/renderers/webgl/filters/Filter.js +++ b/src/core/renderers/webgl/filters/Filter.js @@ -9,7 +9,7 @@ * @memberof PIXI * @extends PIXI.Shader */ -class Filter extends Shader +export default class Filter extends Shader { /** * @param {string} [vertexSrc] - The source of the vertex shader. @@ -56,15 +56,16 @@ * @param {PIXI.RenderTarget} input - The input render target. * @param {PIXI.RenderTarget} output - The target to output to. * @param {boolean} clear - Should the output be cleared before rendering to it + * @param {object} [currentState] - It's current state of filter. + * There are some useful properties in the currentState : + * target, filters, sourceFrame, destinationFrame, renderTarget, resolution */ - apply(filterManager, input, output, clear) + apply(filterManager, input, output, clear, currentState) // eslint-disable-line no-unused-vars { // do as you please! - filterManager.applyFilter(this, input, output, clear); + filterManager.applyFilter(this, input, output, clear, currentState); // or just do a regular render.. } } - -export default Filter; diff --git a/src/core/renderers/webgl/filters/extractUniformsFromSrc.js b/src/core/renderers/webgl/filters/extractUniformsFromSrc.js index d78a0b1..4893214 100644 --- a/src/core/renderers/webgl/filters/extractUniformsFromSrc.js +++ b/src/core/renderers/webgl/filters/extractUniformsFromSrc.js @@ -12,7 +12,7 @@ function extractUniformsFromString(string) { - const maskRegex = new RegExp('^(projectionMatrix|uSampler|filterArea)$'); + const maskRegex = new RegExp('^(projectionMatrix|uSampler|filterArea|filterClamp)$'); const uniforms = {}; let nameSplit; diff --git a/src/core/renderers/webgl/managers/FilterManager.js b/src/core/renderers/webgl/managers/FilterManager.js index 43fc869..6c7422d 100644 --- a/src/core/renderers/webgl/managers/FilterManager.js +++ b/src/core/renderers/webgl/managers/FilterManager.js @@ -2,6 +2,7 @@ import RenderTarget from '../utils/RenderTarget'; import Quad from '../utils/Quad'; import { Rectangle } from '../../../math'; +import Shader from '../../../Shader'; import * as filterTransforms from '../filters/filterTransforms'; import bitTwiddle from 'bit-twiddle'; @@ -22,8 +23,6 @@ this.filters = []; this.target = null; this.resolution = 1; - - this.firstRun = true; } } @@ -155,7 +154,7 @@ if (filters.length === 1) { - filters[0].apply(this, currentState.renderTarget, lastState.renderTarget, false); + filters[0].apply(this, currentState.renderTarget, lastState.renderTarget, false, currentState); this.freePotRenderTarget(currentState.renderTarget); } else @@ -177,7 +176,7 @@ for (i = 0; i < filters.length - 1; ++i) { - filters[i].apply(this, flip, flop, true); + filters[i].apply(this, flip, flop, true, currentState); const t = flip; @@ -185,7 +184,7 @@ flop = t; } - filters[i].apply(this, flip, lastState.renderTarget, false); + filters[i].apply(this, flip, lastState.renderTarget, false, currentState); this.freePotRenderTarget(flip); this.freePotRenderTarget(flop); @@ -212,16 +211,28 @@ const renderer = this.renderer; const gl = renderer.gl; - renderer.bindShader(filter, true); + let shader = filter.glShaders[renderer.CONTEXT_UID]; - // TODO theres a better way! - // quad can be a mesh and we then should be able to pull this off without accessing the shader directly - const shader = renderer.shaderManager.getGLShader(); - - // TODO change quad to mesh.. - if (!this.firstRun) + // cacheing.. + if (!shader) { - this.firstRun = true; + if (filter.glShaderKey) + { + shader = this.shaderCache[filter.glShaderKey]; + + if (!shader) + { + shader = new Shader(this.gl, filter.vertexSrc, filter.fragmentSrc); + + filter.glShaders[renderer.CONTEXT_UID] = this.shaderCache[filter.glShaderKey] = shader; + } + } + else + { + shader = filter.glShaders[renderer.CONTEXT_UID] = new Shader(this.gl, filter.vertexSrc, filter.fragmentSrc); + } + + // TODO - this only needs to be done once? renderer.bindVao(null); this.quad.initVao(shader); @@ -244,58 +255,146 @@ renderer.maskManager.pushScissorMask(null, renderer.maskManager.scissorData); } - // renderer._bindGLShader(shader); + renderer.bindShader(shader); - let currentState; - const uniforms = filter.uniforms; + // free unit 0 for us, doesn't matter what was there + // don't try to restore it, because syncUniforms can upload it to another slot + // and it'll be a problem + const tex = this.renderer.emptyTextures[0]; - if (uniforms.filterArea) - { - currentState = this.filterData.stack[this.filterData.index]; - - const filterArea = uniforms.filterArea; - - filterArea[0] = currentState.renderTarget.size.width; - filterArea[1] = currentState.renderTarget.size.height; - filterArea[2] = currentState.sourceFrame.x; - filterArea[3] = currentState.sourceFrame.y; - - filter.uniforms.filterArea = filterArea; - } - - // use this to clamp displaced texture coords so they belong to filterArea - // see displacementFilter fragment shader for an example - if (uniforms.filterClamp) - { - currentState = this.filterData.stack[this.filterData.index]; - - const filterClamp = uniforms.filterClamp; - - filterClamp[0] = 0; - filterClamp[1] = 0; - filterClamp[2] = (currentState.sourceFrame.width - 1) / currentState.renderTarget.size.width; - filterClamp[3] = (currentState.sourceFrame.height - 1) / currentState.renderTarget.size.height; - - filter.uniforms.filterClamp = filterClamp; - } - - renderer.shaderManager.setUniforms(filter.uniforms); + this.renderer.boundTextures[0] = tex; + // this syncs the pixi filters uniforms with glsl uniforms + this.syncUniforms(shader, filter); renderer.state.setBlendMode(filter.blendMode); - // temporary bypass cache.. - const tex = this.renderer.boundTextures[0]; - gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, input.texture.texture); this.quad.vao.draw(this.renderer.gl.TRIANGLES, 6, 0); - // restore cache. gl.bindTexture(gl.TEXTURE_2D, tex._glTextures[this.renderer.CONTEXT_UID].texture); } /** + * Uploads the uniforms of the filter. + * + * @param {GLShader} shader - The underlying gl shader. + * @param {PIXI.Filter} filter - The filter we are synchronizing. + */ + syncUniforms(shader, filter) + { + const uniformData = filter.uniformData; + const uniforms = filter.uniforms; + + // 0 is reserved for the pixi texture so we start at 1! + let textureCount = 1; + let currentState; + + // filterArea and filterClamp that are handled by FilterManager directly + // they must not appear in uniformData + + if (shader.uniforms.filterArea) + { + currentState = this.filterData.stack[this.filterData.index]; + + const filterArea = shader.uniforms.filterArea; + + filterArea[0] = currentState.renderTarget.size.width; + filterArea[1] = currentState.renderTarget.size.height; + filterArea[2] = currentState.sourceFrame.x; + filterArea[3] = currentState.sourceFrame.y; + + shader.uniforms.filterArea = filterArea; + } + + // use this to clamp displaced texture coords so they belong to filterArea + // see displacementFilter fragment shader for an example + if (shader.uniforms.filterClamp) + { + currentState = currentState || this.filterData.stack[this.filterData.index]; + + const filterClamp = shader.uniforms.filterClamp; + + filterClamp[0] = 0; + filterClamp[1] = 0; + filterClamp[2] = (currentState.sourceFrame.width - 1) / currentState.renderTarget.size.width; + filterClamp[3] = (currentState.sourceFrame.height - 1) / currentState.renderTarget.size.height; + + shader.uniforms.filterClamp = filterClamp; + } + + // TODO Cacheing layer.. + for (const i in uniformData) + { + if (uniformData[i].type === 'sampler2D' && uniforms[i] !== 0) + { + if (uniforms[i].baseTexture) + { + shader.uniforms[i] = this.renderer.bindTexture(uniforms[i].baseTexture, textureCount); + } + else + { + shader.uniforms[i] = textureCount; + + // TODO + // this is helpful as renderTargets can also be set. + // Although thinking about it, we could probably + // make the filter texture cache return a RenderTexture + // rather than a renderTarget + const gl = this.renderer.gl; + + this.renderer.boundTextures[textureCount] = this.renderer.emptyTextures[textureCount]; + gl.activeTexture(gl.TEXTURE0 + textureCount); + + uniforms[i].texture.bind(); + } + + textureCount++; + } + else if (uniformData[i].type === 'mat3') + { + // check if its pixi matrix.. + if (uniforms[i].a !== undefined) + { + shader.uniforms[i] = uniforms[i].toArray(true); + } + else + { + shader.uniforms[i] = uniforms[i]; + } + } + else if (uniformData[i].type === 'vec2') + { + // check if its a point.. + if (uniforms[i].x !== undefined) + { + const val = shader.uniforms[i] || new Float32Array(2); + + val[0] = uniforms[i].x; + val[1] = uniforms[i].y; + shader.uniforms[i] = val; + } + else + { + shader.uniforms[i] = uniforms[i]; + } + } + else if (uniformData[i].type === 'float') + { + if (shader.uniforms.data[i].value !== uniformData[i]) + { + shader.uniforms[i] = uniforms[i]; + } + } + else + { + shader.uniforms[i] = uniforms[i]; + } + } + } + + /** * Gets a render target from the pool, or creates a new one. * * @param {boolean} clear - Should we clear the render texture when we get it? @@ -442,7 +541,6 @@ renderTarget.resolution = resolution; renderTarget.defaultFrame.width = renderTarget.size.width = minWidth / resolution; renderTarget.defaultFrame.height = renderTarget.size.height = minHeight / resolution; - renderTarget.attachStencilBuffer(); return renderTarget; } diff --git a/src/core/settings.js b/src/core/settings.js index bb08cb0..f4dd087 100644 --- a/src/core/settings.js +++ b/src/core/settings.js @@ -77,11 +77,11 @@ * * @static * @memberof PIXI.settings - * @type {RegExp|string} + * @type {RegExp} * @example `@2x` - * @default /@(.+)x/ + * @default /@([0-9\.]+)x/ */ - RETINA_PREFIX: /@(.+)x/, + RETINA_PREFIX: /@([0-9\.]+)x/, /** * The default render options if none are supplied to {@link PIXI.WebGLRenderer} @@ -175,14 +175,24 @@ SCALE_MODE: 0, /** - * Default specify float precision in shaders. + * Default specify float precision in vertex shader. + * + * @static + * @memberof PIXI.settings + * @type {PIXI.PRECISION} + * @default PIXI.PRECISION.HIGH + */ + PRECISION_VERTEX: 'highp', + + /** + * Default specify float precision in fragment shader. * * @static * @memberof PIXI.settings * @type {PIXI.PRECISION} * @default PIXI.PRECISION.MEDIUM */ - PRECISION: 'mediump', + PRECISION_FRAGMENT: 'mediump', /** * Can we upload the same buffer in a single frame? diff --git a/src/core/sprites/Sprite.js b/src/core/sprites/Sprite.js index 28b0b18..121bf58 100644 --- a/src/core/sprites/Sprite.js +++ b/src/core/sprites/Sprite.js @@ -212,11 +212,11 @@ } else { - w0 = orig.width * (1 - anchor._x); - w1 = orig.width * -anchor._x; + w1 = -anchor._x * orig.width; + w0 = w1 + orig.width; - h0 = orig.height * (1 - anchor._y); - h1 = orig.height * -anchor._y; + h1 = -anchor._y * orig.height; + h0 = h1 + orig.height; } // xy @@ -269,11 +269,11 @@ const tx = wt.tx; const ty = wt.ty; - const w0 = (orig.width) * (1 - anchor._x); - const w1 = (orig.width) * -anchor._x; + const w1 = -anchor._x * orig.width; + const w0 = w1 + orig.width; - const h0 = orig.height * (1 - anchor._y); - const h1 = orig.height * -anchor._y; + const h1 = -anchor._y * orig.height; + const h0 = h1 + orig.height; // xy vertexData[0] = (a * w1) + (c * h1) + tx; @@ -440,7 +440,7 @@ * * @static * @param {number|string|PIXI.BaseTexture|HTMLCanvasElement|HTMLVideoElement} source Source to create texture from - * @return {PIXI.Sprite} The newly created texture + * @return {PIXI.Sprite} The newly created sprite */ static from(source) { @@ -538,8 +538,8 @@ } /** - * The tint applied to the sprite. This is a hex value. A value of - * 0xFFFFFF will remove any tint effect. + * The tint applied to the sprite. This is a hex value. + * A value of 0xFFFFFF will remove any tint effect. * * @member {number} * @default 0xFFFFFF diff --git a/src/core/sprites/canvas/CanvasTinter.js b/src/core/sprites/canvas/CanvasTinter.js index 1cf316b..27e5e0c 100644 --- a/src/core/sprites/canvas/CanvasTinter.js +++ b/src/core/sprites/canvas/CanvasTinter.js @@ -75,8 +75,8 @@ crop.width *= resolution; crop.height *= resolution; - canvas.width = crop.width; - canvas.height = crop.height; + canvas.width = Math.ceil(crop.width); + canvas.height = Math.ceil(crop.height); context.fillStyle = `#${(`00000${(color | 0).toString(16)}`).substr(-6)}`; @@ -130,8 +130,8 @@ crop.width *= resolution; crop.height *= resolution; - canvas.width = crop.width; - canvas.height = crop.height; + canvas.width = Math.ceil(crop.width); + canvas.height = Math.ceil(crop.height); context.globalCompositeOperation = 'copy'; context.fillStyle = `#${(`00000${(color | 0).toString(16)}`).substr(-6)}`; @@ -172,8 +172,8 @@ crop.width *= resolution; crop.height *= resolution; - canvas.width = crop.width; - canvas.height = crop.height; + canvas.width = Math.ceil(crop.width); + canvas.height = Math.ceil(crop.height); context.globalCompositeOperation = 'copy'; context.drawImage( diff --git a/src/core/sprites/webgl/SpriteRenderer.js b/src/core/sprites/webgl/SpriteRenderer.js index 2affcd5..7c352e2 100644 --- a/src/core/sprites/webgl/SpriteRenderer.js +++ b/src/core/sprites/webgl/SpriteRenderer.js @@ -30,7 +30,7 @@ /** * Number of values sent in the vertex buffer. - * positionX, positionY, colorR, colorG, colorB = 5 + * aVertexPosition(2), aTextureCoord(1), aColor(1), aTextureId(1) = 5 * * @member {number} */ @@ -75,7 +75,6 @@ this.shader = null; this.currentIndex = 0; - TICK = 0; this.groups = []; for (let k = 0; k < this.size; k++) diff --git a/src/core/textures/BaseTexture.js b/src/core/textures/BaseTexture.js index 853de7e..aeccf39 100644 --- a/src/core/textures/BaseTexture.js +++ b/src/core/textures/BaseTexture.js @@ -232,16 +232,24 @@ this.realWidth = this.source.naturalWidth || this.source.videoWidth || this.source.width; this.realHeight = this.source.naturalHeight || this.source.videoHeight || this.source.height; - this.width = this.realWidth / this.resolution; - this.height = this.realHeight / this.resolution; - - this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); + this._updateDimensions(); } this.emit('update', this); } /** + * Update dimensions from real values + */ + _updateDimensions() + { + this.width = this.realWidth / this.resolution; + this.height = this.realHeight / this.resolution; + + this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); + } + + /** * Load a source. * * If the source is not-immediately-available, such as an image that needs to be @@ -525,11 +533,7 @@ this.realWidth = Math.round(svgWidth * this.sourceScale); this.realHeight = Math.round(svgHeight * this.sourceScale); - this.width = this.realWidth / this.resolution; - this.height = this.realHeight / this.resolution; - - // Check pow2 after scale - this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); + this._updateDimensions(); // Create a canvas element const canvas = document.createElement('canvas'); @@ -690,4 +694,52 @@ return baseTexture; } + + /** + * Helper function that creates a base texture based on the source you provide. + * The source can be - image url, image element, canvas element. + * + * @static + * @param {string|HTMLImageElement|HTMLCanvasElement} source - The source to create base texture from. + * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values + * @param {number} [sourceScale=(auto)] - Scale for the original image, used with Svg images. + * @return {PIXI.BaseTexture} The new base texture. + */ + static from(source, scaleMode, sourceScale) + { + if (typeof source === 'string') + { + return BaseTexture.fromImage(source, undefined, scaleMode, sourceScale); + } + else if (source instanceof HTMLImageElement) + { + const imageUrl = source.src; + let baseTexture = BaseTextureCache[imageUrl]; + + if (!baseTexture) + { + baseTexture = new BaseTexture(source, scaleMode); + baseTexture.imageUrl = imageUrl; + + if (sourceScale) + { + baseTexture.sourceScale = sourceScale; + } + + // if there is an @2x at the end of the url we are going to assume its a highres image + baseTexture.resolution = getResolutionOfUrl(imageUrl); + + BaseTextureCache[imageUrl] = baseTexture; + } + + return baseTexture; + } + else if (source instanceof HTMLCanvasElement) + { + return BaseTexture.fromCanvas(source, scaleMode); + } + + // lets assume its a base texture! + return source; + } } diff --git a/src/core/textures/Spritesheet.js b/src/core/textures/Spritesheet.js new file mode 100644 index 0000000..d469c62 --- /dev/null +++ b/src/core/textures/Spritesheet.js @@ -0,0 +1,274 @@ +import { Rectangle, Texture } from '../'; +import { getResolutionOfUrl, TextureCache } from '../utils'; + +/** + * Utility class for maintaining reference to a collection + * of Textures on a single Spritesheet. + * + * @class + * @memberof PIXI + */ +export default class Spritesheet +{ + /** + * The maximum number of Textures to build per process. + * + * @type {number} + * @default 1000 + */ + static get BATCH_SIZE() + { + return 1000; + } + + /** + * @param {PIXI.BaseTexture} baseTexture Reference to the source BaseTexture object. + * @param {Object} data - Spritesheet image data. + * @param {string} [resolutionFilename] - The filename to consider when determining + * the resolution of the spritesheet. If not provided, the imageUrl will + * be used on the BaseTexture. + */ + constructor(baseTexture, data, resolutionFilename = null) + { + /** + * Reference to ths source texture + * @type {PIXI.BaseTexture} + */ + this.baseTexture = baseTexture; + + /** + * Map of spritesheet textures. + * @type {Object} + */ + this.textures = {}; + + /** + * Reference to the original JSON data. + * @type {Object} + */ + this.data = data; + + /** + * The resolution of the spritesheet. + * @type {number} + */ + this.resolution = this._updateResolution( + resolutionFilename || this.baseTexture.imageUrl + ); + + /** + * Map of spritesheet frames. + * @type {Object} + * @private + */ + this._frames = this.data.frames; + + /** + * Collection of frame names. + * @type {string[]} + * @private + */ + this._frameKeys = Object.keys(this._frames); + + /** + * Current batch index being processed. + * @type {number} + * @private + */ + this._batchIndex = 0; + + /** + * Callback when parse is completed. + * @type {Function} + * @private + */ + this._callback = null; + } + + /** + * Generate the resolution from the filename or fallback + * to the meta.scale field of the JSON data. + * + * @private + * @param {string} resolutionFilename - The filename to use for resolving + * the default resolution. + * @return {number} Resolution to use for spritesheet. + */ + _updateResolution(resolutionFilename) + { + const scale = this.data.meta.scale; + + // Use a defaultValue of `null` to check if a url-based resolution is set + let resolution = getResolutionOfUrl(resolutionFilename, null); + + // No resolution found via URL + if (resolution === null) + { + // Use the scale value or default to 1 + resolution = scale !== undefined ? parseFloat(scale) : 1; + } + + // For non-1 resolutions, update baseTexture + if (resolution !== 1) + { + this.baseTexture.resolution = resolution; + this.baseTexture.update(); + } + + return resolution; + } + + /** + * Parser spritesheet from loaded data. This is done asynchronously + * to prevent creating too many Texture within a single process. + * + * @param {Function} callback - Callback when complete returns + * a map of the Textures for this spritesheet. + */ + parse(callback) + { + this._batchIndex = 0; + this._callback = callback; + + if (this._frameKeys.length <= Spritesheet.BATCH_SIZE) + { + this._processFrames(0); + this._parseComplete(); + } + else + { + this._nextBatch(); + } + } + + /** + * Process a batch of frames + * + * @private + * @param {number} initialFrameIndex - The index of frame to start. + */ + _processFrames(initialFrameIndex) + { + let frameIndex = initialFrameIndex; + const maxFrames = Spritesheet.BATCH_SIZE; + + while (frameIndex - initialFrameIndex < maxFrames && frameIndex < this._frameKeys.length) + { + const i = this._frameKeys[frameIndex]; + const rect = this._frames[i].frame; + + if (rect) + { + let frame = null; + let trim = null; + const orig = new Rectangle( + 0, + 0, + this._frames[i].sourceSize.w / this.resolution, + this._frames[i].sourceSize.h / this.resolution + ); + + if (this._frames[i].rotated) + { + frame = new Rectangle( + rect.x / this.resolution, + rect.y / this.resolution, + rect.h / this.resolution, + rect.w / this.resolution + ); + } + else + { + frame = new Rectangle( + rect.x / this.resolution, + rect.y / this.resolution, + rect.w / this.resolution, + rect.h / this.resolution + ); + } + + // Check to see if the sprite is trimmed + if (this._frames[i].trimmed) + { + trim = new Rectangle( + this._frames[i].spriteSourceSize.x / this.resolution, + this._frames[i].spriteSourceSize.y / this.resolution, + rect.w / this.resolution, + rect.h / this.resolution + ); + } + + this.textures[i] = new Texture( + this.baseTexture, + frame, + orig, + trim, + this._frames[i].rotated ? 2 : 0 + ); + + // lets also add the frame to pixi's global cache for fromFrame and fromImage functions + TextureCache[i] = this.textures[i]; + } + + frameIndex++; + } + } + + /** + * The parse has completed. + * + * @private + */ + _parseComplete() + { + const callback = this._callback; + + this._callback = null; + this._batchIndex = 0; + callback.call(this, this.textures); + } + + /** + * Begin the next batch of textures. + * + * @private + */ + _nextBatch() + { + this._processFrames(this._batchIndex * Spritesheet.BATCH_SIZE); + this._batchIndex++; + setTimeout(() => + { + if (this._batchIndex * Spritesheet.BATCH_SIZE < this._frameKeys.length) + { + this._nextBatch(); + } + else + { + this._parseComplete(); + } + }, 0); + } + + /** + * Destroy Spritesheet and don't use after this. + * + * @param {boolean} [destroyBase=false] Whether to destroy the base texture as well + */ + destroy(destroyBase = false) + { + for (const i in this.textures) + { + this.textures[i].destroy(); + } + this._frames = null; + this._frameKeys = null; + this.data = null; + this.textures = null; + if (destroyBase) + { + this.baseTexture.destroy(); + } + this.baseTexture = null; + } +} diff --git a/src/core/textures/Texture.js b/src/core/textures/Texture.js index 6403d4f..afe4254 100644 --- a/src/core/textures/Texture.js +++ b/src/core/textures/Texture.js @@ -3,7 +3,7 @@ import TextureUvs from './TextureUvs'; import EventEmitter from 'eventemitter3'; import { Rectangle } from '../math'; -import { TextureCache, BaseTextureCache } from '../utils'; +import { TextureCache, BaseTextureCache, getResolutionOfUrl } from '../utils'; /** * A texture stores the information that represents an image or part of an image. It cannot be added @@ -365,7 +365,8 @@ * The source can be - frame id, image url, video url, canvas element, video element, base texture * * @static - * @param {number|string|PIXI.BaseTexture|HTMLCanvasElement|HTMLVideoElement} source - Source to create texture from + * @param {number|string|HTMLImageElement|HTMLCanvasElement|HTMLVideoElement|PIXI.BaseTexture} + * source - Source to create texture from * @return {PIXI.Texture} The newly created texture */ static from(source) @@ -393,7 +394,7 @@ } else if (source instanceof HTMLImageElement) { - return new Texture(new BaseTexture(source)); + return new Texture(BaseTexture.from(source)); } else if (source instanceof HTMLCanvasElement) { @@ -413,6 +414,43 @@ } /** + * Create a texture from a source and add to the cache. + * + * @static + * @param {HTMLImageElement|HTMLCanvasElement} source - The input source. + * @param {String} imageUrl - File name of texture, for cache and resolving resolution. + * @param {String} [name] - Human readible name for the texture cache. If no name is + * specified, only `imageUrl` will be used as the cache ID. + * @return {PIXI.Texture} Output texture + */ + static fromLoader(source, imageUrl, name) + { + const baseTexture = new BaseTexture(source, null, getResolutionOfUrl(imageUrl)); + const texture = new Texture(baseTexture); + + baseTexture.imageUrl = imageUrl; + + // No name, use imageUrl instead + if (!name) + { + name = imageUrl; + } + + // lets also add the frame to pixi's global cache for fromFrame and fromImage fucntions + BaseTextureCache[name] = baseTexture; + TextureCache[name] = texture; + + // also add references by url if they are different. + if (name !== imageUrl) + { + BaseTextureCache[imageUrl] = baseTexture; + TextureCache[imageUrl] = texture; + } + + return texture; + } + + /** * Adds a texture to the global TextureCache. This cache is shared across the whole PIXI object. * * @static @@ -522,6 +560,29 @@ } } +function createWhiteTexture() +{ + const canvas = document.createElement('canvas'); + + canvas.width = 10; + canvas.height = 10; + + const context = canvas.getContext('2d'); + + context.fillStyle = 'white'; + context.fillRect(0, 0, 10, 10); + + return new Texture(new BaseTexture(canvas)); +} + +function removeAllHandlers(tex) +{ + tex.destroy = function _emptyDestroy() { /* empty */ }; + tex.on = function _emptyOn() { /* empty */ }; + tex.once = function _emptyOnce() { /* empty */ }; + tex.emit = function _emptyEmit() { /* empty */ }; +} + /** * An empty texture, used often to not have to create multiple empty textures. * Can not be destroyed. @@ -530,7 +591,14 @@ * @constant */ Texture.EMPTY = new Texture(new BaseTexture()); -Texture.EMPTY.destroy = function _emptyDestroy() { /* empty */ }; -Texture.EMPTY.on = function _emptyOn() { /* empty */ }; -Texture.EMPTY.once = function _emptyOnce() { /* empty */ }; -Texture.EMPTY.emit = function _emptyEmit() { /* empty */ }; +removeAllHandlers(Texture.EMPTY); + +/** + * A white texture of 10x10 size, used for graphics and other things + * Can not be destroyed. + * + * @static + * @constant + */ +Texture.WHITE = createWhiteTexture(); +removeAllHandlers(Texture.WHITE); diff --git a/src/core/ticker/Ticker.js b/src/core/ticker/Ticker.js index d98a1f0..17a1517 100644 --- a/src/core/ticker/Ticker.js +++ b/src/core/ticker/Ticker.js @@ -67,11 +67,12 @@ * is based, this value is neither capped nor scaled. * If the platform supports DOMHighResTimeStamp, * this value will have a precision of 1 µs. + * Defaults to target frame time * * @member {number} - * @default 1 / TARGET_FPMS + * @default 16.66 */ - this.elapsedMS = 1 / settings.TARGET_FPMS; // default to target frame time + this.elapsedMS = 1 / settings.TARGET_FPMS; /** * The last time {@link PIXI.ticker.Ticker#update} was invoked. diff --git a/src/core/utils/index.js b/src/core/utils/index.js index 6ff609f..be44750 100644 --- a/src/core/utils/index.js +++ b/src/core/utils/index.js @@ -2,6 +2,7 @@ import settings from '../settings'; import EventEmitter from 'eventemitter3'; import pluginTarget from './pluginTarget'; +import * as mixins from './mixin'; import * as isMobile from 'ismobilejs'; let nextUid = 0; @@ -33,6 +34,7 @@ * @type {mixin} */ pluginTarget, + mixins, }; /** @@ -93,7 +95,7 @@ */ export function rgb2hex(rgb) { - return (((rgb[0] * 255) << 16) + ((rgb[1] * 255) << 8) + (rgb[2] * 255)); + return (((rgb[0] * 255) << 16) + ((rgb[1] * 255) << 8) + (rgb[2] * 255 | 0)); } /** @@ -368,3 +370,43 @@ * @private */ export const ProgramCache = {}; + +/** + * Destroys all texture in the cache + * + * @memberof PIXI.utils + * @function destroyTextureCache + */ +export function destroyTextureCache() +{ + let key; + + for (key in TextureCache) + { + TextureCache[key].destroy(); + } + for (key in BaseTextureCache) + { + BaseTextureCache[key].destroy(); + } +} + +/** + * Removes all textures from cache, but does not destroy them + * + * @memberof PIXI.utils + * @function clearTextureCache + */ +export function clearTextureCache() +{ + let key; + + for (key in TextureCache) + { + delete TextureCache[key]; + } + for (key in BaseTextureCache) + { + delete BaseTextureCache[key]; + } +} diff --git a/src/core/utils/mixin.js b/src/core/utils/mixin.js new file mode 100644 index 0000000..114de2a --- /dev/null +++ b/src/core/utils/mixin.js @@ -0,0 +1,59 @@ +/** + * Mixes all enumerable properties and methods from a source object to a target object. + * + * @memberof PIXI.utils.mixins + * @function mixin + * @param {object} target The prototype or instance that properties and methods should be added to. + * @param {object} source The source of properties and methods to mix in. + */ +export function mixin(target, source) +{ + if (!target || !source) return; + // in ES8/ES2017, this would be really easy: + // Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); + + // get all the enumerable property keys + const keys = Object.keys(source); + + // loop through properties + for (let i = 0; i < keys.length; ++i) + { + const propertyName = keys[i]; + + // Set the property using the property descriptor - this works for accessors and normal value properties + Object.defineProperty(target, propertyName, Object.getOwnPropertyDescriptor(source, propertyName)); + } +} + +const mixins = []; + +/** + * Queues a mixin to be handled towards the end of the initialization of PIXI, so that deprecation + * can take effect. + * + * @memberof PIXI.utils.mixins + * @function delayMixin + * @private + * @param {object} target The prototype or instance that properties and methods should be added to. + * @param {object} source The source of properties and methods to mix in. + */ +export function delayMixin(target, source) +{ + mixins.push(target, source); +} + +/** + * Handles all mixins queued via delayMixin(). + * + * @memberof PIXI.utils.mixins + * @function performMixins + * @private + */ +export function performMixins() +{ + for (let i = 0; i < mixins.length; i += 2) + { + mixin(mixins[i], mixins[i + 1]); + } + mixins.length = 0; +} diff --git a/src/deprecation.js b/src/deprecation.js index 045a19f..b9c73b3 100644 --- a/src/deprecation.js +++ b/src/deprecation.js @@ -5,6 +5,7 @@ import * as filters from './filters'; import * as prepare from './prepare'; import * as loaders from './loaders'; +import * as interaction from './interaction'; // provide method to give a stack track for warnings // useful for tracking-down where deprecated methods/properties/classes @@ -534,7 +535,7 @@ { parent: 'GC_MODES', target: 'GC_MODE' }, { parent: 'WRAP_MODES', target: 'WRAP_MODE' }, { parent: 'SCALE_MODES', target: 'SCALE_MODE' }, - { parent: 'PRECISION', target: 'PRECISION' }, + { parent: 'PRECISION', target: 'PRECISION_FRAGMENT' }, ]; for (let i = 0; i < defaults.length; i++) @@ -558,6 +559,32 @@ }); } +Object.defineProperties(core.settings, { + + /** + * @static + * @name PRECISION + * @memberof PIXI.settings + * @see PIXI.PRECISION + * @deprecated since version 4.4.0 + */ + PRECISION: { + enumerable: true, + get() + { + warn('PIXI.settings.PRECISION has been deprecated, please use PIXI.settings.PRECISION_FRAGMENT'); + + return core.settings.PRECISION_FRAGMENT; + }, + set(value) + { + warn('PIXI.settings.PRECISION has been deprecated, please use PIXI.settings.PRECISION_FRAGMENT'); + + core.settings.PRECISION_FRAGMENT = value; + }, + }, +}); + Object.defineProperties(extras, { /** @@ -694,7 +721,7 @@ warn('determineFontProperties is now deprecated, please use the static calculateFontProperties method, ' + 'e.g : Text.calculateFontProperties(fontStyle);'); - return Text.calculateFontProperties(fontStyle); + return core.Text.calculateFontProperties(fontStyle); }; Object.defineProperties(core.TextStyle.prototype, { @@ -1003,3 +1030,69 @@ }, }, }); + +/** + * @name PIXI.interaction.interactiveTarget#defaultCursor + * @static + * @type {number} + * @see PIXI.interaction.interactiveTarget#cursor + * @deprecated since 4.3.0 + */ +Object.defineProperty(interaction.interactiveTarget, 'defaultCursor', { + set(value) + { + warn('Property defaultCursor has been replaced with \'cursor\'. '); + this.cursor = value; + }, + get() + { + warn('Property defaultCursor has been replaced with \'cursor\'. '); + + return this.cursor; + }, + enumerable: true, +}); + +/** + * @name PIXI.interaction.InteractionManager#defaultCursorStyle + * @static + * @type {string} + * @see PIXI.interaction.InteractionManager#cursorStyles + * @deprecated since 4.3.0 + */ +Object.defineProperty(interaction.InteractionManager, 'defaultCursorStyle', { + set(value) + { + warn('Property defaultCursorStyle has been replaced with \'cursorStyles.default\'. '); + this.cursorStyles.default = value; + }, + get() + { + warn('Property defaultCursorStyle has been replaced with \'cursorStyles.default\'. '); + + return this.cursorStyles.default; + }, +}); + +/** + * @name PIXI.interaction.InteractionManager#currentCursorStyle + * @static + * @type {string} + * @see PIXI.interaction.InteractionManager#cursorStyles + * @deprecated since 4.3.0 + */ +Object.defineProperty(interaction.InteractionManager, 'currentCursorStyle', { + set(value) + { + warn('Property currentCursorStyle has been removed.' + + 'See the currentCursorMode property, which works differently.'); + this.currentCursorMode = value; + }, + get() + { + warn('Property currentCursorStyle has been removed.' + + 'See the currentCursorMode property, which works differently.'); + + return this.currentCursorMode; + }, +}); diff --git a/src/extras/BitmapText.js b/src/extras/BitmapText.js index fb6c343..0a0e150 100644 --- a/src/extras/BitmapText.js +++ b/src/extras/BitmapText.js @@ -455,6 +455,73 @@ return this._textHeight; } + + /** + * Register a bitmap font with data and a texture. + * + * @static + * @param {XMLDocument} xml - The XML document data. + * @param {PIXI.Texture} texture - Texture with all symbols. + * @return {Object} Result font object with font, size, lineHeight and char fields. + */ + static registerFont(xml, texture) + { + const data = {}; + const info = xml.getElementsByTagName('info')[0]; + const common = xml.getElementsByTagName('common')[0]; + + data.font = info.getAttribute('face'); + data.size = parseInt(info.getAttribute('size'), 10); + data.lineHeight = parseInt(common.getAttribute('lineHeight'), 10); + data.chars = {}; + + // parse letters + const letters = xml.getElementsByTagName('char'); + + for (let i = 0; i < letters.length; i++) + { + const letter = letters[i]; + const charCode = parseInt(letter.getAttribute('id'), 10); + + const textureRect = new core.Rectangle( + parseInt(letter.getAttribute('x'), 10) + texture.frame.x, + parseInt(letter.getAttribute('y'), 10) + texture.frame.y, + parseInt(letter.getAttribute('width'), 10), + parseInt(letter.getAttribute('height'), 10) + ); + + data.chars[charCode] = { + xOffset: parseInt(letter.getAttribute('xoffset'), 10), + yOffset: parseInt(letter.getAttribute('yoffset'), 10), + xAdvance: parseInt(letter.getAttribute('xadvance'), 10), + kerning: {}, + texture: new core.Texture(texture.baseTexture, textureRect), + + }; + } + + // parse kernings + const kernings = xml.getElementsByTagName('kerning'); + + for (let i = 0; i < kernings.length; i++) + { + const kerning = kernings[i]; + const first = parseInt(kerning.getAttribute('first'), 10); + const second = parseInt(kerning.getAttribute('second'), 10); + const amount = parseInt(kerning.getAttribute('amount'), 10); + + if (data.chars[second]) + { + data.chars[second].kerning[first] = amount; + } + } + + // I'm leaving this as a temporary fix so we can test the bitmap fonts in v3 + // but it's very likely to change + BitmapText.fonts[data.font] = data; + + return data; + } } BitmapText.fonts = {}; diff --git a/src/index.js b/src/index.js index 59356cf..c553427 100644 --- a/src/index.js +++ b/src/index.js @@ -16,6 +16,10 @@ import * as particles from './particles'; import * as prepare from './prepare'; +// handle mixins now, after all code has been added, including deprecation +import { utils } from './core'; +utils.mixins.performMixins(); + export { accessibility, extract, diff --git a/src/interaction/InteractionManager.js b/src/interaction/InteractionManager.js index 7d51134..8baf13a 100644 --- a/src/interaction/InteractionManager.js +++ b/src/interaction/InteractionManager.js @@ -1,18 +1,20 @@ 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'; -// Mix interactiveTarget into core.DisplayObject.prototype -Object.assign( +// Mix interactiveTarget into core.DisplayObject.prototype, after deprecation has been handled +core.utils.mixins.delayMixin( core.DisplayObject.prototype, 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 +70,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 +101,6 @@ this.eventData = new InteractionEvent(); /** - * Tiny little interactiveData pool ! - * - * @member {PIXI.interaction.InteractionData[]} - */ - this.interactiveDataPool = []; - - /** * The DOM element to bind to. * * @private @@ -153,66 +155,12 @@ */ this.supportsPointerEvents = !!window.PointerEvent; - /** - * Are touch events being 'normalized' and converted into pointer events if pointer events are not supported - * For example, on a touch screen mobile device, a touchstart would also be emitted as a pointerdown - * - * @private - * @readonly - * @member {boolean} - */ - this.normalizeTouchEvents = !this.supportsPointerEvents && this.supportsTouchEvents; - - /** - * Are mouse events being 'normalized' and converted into pointer events if pointer events are not supported - * For example, on a desktop pc, a mousedown would also be emitted as a pointerdown - * - * @private - * @readonly - * @member {boolean} - */ - this.normalizeMouseEvents = !this.supportsPointerEvents && !MobileDevice.any; - // this will make it so that you don't have to call bind all the time /** * @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 +168,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,41 +199,32 @@ this.onPointerOver = this.onPointerOver.bind(this); /** - * @private - * @member {Function} + * Dictionary of how different cursor modes are handled. Strings are handled as CSS cursor + * values, objects are handled as dictionaries of CSS values for interactionDOMElement, + * and functions are called instead of changing the CSS. + * Default CSS cursor values are provided for 'default' and 'pointer' modes. + * @member {Object.)>} */ - this.onTouchStart = this.onTouchStart.bind(this); - this.processTouchStart = this.processTouchStart.bind(this); + this.cursorStyles = { + default: 'inherit', + pointer: 'pointer', + }; /** - * @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. - * - * @member {string} - * @default 'inherit' - */ - this.defaultCursorStyle = 'inherit'; - - /** - * The css style of the cursor that is being used. + * The mode of the cursor that is being used. + * The value of this is a key from the cursorStyles dictionary. * * @member {string} */ - this.currentCursorStyle = 'inherit'; + this.currentCursorMode = null; + + /** + * Internal cached let. + * + * @private + * @member {string} + */ + this.cursor = null; /** * Internal cached let. @@ -303,6 +249,7 @@ * object. * * @event mousedown + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -311,6 +258,7 @@ * on the display object. * * @event rightdown + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -319,6 +267,7 @@ * object. * * @event mouseup + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -327,6 +276,7 @@ * over the display object. * * @event rightup + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -335,6 +285,7 @@ * the display object. * * @event click + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -343,6 +294,7 @@ * and released on the display object. * * @event rightclick + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -352,6 +304,7 @@ * [mousedown]{@link PIXI.interaction.InteractionManager#event:mousedown}. * * @event mouseupoutside + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -361,6 +314,7 @@ * [rightdown]{@link PIXI.interaction.InteractionManager#event:rightdown}. * * @event rightupoutside + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -368,6 +322,7 @@ * Fired when a pointer device (usually a mouse) is moved while over the display object * * @event mousemove + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -375,6 +330,7 @@ * Fired when a pointer device (usually a mouse) is moved onto the display object * * @event mouseover + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -382,6 +338,7 @@ * Fired when a pointer device (usually a mouse) is moved off the display object * * @event mouseout + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -389,6 +346,7 @@ * Fired when a pointer device button is pressed on the display object. * * @event pointerdown + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -396,6 +354,14 @@ * Fired when a pointer device button is released over the display object. * * @event pointerup + * @type {PIXI.interaction.InteractionData} + * @memberof PIXI.interaction.InteractionManager# + */ + + /** + * Fired when the operating system cancels a pointer event + * + * @event pointercancel * @memberof PIXI.interaction.InteractionManager# */ @@ -403,6 +369,7 @@ * Fired when a pointer device button is pressed and released on the display object. * * @event pointertap + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -411,6 +378,7 @@ * registered a [pointerdown]{@link PIXI.interaction.InteractionManager#event:pointerdown}. * * @event pointerupoutside + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -418,6 +386,7 @@ * Fired when a pointer device is moved while over the display object * * @event pointermove + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -425,6 +394,7 @@ * Fired when a pointer device is moved onto the display object * * @event pointerover + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -432,6 +402,7 @@ * Fired when a pointer device is moved off the display object * * @event pointerout + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -439,6 +410,7 @@ * Fired when a touch point is placed on the display object. * * @event touchstart + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -446,6 +418,14 @@ * Fired when a touch point is removed from the display object. * * @event touchend + * @type {PIXI.interaction.InteractionData} + * @memberof PIXI.interaction.InteractionManager# + */ + + /** + * Fired when the operating system cancels a touch + * + * @event touchcancel * @memberof PIXI.interaction.InteractionManager# */ @@ -453,6 +433,7 @@ * Fired when a touch point is placed and removed from the display object. * * @event tap + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -461,6 +442,7 @@ * registered a [touchstart]{@link PIXI.interaction.InteractionManager#event:touchstart}. * * @event touchendoutside + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -468,6 +450,7 @@ * Fired when a touch point is moved along the display object. * * @event touchmove + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ } @@ -524,45 +507,30 @@ { window.document.addEventListener('pointermove', this.onPointerMove, true); this.interactionDOMElement.addEventListener('pointerdown', this.onPointerDown, true); - this.interactionDOMElement.addEventListener('pointerout', this.onPointerOut, true); + // pointerout is fired in addition to pointerup (for touch events) and pointercancel + // we already handle those, so for the purposes of what we do in onPointerOut, we only + // care about the pointerleave event + this.interactionDOMElement.addEventListener('pointerleave', 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; @@ -596,45 +564,26 @@ { window.document.removeEventListener('pointermove', this.onPointerMove, true); this.interactionDOMElement.removeEventListener('pointerdown', this.onPointerDown, true); - this.interactionDOMElement.removeEventListener('pointerout', this.onPointerOut, true); + this.interactionDOMElement.removeEventListener('pointerleave', 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) + 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.onPointerDown, true); + this.interactionDOMElement.removeEventListener('touchcancel', this.onPointerCancel, 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); - - 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 = null; @@ -664,7 +613,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; @@ -672,25 +621,80 @@ return; } - this.cursor = this.defaultCursorStyle; + this.cursor = null; // 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(); - - this.processInteractive(this.mouse.global, this.renderer._lastObjectRendered, this.processMouseOverOut, true); - - if (this.currentCursorStyle !== this.cursor) + for (const k in this.activeInteractionData) { - this.currentCursorStyle = this.cursor; - this.interactionDOMElement.style.cursor = this.cursor; + // eslint-disable-next-line no-prototype-builtins + if (this.activeInteractionData.hasOwnProperty(k)) + { + const interactionData = this.activeInteractionData[k]; + + if (interactionData.originalEvent && interactionData.pointerType !== 'touch') + { + const interactionEvent = this.configureInteractionEventForDOMEvent( + this.eventData, + interactionData.originalEvent, + interactionData + ); + + this.processInteractive( + interactionEvent, + this.renderer._lastObjectRendered, + this.processPointerOverOut, + true + ); + } + } } + this.setCursorMode(this.cursor); + // TODO } /** + * Sets the current cursor mode, handling any callbacks or CSS style changes. + * + * @param {string} mode - cursor mode, a key from the cursorStyles dictionary + */ + setCursorMode(mode) + { + mode = mode || 'default'; + // if the mode didn't actually change, bail early + if (this.currentCursorMode === mode) + { + return; + } + this.currentCursorMode = mode; + const style = this.cursorStyles[mode]; + + // only do things if there is a cursor style for it + if (style) + { + switch (typeof style) + { + case 'string': + // string styles are handled as cursor CSS + this.interactionDOMElement.style.cursor = style; + break; + case 'function': + // functions are just called, and passed the cursor mode + style(mode); + break; + case 'object': + // if it is an object, assume that it is a dictionary of CSS styles, + // apply it to the interactionDOMElement + Object.assign(this.interactionDOMElement.style, style); + break; + } + } + } + + /** * Dispatches an event on the display object that was interacted with * * @param {PIXI.Container|PIXI.Sprite|PIXI.extras.TilingSprite} displayObject - the display object in question @@ -748,22 +752,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 @@ -787,9 +795,8 @@ { interactiveParent = false; } - - // it has a mask! Then lets hit test that before continuing.. - if (hitTest && displayObject._mask) + // it has a mask! Then lets hit test that before continuing + else if (hitTest && displayObject._mask) { if (!displayObject._mask.containsPoint(point)) { @@ -797,14 +804,7 @@ } } - // it has a filterArea! Same as mask but easier, its a rectangle - if (hitTest && displayObject.filterArea) - { - if (!displayObject.filterArea.contains(point.x, point.y)) - { - hitTest = false; - } - } + let keepHitTestingAfterChildren = hitTest; // ** FREE TIP **! If an object is not interactive or has no buttons in it // (such as a game scene!) set interactiveChildren to false for that displayObject. @@ -818,7 +818,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 @@ -838,15 +838,20 @@ // This means we no longer need to hit test anything else. We still need to run // through all objects, but we don't need to perform any hit tests. - // { - hitTest = false; - // } + keepHitTestingAfterChildren = false; + + if (child.interactive) + { + hitTest = false; + } // we can break now as we have hit an object. } } } + hitTest = keepHitTestingAfterChildren; + // no point running this if the item is not interactive or does not have an interactive parent. if (interactive) { @@ -868,14 +873,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 +886,175 @@ } /** - * 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].isNormalized) { - 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); + if (!displayObject.trackedPointers[id]) + { + displayObject.trackedPointers[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.trackedPointers[id].rightDown = true; + } + else + { + displayObject.trackedPointers[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.trackedPointers[id] !== undefined) + { + delete displayObject.trackedPointers[id]; + this.dispatchEvent(displayObject, 'pointercancel', interactionEvent); + + if (e.type === 'touchcancel' || e.pointerType === 'touch') + { + this.dispatchEvent(displayObject, 'touchcancel', interactionEvent); + } } } @@ -1159,42 +1066,94 @@ */ 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) { - if (hit) - { - this.dispatchEvent(displayObject, 'pointerup', this.eventData); + const e = interactionEvent.data.originalEvent; - if (displayObject._pointerDown) + const id = interactionEvent.data.identifier; + + const trackingData = displayObject.trackedPointers[id]; + + const isTouch = (e.type === 'touchend' || e.pointerType === 'touch'); + + const isMouse = (e.type.indexOf('mouse') === 0 || e.pointerType === 'mouse'); + + // 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) { - displayObject._pointerDown = false; - this.dispatchEvent(displayObject, 'pointertap', this.eventData); + 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); + } + // update the down state of the tracking data + if (trackingData) + { + if (isRightButton) + { + trackingData.rightDown = hit; + } + else + { + trackingData.leftDown = hit; + } } } - else if (displayObject._pointerDown) + + // Pointers and Touches, and Mouse + if (hit) { - displayObject._pointerDown = false; - this.dispatchEvent(displayObject, 'pointerupoutside', this.eventData); + this.dispatchEvent(displayObject, 'pointerup', interactionEvent); + if (isTouch) this.dispatchEvent(displayObject, 'touchend', interactionEvent); + + if (trackingData) + { + this.dispatchEvent(displayObject, 'pointertap', interactionEvent); + if (isTouch) + { + this.dispatchEvent(displayObject, 'tap', interactionEvent); + // touches are no longer over (if they ever were) when we get the touchend + // so we should ensure that we don't keep pretending that they are + trackingData.over = false; + } + } + } + else if (trackingData) + { + this.dispatchEvent(displayObject, 'pointerupoutside', interactionEvent); + if (isTouch) this.dispatchEvent(displayObject, 'touchendoutside', interactionEvent); + } + // Only remove the tracking data if there is no over/down state still associated with it + if (trackingData && trackingData.none) + { + delete displayObject.trackedPointers[id]; } } @@ -1202,39 +1161,78 @@ * 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 = null; + } - 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') + { + this.setCursorMode(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 (isMouse) { - 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 +1240,94 @@ * 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.setCursorMode(null); + } - 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'); + + let trackingData = displayObject.trackedPointers[id]; + + // if we just moused over the display object, then we need to track that state + if (hit && !trackingData) + { + trackingData = displayObject.trackedPointers[id] = new InteractionTrackingData(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); + } + } + + // only change the cursor if it has not already been changed (by something deeper in the + // display tree) + if (isMouse && this.cursor === null) + { + this.cursor = displayObject.cursor; } } - 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); + } + // if there is no mouse down information for the pointer, then it is safe to delete + if (trackingData.none) + { + delete displayObject.trackedPointers[id]; + } } } @@ -1287,253 +1335,154 @@ * 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 && pointerEvent.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; + + // mark the touch as normalized, just so that we know we did it + touch.isNormalized = true; + + normalizedEvents.push(touch); + } } - else if (this.normalizeMouseEvents) + // apparently PointerEvent subclasses MouseEvent, so yay + else if (event instanceof MouseEvent && (!this.supportsPointerEvents || !(event instanceof window.PointerEvent))) { if (typeof event.isPrimary === 'undefined') event.isPrimary = true; if (typeof event.width === 'undefined') event.width = 1; @@ -1541,10 +1490,21 @@ 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; + + // mark the mouse event as normalized, just so that we know we did it + event.isNormalized = true; + + normalizedEvents.push(event); } + else + { + normalizedEvents.push(event); + } + + return normalizedEvents; } /** @@ -1563,30 +1523,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 +1542,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..6a1f69e --- /dev/null +++ b/src/interaction/InteractionTrackingData.js @@ -0,0 +1,147 @@ +/** + * 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 inactive (not over or down)? + * + * @member {number} + * @memberof PIXI.interaction.InteractionTrackingData# + */ + get none() + { + return this._flags === this.constructor.FLAGS.NONE; + } + + /** + * 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..d252fba 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 * ); */ @@ -38,68 +38,56 @@ hitArea: null, /** - * If enabled, the mouse cursor will change when hovered over the displayObject if it is interactive + * If enabled, the mouse cursor use the pointer behavior when hovered over the displayObject if it is interactive + * Setting this changes the 'cursor' property to `'pointer'`. * - * @inner {boolean} + * @member {boolean} + * @memberof PIXI.interaction.interactiveTarget# */ - buttonMode: false, + get buttonMode() + { + return this.cursor === 'pointer'; + }, + set buttonMode(value) + { + if (value) + { + this.cursor = 'pointer'; + } + else if (this.cursor === 'pointer') + { + this.cursor = null; + } + }, /** - * If buttonMode is enabled, this defines what CSS cursor property is used when the mouse cursor - * is hovered over the displayObject + * This defines what cursor mode is used when the mouse cursor + * is hovered over the displayObject. * * @see https://developer.mozilla.org/en/docs/Web/CSS/cursor * * @inner {string} */ - defaultCursor: 'pointer', - - // some internal checks.. - /** - * Internal check to detect if the mouse cursor is hovered over the displayObject - * - * @inner {boolean} - * @private - */ - _over: false, + cursor: null, /** - * Internal check to detect if the left mouse button is pressed on the displayObject + * Internal set of all active pointers, by identifier * - * @inner {boolean} + * @member {Map} + * @memberof PIXI.interaction.interactiveTarget# * @private */ - _isLeftDown: false, + get trackedPointers() + { + if (this._trackedPointers === undefined) this._trackedPointers = {}; + + return this._trackedPointers; + }, /** - * Internal check to detect if the right mouse button is pressed on the displayObject + * Map of all tracked pointers, by identifier. Use trackedPointers to access. * - * @inner {boolean} - * @private + * @private {Map} */ - _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, + _trackedPointers: undefined, }; diff --git a/src/loaders/bitmapFontParser.js b/src/loaders/bitmapFontParser.js index cc2618c..f4cf996 100644 --- a/src/loaders/bitmapFontParser.js +++ b/src/loaders/bitmapFontParser.js @@ -1,63 +1,19 @@ import * as path from 'path'; -import { Rectangle, Texture, utils } from '../core'; +import { utils } from '../core'; import { Resource } from 'resource-loader'; import { BitmapText } from '../extras'; +/** + * Register a BitmapText font from loader resource. + * + * @function parseBitmapFontData + * @memberof PIXI.loaders + * @param {PIXI.loaders.Resource} resource - Loader resource. + * @param {PIXI.Texture} texture - Reference to texture. + */ export function parse(resource, texture) { - const data = {}; - const info = resource.data.getElementsByTagName('info')[0]; - const common = resource.data.getElementsByTagName('common')[0]; - - data.font = info.getAttribute('face'); - data.size = parseInt(info.getAttribute('size'), 10); - data.lineHeight = parseInt(common.getAttribute('lineHeight'), 10); - data.chars = {}; - - // parse letters - const letters = resource.data.getElementsByTagName('char'); - - for (let i = 0; i < letters.length; i++) - { - const charCode = parseInt(letters[i].getAttribute('id'), 10); - - const textureRect = new Rectangle( - parseInt(letters[i].getAttribute('x'), 10) + texture.frame.x, - parseInt(letters[i].getAttribute('y'), 10) + texture.frame.y, - parseInt(letters[i].getAttribute('width'), 10), - parseInt(letters[i].getAttribute('height'), 10) - ); - - data.chars[charCode] = { - xOffset: parseInt(letters[i].getAttribute('xoffset'), 10), - yOffset: parseInt(letters[i].getAttribute('yoffset'), 10), - xAdvance: parseInt(letters[i].getAttribute('xadvance'), 10), - kerning: {}, - texture: new Texture(texture.baseTexture, textureRect), - - }; - } - - // parse kernings - const kernings = resource.data.getElementsByTagName('kerning'); - - for (let i = 0; i < kernings.length; i++) - { - const first = parseInt(kernings[i].getAttribute('first'), 10); - const second = parseInt(kernings[i].getAttribute('second'), 10); - const amount = parseInt(kernings[i].getAttribute('amount'), 10); - - if (data.chars[second]) - { - data.chars[second].kerning[first] = amount; - } - } - - resource.bitmapFont = data; - - // I'm leaving this as a temporary fix so we can test the bitmap fonts in v3 - // but it's very likely to change - BitmapText.fonts[data.font] = data; + resource.bitmapFont = BitmapText.registerFont(resource.data, texture); } export default function () diff --git a/src/loaders/index.js b/src/loaders/index.js index 8942436..5189793 100644 --- a/src/loaders/index.js +++ b/src/loaders/index.js @@ -5,4 +5,11 @@ export { default as bitmapFontParser, parse as parseBitmapFontData } from './bitmapFontParser'; export { default as spritesheetParser } from './spritesheetParser'; export { default as textureParser } from './textureParser'; + +/** + * Reference to **resource-loader**'s Resource class. + * See https://github.com/englercj/resource-loader + * @class Resource + * @memberof PIXI.loaders + */ export { Resource } from 'resource-loader'; diff --git a/src/loaders/loader.js b/src/loaders/loader.js index 0a670cb..767a411 100644 --- a/src/loaders/loader.js +++ b/src/loaders/loader.js @@ -7,20 +7,47 @@ /** * - * The new loader, extends Resource Loader by Chad Engler : https://github.com/englercj/resource-loader + * The new loader, extends Resource Loader by Chad Engler: https://github.com/englercj/resource-loader * * ```js - * let loader = PIXI.loader; // pixi exposes a premade instance for you to use. + * const loader = PIXI.loader; // pixi exposes a premade instance for you to use. * //or - * let loader = new PIXI.loaders.Loader(); // you can also create your own if you want + * const loader = new PIXI.loaders.Loader(); // you can also create your own if you want * - * loader.add('bunny', 'data/bunny.png'); - * loader.add('spaceship', 'assets/spritesheet.json'); + * const sprites = {}; + * + * // Chainable `add` to enqueue a resource + * loader.add('bunny', 'data/bunny.png') + * .add('spaceship', 'assets/spritesheet.json'); * loader.add('scoreFont', 'assets/score.fnt'); * - * loader.once('complete',onAssetsLoaded); + * // Chainable `pre` to add a middleware that runs for each resource, *before* loading that resource. + * // This is useful to implement custom caching modules (using filesystem, indexeddb, memory, etc). + * loader.pre(cachingMiddleware); * - * loader.load(); + * // Chainable `use` to add a middleware that runs for each resource, *after* loading that resource. + * // This is useful to implement custom parsing modules (like spritesheet parsers, spine parser, etc). + * loader.use(parsingMiddleware); + * + * // The `load` method loads the queue of resources, and calls the passed in callback called once all + * // resources have loaded. + * loader.load((loader, resources) => { + * // resources is an object where the key is the name of the resource loaded and the value is the resource object. + * // They have a couple default properties: + * // - `url`: The URL that the resource was loaded from + * // - `error`: The error that happened when trying to load (if any) + * // - `data`: The raw data that was loaded + * // also may contain other properties based on the middleware that runs. + * sprites.bunny = new PIXI.TilingSprite(resources.bunny.texture); + * sprites.spaceship = new PIXI.TilingSprite(resources.spaceship.texture); + * sprites.scoreFont = new PIXI.TilingSprite(resources.scoreFont.texture); + * }); + * + * // throughout the process multiple signals can be dispatched. + * loader.onProgress.add(() => {}); // called once per loaded/errored file + * loader.onError.add(() => {}); // called once per errored file + * loader.onLoad.add(() => {}); // called once per loaded file + * loader.onComplete.add(() => {}); // called once when the queued resources all load. * ``` * * @see https://github.com/englercj/resource-loader diff --git a/src/loaders/spritesheetParser.js b/src/loaders/spritesheetParser.js index 7570332..e57dfb2 100644 --- a/src/loaders/spritesheetParser.js +++ b/src/loaders/spritesheetParser.js @@ -1,8 +1,6 @@ import { Resource } from 'resource-loader'; import path from 'path'; -import * as core from '../core'; - -const BATCH_SIZE = 1000; +import { Spritesheet } from '../core'; export default function () { @@ -43,134 +41,18 @@ // load the image for this sheet this.add(imageResourceName, resourcePath, loadOptions, function onImageLoad(res) { - resource.textures = {}; + const spritesheet = new Spritesheet( + res.texture.baseTexture, + resource.data, + resource.url + ); - const frames = resource.data.frames; - const frameKeys = Object.keys(frames); - const baseTexture = res.texture.baseTexture; - const scale = resource.data.meta.scale; - - // Use a defaultValue of `null` to check if a url-based resolution is set - let resolution = core.utils.getResolutionOfUrl(resource.url, null); - - // No resolution found via URL - if (resolution === null) + spritesheet.parse(() => { - // Use the scale value or default to 1 - resolution = scale !== undefined ? scale : 1; - } - - // For non-1 resolutions, update baseTexture - if (resolution !== 1) - { - baseTexture.resolution = resolution; - baseTexture.update(); - } - - let batchIndex = 0; - - function processFrames(initialFrameIndex, maxFrames) - { - let frameIndex = initialFrameIndex; - - while (frameIndex - initialFrameIndex < maxFrames && frameIndex < frameKeys.length) - { - const i = frameKeys[frameIndex]; - const rect = frames[i].frame; - - if (rect) - { - let frame = null; - let trim = null; - const orig = new core.Rectangle( - 0, - 0, - frames[i].sourceSize.w / resolution, - frames[i].sourceSize.h / resolution - ); - - if (frames[i].rotated) - { - frame = new core.Rectangle( - rect.x / resolution, - rect.y / resolution, - rect.h / resolution, - rect.w / resolution - ); - } - else - { - frame = new core.Rectangle( - rect.x / resolution, - rect.y / resolution, - rect.w / resolution, - rect.h / resolution - ); - } - - // Check to see if the sprite is trimmed - if (frames[i].trimmed) - { - trim = new core.Rectangle( - frames[i].spriteSourceSize.x / resolution, - frames[i].spriteSourceSize.y / resolution, - rect.w / resolution, - rect.h / resolution - ); - } - - resource.textures[i] = new core.Texture( - baseTexture, - frame, - orig, - trim, - frames[i].rotated ? 2 : 0 - ); - - // lets also add the frame to pixi's global cache for fromFrame and fromImage functions - core.utils.TextureCache[i] = resource.textures[i]; - } - - frameIndex++; - } - } - - function shouldProcessNextBatch() - { - return batchIndex * BATCH_SIZE < frameKeys.length; - } - - function processNextBatch(done) - { - processFrames(batchIndex * BATCH_SIZE, BATCH_SIZE); - batchIndex++; - setTimeout(done, 0); - } - - function iteration() - { - processNextBatch(() => - { - if (shouldProcessNextBatch()) - { - iteration(); - } - else - { - next(); - } - }); - } - - if (frameKeys.length <= BATCH_SIZE) - { - processFrames(0, BATCH_SIZE); + resource.spritesheet = spritesheet; + resource.textures = spritesheet.textures; next(); - } - else - { - iteration(); - } + }); }); }; } diff --git a/src/loaders/textureParser.js b/src/loaders/textureParser.js index 5398a7f..480edc1 100644 --- a/src/loaders/textureParser.js +++ b/src/loaders/textureParser.js @@ -1,5 +1,5 @@ -import * as core from '../core'; import { Resource } from 'resource-loader'; +import Texture from '../core/textures/Texture'; export default function () { @@ -8,23 +8,12 @@ // create a new texture if the data is an Image object if (resource.data && resource.type === Resource.TYPE.IMAGE) { - const baseTexture = new core.BaseTexture(resource.data, null, core.utils.getResolutionOfUrl(resource.url)); - - baseTexture.imageUrl = resource.url; - resource.texture = new core.Texture(baseTexture); - - // lets also add the frame to pixi's global cache for fromFrame and fromImage fucntions - core.utils.BaseTextureCache[resource.name] = baseTexture; - core.utils.TextureCache[resource.name] = resource.texture; - - // also add references by url if they are different. - if (resource.name !== resource.url) - { - core.utils.BaseTextureCache[resource.url] = baseTexture; - core.utils.TextureCache[resource.url] = resource.texture; - } + resource.texture = Texture.fromLoader( + resource.data, + resource.url, + resource.name + ); } - next(); }; } diff --git a/test/core/Container.js b/test/core/Container.js index 8a10b40..d11be33 100644 --- a/test/core/Container.js +++ b/test/core/Container.js @@ -1,5 +1,33 @@ 'use strict'; +function testAddChild(fn) +{ + fn(function (container, obj) + { + container.addChild(obj); + }); + fn(function (container, obj) + { + container.addChildAt(obj); + }); +} + +function testRemoveChild(fn) +{ + fn(function (container, obj) + { + container.removeChild(obj); + }); + fn(function (container, obj) + { + container.removeChildAt(container.children.indexOf(obj)); + }); + fn(function (container, obj) + { + container.removeChildren(container.children.indexOf(obj), container.children.indexOf(obj) + 1); + }); +} + describe('PIXI.Container', function () { describe('parent', function () @@ -70,6 +98,49 @@ expect(spy).to.have.been.called; expect(spy).to.have.been.calledWith(0); }); + + it('should flag child transform and container bounds for recalculation', testAddChild(function (mockAddChild) + { + const container = new PIXI.Container(); + const child = new PIXI.Container(); + + container.getBounds(); + child.getBounds(); + + const boundsID = container._boundsID; + const childParentID = child.transform._parentID; + + mockAddChild(container, child); + + expect(boundsID).to.not.be.equals(container._boundsID); + expect(childParentID).to.not.be.equals(child.transform._parentID); + })); + + it('should recalculate added child correctly', testAddChild(function (mockAddChild) + { + const parent = new PIXI.Container(); + const container = new PIXI.Container(); + const graphics = new PIXI.Graphics(); + + parent.addChild(container); + + graphics.drawRect(0, 0, 10, 10); + container.position.set(100, 200); + container.updateTransform(); + + graphics.getBounds(); + // Oops, that can happen sometimes! + graphics.transform._parentID = container.transform._worldID + 1; + + mockAddChild(container, graphics); + + const bounds = graphics.getBounds(); + + expect(bounds.x).to.be.equal(100); + expect(bounds.y).to.be.equal(200); + expect(bounds.width).to.be.equal(10); + expect(bounds.height).to.be.equal(10); + })); }); describe('removeChildAt', function () @@ -202,6 +273,44 @@ expect(spy).to.have.been.called; expect(spy).to.have.been.calledWith(0); }); + + it('should flag transform for recalculation', testRemoveChild(function (mockRemoveChild) + { + const container = new PIXI.Container(); + const child = new PIXI.Container(); + + container.addChild(child); + container.getBounds(); + + const childParentID = child.transform._parentID; + const boundsID = container._boundsID; + + mockRemoveChild(container, child); + + expect(childParentID).to.not.be.equals(child.transform._parentID); + expect(boundsID).to.not.be.equals(container._boundsID); + })); + + it('should recalculate removed child correctly', testRemoveChild(function (mockRemoveChild) + { + const parent = new PIXI.Container(); + const container = new PIXI.Container(); + const graphics = new PIXI.Graphics(); + + parent.addChild(container); + + graphics.drawRect(0, 0, 10, 10); + container.position.set(100, 200); + container.addChild(graphics); + graphics.getBounds(); + + mockRemoveChild(container, graphics); + + const bounds = graphics.getBounds(); + + expect(bounds.x).to.be.equal(0); + expect(bounds.y).to.be.equal(0); + })); }); describe('getChildIndex', function () diff --git a/test/core/Graphics.js b/test/core/Graphics.js index 98c7584..11e47a0 100644 --- a/test/core/Graphics.js +++ b/test/core/Graphics.js @@ -1,5 +1,8 @@ 'use strict'; +const MockPointer = require('../interaction/MockPointer'); +const withGL = require('../withGL'); + describe('PIXI.Graphics', function () { describe('constructor', function () @@ -218,4 +221,153 @@ expect(spy).to.have.been.calledOnce; }); }); + + describe('mask', function () + { + it('should trigger interaction callback when no mask present', function () + { + const stage = new PIXI.Container(); + const pointer = new MockPointer(stage); + const graphics = new PIXI.Graphics(); + const mask = new PIXI.Graphics(); + const spy = sinon.spy(); + + graphics.interactive = true; + graphics.beginFill(0xFF0000); + graphics.drawRect(0, 0, 50, 50); + graphics.on('click', spy); + stage.addChild(graphics); + mask.beginFill(); + mask.drawRect(0, 0, 50, 50); + graphics.mask = mask; + + pointer.click(10, 10); + + expect(spy).to.have.been.calledOnce; + }); + it('should trigger interaction callback when mask uses beginFill', function () + { + const stage = new PIXI.Container(); + const pointer = new MockPointer(stage); + const graphics = new PIXI.Graphics(); + const mask = new PIXI.Graphics(); + const spy = sinon.spy(); + + graphics.interactive = true; + graphics.beginFill(0xFF0000); + graphics.drawRect(0, 0, 50, 50); + graphics.on('click', spy); + stage.addChild(graphics); + mask.beginFill(); + mask.drawRect(0, 0, 50, 50); + graphics.mask = mask; + + pointer.click(10, 10); + + expect(spy).to.have.been.calledOnce; + }); + + it('should not trigger interaction callback when mask doesn\'t use beginFill', function () + { + const stage = new PIXI.Container(); + const pointer = new MockPointer(stage); + const graphics = new PIXI.Graphics(); + const mask = new PIXI.Graphics(); + const spy = sinon.spy(); + + graphics.interactive = true; + graphics.beginFill(0xFF0000); + graphics.drawRect(0, 0, 50, 50); + graphics.on('click', spy); + stage.addChild(graphics); + mask.drawRect(0, 0, 50, 50); + graphics.mask = mask; + + pointer.click(10, 10); + + expect(spy).to.have.not.been.called; + }); + + it('should trigger interaction callback when mask doesn\'t use beginFill but hitArea is defined', function () + { + const stage = new PIXI.Container(); + const pointer = new MockPointer(stage); + const graphics = new PIXI.Graphics(); + const mask = new PIXI.Graphics(); + const spy = sinon.spy(); + + graphics.interactive = true; + graphics.beginFill(0xFF0000); + graphics.hitArea = new PIXI.Rectangle(0, 0, 50, 50); + graphics.drawRect(0, 0, 50, 50); + graphics.on('click', spy); + stage.addChild(graphics); + mask.drawRect(0, 0, 50, 50); + graphics.mask = mask; + + pointer.click(10, 10); + + expect(spy).to.have.been.calledOnce; + }); + + it('should trigger interaction callback when mask is a sprite', function () + { + const stage = new PIXI.Container(); + const pointer = new MockPointer(stage); + const graphics = new PIXI.Graphics(); + const mask = new PIXI.Graphics(); + const spy = sinon.spy(); + + graphics.interactive = true; + graphics.beginFill(0xFF0000); + graphics.drawRect(0, 0, 50, 50); + graphics.on('click', spy); + stage.addChild(graphics); + mask.drawRect(0, 0, 50, 50); + graphics.mask = new PIXI.Sprite(mask.generateCanvasTexture()); + + pointer.click(10, 10); + + expect(spy).to.have.been.calledOnce; + }); + + it('should calculate tint, alpha and blendMode of fastRect correctly', withGL(function () + { + const renderer = new PIXI.WebGLRenderer(200, 200, {}); + + try + { + const graphics = new PIXI.Graphics(); + + graphics.beginFill(0x102030, 0.6); + graphics.drawRect(2, 3, 100, 100); + graphics.endFill(); + graphics.tint = 0x101010; + graphics.blendMode = 2; + graphics.alpha = 0.3; + + renderer.render(graphics); + + expect(graphics.isFastRect()).to.be.true; + + const sprite = graphics._spriteRect; + + expect(sprite).to.not.be.equals(null); + expect(sprite.worldAlpha).to.equals(0.18); + expect(sprite.blendMode).to.equals(2); + expect(sprite.tint).to.equals(0x010203); + + const bounds = sprite.getBounds(); + + expect(bounds.x).to.equals(2); + expect(bounds.y).to.equals(3); + expect(bounds.width).to.equals(100); + expect(bounds.height).to.equals(100); + } + finally + { + renderer.destroy(); + } + })); + }); }); diff --git a/package.json b/package.json index 6a366ba..71ade15 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pixi.js", - "version": "4.3.4", + "version": "4.3.5", "description": "Pixi.js is a fast lightweight 2D library that works across all devices.", "author": "Mat Groves", "contributors": [ @@ -61,7 +61,7 @@ "ismobilejs": "^0.4.0", "object-assign": "^4.0.1", "pixi-gl-core": "^1.0.3", - "resource-loader": "^2.0.4" + "resource-loader": "^2.0.6" }, "devDependencies": { "babel-cli": "^6.18.0", @@ -70,9 +70,9 @@ "babel-preset-es2015": "^6.14.0", "babelify": "^7.3.0", "del": "^2.2.0", - "electron-prebuilt": "^1.3.2", + "electron": "^1.4.15", "eslint": "^3.5.0", - "floss": "^1.2.0", + "floss": "^2.0.1", "gh-pages": "^0.11.0", "jaguarjs-jsdoc": "^1.0.1", "js-md5": "^0.4.1", diff --git a/src/accessibility/AccessibilityManager.js b/src/accessibility/AccessibilityManager.js index df2716a..f16bcb1 100644 --- a/src/accessibility/AccessibilityManager.js +++ b/src/accessibility/AccessibilityManager.js @@ -3,7 +3,7 @@ import accessibleTarget from './accessibleTarget'; // add some extra variables to the container.. -Object.assign( +core.utils.mixins.delayMixin( core.DisplayObject.prototype, accessibleTarget ); diff --git a/src/core/Application.js b/src/core/Application.js index ea0f058..5340578 100644 --- a/src/core/Application.js +++ b/src/core/Application.js @@ -1,6 +1,6 @@ import { autoDetectRenderer } from './autoDetectRenderer'; import Container from './display/Container'; -import Ticker from './ticker/Ticker'; +import { shared, Ticker } from './ticker'; /** * Convenience class to create a new PIXI application. @@ -35,9 +35,9 @@ * @param {boolean} [noWebGL=false] - prevents selection of WebGL renderer, even if such is present * @param {boolean} [options.legacy=false] - If true Pixi will aim to ensure compatibility * with older / less advanced devices. If you experience unexplained flickering try setting this to true. - + * @param {boolean} [useSharedTicker=false] - `true` to use PIXI.ticker.shared, `false` to create new ticker. */ - constructor(width, height, options, noWebGL) + constructor(width, height, options, noWebGL, useSharedTicker = false) { /** * WebGL renderer if available, otherwise CanvasRenderer @@ -46,23 +46,46 @@ this.renderer = autoDetectRenderer(width, height, options, noWebGL); /** - * The root display container that's renderered. + * The root display container that's rendered. * @member {PIXI.Container} */ this.stage = new Container(); /** + * Internal reference to the ticker + * @member {PIXI.ticker.Ticker} + * @private + */ + this._ticker = null; + + /** * Ticker for doing render updates. * @member {PIXI.ticker.Ticker} + * @default PIXI.ticker.shared */ - this.ticker = new Ticker(); - - this.ticker.add(this.render, this); + this.ticker = useSharedTicker ? shared : new Ticker(); // Start the rendering this.start(); } + set ticker(ticker) // eslint-disable-line require-jsdoc + { + if (this._ticker) + { + this._ticker.remove(this.render, this); + } + this._ticker = ticker; + if (ticker) + { + ticker.add(this.render, this); + } + } + get ticker() // eslint-disable-line require-jsdoc + { + return this._ticker; + } + /** * Render the current stage. */ @@ -76,7 +99,7 @@ */ stop() { - this.ticker.stop(); + this._ticker.stop(); } /** @@ -84,7 +107,7 @@ */ start() { - this.ticker.start(); + this._ticker.start(); } /** @@ -98,13 +121,22 @@ } /** + * Reference to the renderer's screen rectangle. Its safe to use as filterArea or hitArea for whole screen + * @member {PIXI.Rectangle} + * @readonly + */ + get screen() + { + return this.renderer.screen; + } + + /** * Destroy and don't use after this. * @param {Boolean} [removeView=false] Automatically remove canvas from DOM. */ destroy(removeView) { this.stop(); - this.ticker.remove(this.render, this); this.ticker = null; this.stage.destroy(); diff --git a/src/core/Shader.js b/src/core/Shader.js new file mode 100644 index 0000000..78b17e6 --- /dev/null +++ b/src/core/Shader.js @@ -0,0 +1,46 @@ +import { GLShader } from 'pixi-gl-core'; +import settings from './settings'; + +function checkPrecision(src, def) +{ + if (src instanceof Array) + { + if (src[0].substring(0, 9) !== 'precision') + { + const copy = src.slice(0); + + copy.unshift(`precision ${def} float;`); + + return copy; + } + } + else if (src.substring(0, 9) !== 'precision') + { + return `precision ${def} float;\n${src}`; + } + + return src; +} + +/** + * Wrapper class, webGL Shader for Pixi. + * Adds precision string if vertexSrc or fragmentSrc have no mention of it. + * + * @class + * @extends GLShader + * @memberof PIXI + */ +export default class Shader extends GLShader +{ + /** + * + * @param {WebGLRenderingContext} gl - The current WebGL rendering context + * @param {string|string[]} vertexSrc - The vertex shader source as an array of strings. + * @param {string|string[]} fragmentSrc - The fragment shader source as an array of strings. + */ + constructor(gl, vertexSrc, fragmentSrc) + { + super(gl, checkPrecision(vertexSrc, settings.PRECISION_VERTEX), + checkPrecision(fragmentSrc, settings.PRECISION_FRAGMENT)); + } +} diff --git a/src/core/const.js b/src/core/const.js index ef6f662..3de0efe 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -188,7 +188,7 @@ * The gc modes that are supported by pixi. * * The {@link PIXI.settings.GC_MODE} Garbage Collection mode for pixi textures is AUTO - * If set to GC_MODE, the renderer will occasianally check textures usage. If they are not + * If set to GC_MODE, the renderer will occasionally check textures usage. If they are not * used for a specified period of time they will be removed from the GPU. They will of course * be uploaded again when they are required. This is a silent behind the scenes process that * should ensure that the GPU does not get filled up. diff --git a/src/core/display/Container.js b/src/core/display/Container.js index ed8cd5f..17fde8d 100644 --- a/src/core/display/Container.js +++ b/src/core/display/Container.js @@ -73,11 +73,12 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.push(child); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -108,11 +109,12 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.splice(index, 0, child); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -225,10 +227,11 @@ if (index === -1) return null; child.parent = null; + // ensure child transform will be recalculated + child.transform._parentID = -1; removeItems(this.children, index, 1); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -249,11 +252,12 @@ { const child = this.getChildAt(index); + // ensure child transform will be recalculated.. child.parent = null; + child.transform._parentID = -1; removeItems(this.children, index, 1); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -284,13 +288,12 @@ for (let i = 0; i < removed.length; ++i) { removed[i].parent = null; + if (removed[i].transform) + { + removed[i].transform._parentID = -1; + } } - // ensure a transform will be recalculated.. - if (this.transform) - { - this.transform._parentID = -1; - } this._boundsID++; this.onChildrenChange(beginIndex); diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index f37f2f5..be4c4fb 100644 --- a/src/core/graphics/Graphics.js +++ b/src/core/graphics/Graphics.js @@ -28,8 +28,9 @@ { /** * + * @param {boolean} [nativeLines=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP */ - constructor() + constructor(nativeLines = false) { super(); @@ -50,6 +51,13 @@ this.lineWidth = 0; /** + * If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * + * @member {boolean} + */ + this.nativeLines = nativeLines; + + /** * The color of any lines drawn. * * @member {string} @@ -757,28 +765,14 @@ if (!this._spriteRect) { - if (!Graphics._SPRITE_TEXTURE) - { - Graphics._SPRITE_TEXTURE = RenderTexture.create(10, 10); - - const canvas = document.createElement('canvas'); - - canvas.width = 10; - canvas.height = 10; - - const context = canvas.getContext('2d'); - - context.fillStyle = 'white'; - context.fillRect(0, 0, 10, 10); - - Graphics._SPRITE_TEXTURE = Texture.fromCanvas(canvas); - } - - this._spriteRect = new Sprite(Graphics._SPRITE_TEXTURE); + this._spriteRect = new Sprite(new Texture(Texture.WHITE)); } + + const sprite = this._spriteRect; + if (this.tint === 0xffffff) { - this._spriteRect.tint = this.graphicsData[0].fillColor; + sprite.tint = this.graphicsData[0].fillColor; } else { @@ -792,20 +786,21 @@ t1[1] *= t2[1]; t1[2] *= t2[2]; - this._spriteRect.tint = rgb2hex(t1); + sprite.tint = rgb2hex(t1); } - this._spriteRect.alpha = this.graphicsData[0].fillAlpha; - this._spriteRect.worldAlpha = this.worldAlpha * this._spriteRect.alpha; + sprite.alpha = this.graphicsData[0].fillAlpha; + sprite.worldAlpha = this.worldAlpha * sprite.alpha; + sprite.blendMode = this.blendMode; - Graphics._SPRITE_TEXTURE._frame.width = rect.width; - Graphics._SPRITE_TEXTURE._frame.height = rect.height; + sprite.texture._frame.width = rect.width; + sprite.texture._frame.height = rect.height; - this._spriteRect.transform.worldTransform = this.transform.worldTransform; + sprite.transform.worldTransform = this.transform.worldTransform; - this._spriteRect.anchor.set(-rect.x / rect.width, -rect.y / rect.height); - this._spriteRect._onAnchorUpdate(); + sprite.anchor.set(-rect.x / rect.width, -rect.y / rect.height); + sprite._onAnchorUpdate(); - this._spriteRect._renderWebGL(renderer); + sprite._renderWebGL(renderer); } /** @@ -1031,6 +1026,7 @@ this.fillColor, this.fillAlpha, this.filling, + this.nativeLines, shape ); diff --git a/src/core/graphics/GraphicsData.js b/src/core/graphics/GraphicsData.js index f5a87ae..b6f03c0 100644 --- a/src/core/graphics/GraphicsData.js +++ b/src/core/graphics/GraphicsData.js @@ -14,14 +14,19 @@ * @param {number} fillColor - the color of the fill * @param {number} fillAlpha - the alpha of the fill * @param {boolean} fill - whether or not the shape is filled with a colour + * @param {boolean} nativeLines - the method for drawing lines * @param {PIXI.Circle|PIXI.Rectangle|PIXI.Ellipse|PIXI.Polygon} shape - The shape object to draw. */ - constructor(lineWidth, lineColor, lineAlpha, fillColor, fillAlpha, fill, shape) + constructor(lineWidth, lineColor, lineAlpha, fillColor, fillAlpha, fill, nativeLines, shape) { /** * @member {number} the width of the line to draw */ this.lineWidth = lineWidth; + /** + * @member {boolean} if true the liens will be draw using LINES instead of TRIANGLE_STRIP + */ + this.nativeLines = nativeLines; /** * @member {number} the color of the line to draw @@ -85,6 +90,7 @@ this.fillColor, this.fillAlpha, this.fill, + this.nativeLines, this.shape ); } diff --git a/src/core/graphics/webgl/GraphicsRenderer.js b/src/core/graphics/webgl/GraphicsRenderer.js index 14083fe..67f5e92 100644 --- a/src/core/graphics/webgl/GraphicsRenderer.js +++ b/src/core/graphics/webgl/GraphicsRenderer.js @@ -102,7 +102,15 @@ shaderTemp.uniforms.alpha = graphics.worldAlpha; renderer.bindVao(webGLData.vao); - webGLData.vao.draw(gl.TRIANGLE_STRIP, webGLData.indices.length); + + if (graphics.nativeLines) + { + gl.drawArrays(gl.LINES, 0, webGLData.points.length / 6); + } + else + { + webGLData.vao.draw(gl.TRIANGLE_STRIP, webGLData.indices.length); + } } } diff --git a/src/core/graphics/webgl/utils/buildLine.js b/src/core/graphics/webgl/utils/buildLine.js index ecf43f7..4d6862e 100644 --- a/src/core/graphics/webgl/utils/buildLine.js +++ b/src/core/graphics/webgl/utils/buildLine.js @@ -11,7 +11,29 @@ * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties * @param {object} webGLData - an object containing all the webGL-specific information to create this shape */ -export default function buildLine(graphicsData, webGLData) +export default function (graphicsData, webGLData) +{ + if (graphicsData.nativeLines) + { + buildNativeLine(graphicsData, webGLData); + } + else + { + buildLine(graphicsData, webGLData); + } +} + +/** + * Builds a line to draw using the poligon method. + * + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @private + * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties + * @param {object} webGLData - an object containing all the webGL-specific information to create this shape + */ +function buildLine(graphicsData, webGLData) { // TODO OPTIMISE! let points = graphicsData.points; @@ -224,3 +246,46 @@ indices.push(indexStart - 1); } + +/** + * Builds a line to draw using the gl.drawArrays(gl.LINES) method + * + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @private + * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties + * @param {object} webGLData - an object containing all the webGL-specific information to create this shape + */ +function buildNativeLine(graphicsData, webGLData) +{ + let i = 0; + const points = graphicsData.points; + + if (points.length === 0) return; + + const verts = webGLData.points; + const length = points.length / 2; + + // sort color + const color = hex2rgb(graphicsData.lineColor); + const alpha = graphicsData.lineAlpha; + const r = color[0] * alpha; + const g = color[1] * alpha; + const b = color[2] * alpha; + + for (i = 1; i < length; i++) + { + const p1x = points[(i - 1) * 2]; + const p1y = points[((i - 1) * 2) + 1]; + + const p2x = points[i * 2]; + const p2y = points[(i * 2) + 1]; + + verts.push(p1x, p1y); + verts.push(r, g, b, alpha); + + verts.push(p2x, p2y); + verts.push(r, g, b, alpha); + } +} diff --git a/src/core/graphics/webgl/utils/buildRoundedRectangle.js b/src/core/graphics/webgl/utils/buildRoundedRectangle.js index 9abc807..a6c8461 100644 --- a/src/core/graphics/webgl/utils/buildRoundedRectangle.js +++ b/src/core/graphics/webgl/utils/buildRoundedRectangle.js @@ -77,6 +77,26 @@ } /** + * Calculate a single point for a quadratic bezier curve. + * Utility function used by quadraticBezierCurve. + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @private + * @param {number} n1 - first number + * @param {number} n2 - second number + * @param {number} perc - percentage + * @return {number} the result + * + */ +function getPt(n1, n2, perc) +{ + const diff = n2 - n1; + + return n1 + (diff * perc); +} + +/** * Calculate the points for a quadratic bezier curve. (helper function..) * Based on: https://stackoverflow.com/questions/785097/how-do-i-implement-a-bezier-curve-in-c * @@ -105,13 +125,6 @@ let x = 0; let y = 0; - function getPt(n1, n2, perc) - { - const diff = n2 - n1; - - return n1 + (diff * perc); - } - for (let i = 0, j = 0; i <= n; ++i) { j = i / n; diff --git a/src/core/index.js b/src/core/index.js index f7a817c..4f414e8 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -30,6 +30,7 @@ export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; export { default as CanvasGraphicsRenderer } from './graphics/canvas/CanvasGraphicsRenderer'; +export { default as Spritesheet } from './textures/Spritesheet'; export { default as Texture } from './textures/Texture'; export { default as BaseTexture } from './textures/BaseTexture'; export { default as RenderTexture } from './textures/RenderTexture'; diff --git a/src/core/math/Matrix.js b/src/core/math/Matrix.js index 0b77477..3679860 100644 --- a/src/core/math/Matrix.js +++ b/src/core/math/Matrix.js @@ -13,45 +13,50 @@ export default class Matrix { /** - * + * @param {number} [a=1] - x scale + * @param {number} [b=0] - y skew + * @param {number} [c=0] - x skew + * @param {number} [d=1] - y scale + * @param {number} [tx=0] - x translation + * @param {number} [ty=0] - y translation */ - constructor() + constructor(a = 1, b = 0, c = 0, d = 1, tx = 0, ty = 0) { /** * @member {number} * @default 1 */ - this.a = 1; + this.a = a; /** * @member {number} * @default 0 */ - this.b = 0; + this.b = b; /** * @member {number} * @default 0 */ - this.c = 0; + this.c = c; /** * @member {number} * @default 1 */ - this.d = 1; + this.d = d; /** * @member {number} * @default 0 */ - this.tx = 0; + this.tx = tx; /** * @member {number} * @default 0 */ - this.ty = 0; + this.ty = ty; this.array = null; } diff --git a/src/core/renderers/SystemRenderer.js b/src/core/renderers/SystemRenderer.js index fa116a6..a24c775 100644 --- a/src/core/renderers/SystemRenderer.js +++ b/src/core/renderers/SystemRenderer.js @@ -1,5 +1,5 @@ import { sayHello, hex2string, hex2rgb } from '../utils'; -import { Matrix } from '../math'; +import { Matrix, Rectangle } from '../math'; import { RENDERER_TYPE } from '../const'; import settings from '../settings'; import Container from '../display/Container'; @@ -21,8 +21,8 @@ { /** * @param {string} system - The name of the system this renderer is for. - * @param {number} [width=800] - the width of the canvas view - * @param {number} [height=600] - the height of the canvas view + * @param {number} [screenWidth=800] - the width of the screen + * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false @@ -37,7 +37,7 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(system, width, height, options) + constructor(system, screenWidth, screenHeight, options) { super(); @@ -69,20 +69,13 @@ this.type = RENDERER_TYPE.UNKNOWN; /** - * The width of the canvas view + * Measurements of the screen. (0, 0, screenWidth, screenHeight) * - * @member {number} - * @default 800 - */ - this.width = width || 800; - - /** - * The height of the canvas view + * Its safe to use as filterArea or hitArea for whole stage * - * @member {number} - * @default 600 + * @member {PIXI.Rectangle} */ - this.height = height || 600; + this.screen = new Rectangle(0, 0, screenWidth || 800, screenHeight || 600); /** * The canvas element that everything is drawn to @@ -107,7 +100,7 @@ this.transparent = options.transparent; /** - * Whether the render view should be resized automatically + * Whether css dimensions of canvas view should be resized to screen dimensions automatically * * @member {boolean} */ @@ -192,23 +185,48 @@ } /** - * Resizes the canvas view to the specified width and height + * Same as view.width, actual number of pixels in the canvas by horizontal * - * @param {number} width - the new width of the canvas view - * @param {number} height - the new height of the canvas view + * @member {number} + * @readonly + * @default 800 */ - resize(width, height) + get width() { - this.width = width * this.resolution; - this.height = height * this.resolution; + return this.view.width; + } - this.view.width = this.width; - this.view.height = this.height; + /** + * Same as view.height, actual number of pixels in the canvas by vertical + * + * @member {number} + * @readonly + * @default 600 + */ + get height() + { + return this.view.height; + } + + /** + * Resizes the screen and canvas to the specified width and height + * Canvas dimensions are multiplied by resolution + * + * @param {number} screenWidth - the new width of the screen + * @param {number} screenHeight - the new height of the screen + */ + resize(screenWidth, screenHeight) + { + this.screen.width = screenWidth; + this.screen.height = screenHeight; + + this.view.width = screenWidth * this.resolution; + this.view.height = screenHeight * this.resolution; if (this.autoResize) { - this.view.style.width = `${this.width / this.resolution}px`; - this.view.style.height = `${this.height / this.resolution}px`; + this.view.style.width = `${screenWidth}px`; + this.view.style.height = `${screenHeight}px`; } } @@ -249,11 +267,10 @@ this.type = RENDERER_TYPE.UNKNOWN; - this.width = 0; - this.height = 0; - this.view = null; + this.screen = null; + this.resolution = 0; this.transparent = false; diff --git a/src/core/renderers/canvas/CanvasRenderer.js b/src/core/renderers/canvas/CanvasRenderer.js index 1bd2d5e..e84e49c 100644 --- a/src/core/renderers/canvas/CanvasRenderer.js +++ b/src/core/renderers/canvas/CanvasRenderer.js @@ -18,8 +18,8 @@ export default class CanvasRenderer extends SystemRenderer { /** - * @param {number} [width=800] - the width of the canvas view - * @param {number} [height=600] - the height of the canvas view + * @param {number} [screenWidth=800] - the width of the screen + * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false @@ -34,9 +34,9 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(width, height, options = {}) + constructor(screenWidth, screenHeight, options = {}) { - super('Canvas', width, height, options); + super('Canvas', screenWidth, screenHeight, options); this.type = RENDERER_TYPE.CANVAS; @@ -96,7 +96,7 @@ this.context = null; this.renderingToScreen = false; - this.resize(width, height); + this.resize(screenWidth, screenHeight); } /** @@ -283,12 +283,12 @@ * * @extends PIXI.SystemRenderer#resize * - * @param {number} width - The new width of the canvas view - * @param {number} height - The new height of the canvas view + * @param {number} screenWidth - the new width of the screen + * @param {number} screenHeight - the new height of the screen */ - resize(width, height) + resize(screenWidth, screenHeight) { - super.resize(width, height); + super.resize(screenWidth, screenHeight); // reset the scale mode.. oddly this seems to be reset when the canvas is resized. // surely a browser bug?? Let pixi fix that for you.. diff --git a/src/core/renderers/webgl/TextureManager.js b/src/core/renderers/webgl/TextureManager.js index b685c1a..8227289 100644 --- a/src/core/renderers/webgl/TextureManager.js +++ b/src/core/renderers/webgl/TextureManager.js @@ -61,7 +61,7 @@ * Updates and/or Creates a WebGL texture for the renderer's context. * * @param {PIXI.BaseTexture|PIXI.Texture} texture - the texture to update - * @param {Number} location - the location the texture will be bound to. + * @param {number} location - the location the texture will be bound to. * @return {GLTexture} The gl texture. */ updateTexture(texture, location) diff --git a/src/core/renderers/webgl/WebGLRenderer.js b/src/core/renderers/webgl/WebGLRenderer.js index e4cd8a0..830b382 100644 --- a/src/core/renderers/webgl/WebGLRenderer.js +++ b/src/core/renderers/webgl/WebGLRenderer.js @@ -31,8 +31,8 @@ { /** * - * @param {number} [width=0] - the width of the canvas view - * @param {number} [height=0] - the height of the canvas view + * @param {number} [screenWidth=800] - the width of the screen + * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false @@ -53,9 +53,9 @@ * @param {boolean} [options.legacy=false] - If true Pixi will aim to ensure compatibility * with older / less advanced devices. If you experiance unexplained flickering try setting this to true. */ - constructor(width, height, options = {}) + constructor(screenWidth, screenHeight, options = {}) { - super('WebGL', width, height, options); + super('WebGL', screenWidth, screenHeight, options); this.legacy = !!options.legacy; @@ -245,7 +245,7 @@ glCore._testingContext = gl; // setup the width/height properties and gl viewport - this.resize(this.width, this.height); + this.resize(this.screen.width, this.screen.height); } /** @@ -343,16 +343,16 @@ /** * Resizes the webGL view to the specified width and height. * - * @param {number} width - the new width of the webGL view - * @param {number} height - the new height of the webGL view + * @param {number} screenWidth - the new width of the screen + * @param {number} screenHeight - the new height of the screen */ - resize(width, height) + resize(screenWidth, screenHeight) { // if(width * this.resolution === this.width && height * this.resolution === this.height)return; - SystemRenderer.prototype.resize.call(this, width, height); + SystemRenderer.prototype.resize.call(this, screenWidth, screenHeight); - this.rootRenderTarget.resize(width, height); + this.rootRenderTarget.resize(screenWidth, screenHeight); if (this._activeRenderTarget === this.rootRenderTarget) { @@ -396,6 +396,26 @@ } /** + * Erases the render texture and fills the drawing area with a colour + * + * @param {PIXI.RenderTexture} renderTexture - The render texture to clear + * @param {number} [clearColor] - The colour + * @return {PIXI.WebGLRenderer} Returns itself. + */ + clearRenderTexture(renderTexture, clearColor) + { + const baseTexture = renderTexture.baseTexture; + const renderTarget = baseTexture._glRenderTargets[this.CONTEXT_UID]; + + if (renderTarget) + { + renderTarget.clear(clearColor); + } + + return this; + } + + /** * Binds a render texture for rendering * * @param {PIXI.RenderTexture} renderTexture - The render texture to render @@ -474,9 +494,10 @@ * Changes the current GLShader to the one given in parameter * * @param {PIXI.glCore.Shader} shader - the new glShader + * @param {boolean} [autoProject=true] - Whether automatically set the projection matrix * @return {PIXI.WebGLRenderer} Returns itself. */ - _bindGLShader(shader) + _bindGLShader(shader, autoProject) { // TODO cache if (this._activeShader !== shader) @@ -484,8 +505,14 @@ this._activeShader = shader; shader.bind(); - // automatically set the projection matrix - shader.uniforms.projectionMatrix = this._activeRenderTarget.projectionMatrix.toArray(true); + // `autoProject` normally would be a default parameter set to true + // but because of how Babel transpiles default parameters + // it hinders the performance of this method. + if (autoProject !== false) + { + // automatically set the projection matrix + shader.uniforms.projectionMatrix = this._activeRenderTarget.projectionMatrix.toArray(true); + } } return this; diff --git a/src/core/renderers/webgl/WebGLState.js b/src/core/renderers/webgl/WebGLState.js index ba6ecc5..6c846d5 100755 --- a/src/core/renderers/webgl/WebGLState.js +++ b/src/core/renderers/webgl/WebGLState.js @@ -82,18 +82,20 @@ push() { // next state.. - let state = this.stack[++this.stackIndex]; + let state = this.stack[this.stackIndex]; if (!state) { state = this.stack[this.stackIndex] = new Uint8Array(16); } + ++this.stackIndex; + // copy state.. // set active state so we can force overrides of gl state for (let i = 0; i < this.activeState.length; i++) { - this.activeState[i] = state[i]; + state[i] = this.activeState[i]; } } diff --git a/src/core/renderers/webgl/filters/Filter.js b/src/core/renderers/webgl/filters/Filter.js index 2c3eaad..4eb6128 100644 --- a/src/core/renderers/webgl/filters/Filter.js +++ b/src/core/renderers/webgl/filters/Filter.js @@ -9,7 +9,7 @@ * @memberof PIXI * @extends PIXI.Shader */ -class Filter extends Shader +export default class Filter extends Shader { /** * @param {string} [vertexSrc] - The source of the vertex shader. @@ -56,15 +56,16 @@ * @param {PIXI.RenderTarget} input - The input render target. * @param {PIXI.RenderTarget} output - The target to output to. * @param {boolean} clear - Should the output be cleared before rendering to it + * @param {object} [currentState] - It's current state of filter. + * There are some useful properties in the currentState : + * target, filters, sourceFrame, destinationFrame, renderTarget, resolution */ - apply(filterManager, input, output, clear) + apply(filterManager, input, output, clear, currentState) // eslint-disable-line no-unused-vars { // do as you please! - filterManager.applyFilter(this, input, output, clear); + filterManager.applyFilter(this, input, output, clear, currentState); // or just do a regular render.. } } - -export default Filter; diff --git a/src/core/renderers/webgl/filters/extractUniformsFromSrc.js b/src/core/renderers/webgl/filters/extractUniformsFromSrc.js index d78a0b1..4893214 100644 --- a/src/core/renderers/webgl/filters/extractUniformsFromSrc.js +++ b/src/core/renderers/webgl/filters/extractUniformsFromSrc.js @@ -12,7 +12,7 @@ function extractUniformsFromString(string) { - const maskRegex = new RegExp('^(projectionMatrix|uSampler|filterArea)$'); + const maskRegex = new RegExp('^(projectionMatrix|uSampler|filterArea|filterClamp)$'); const uniforms = {}; let nameSplit; diff --git a/src/core/renderers/webgl/managers/FilterManager.js b/src/core/renderers/webgl/managers/FilterManager.js index 43fc869..6c7422d 100644 --- a/src/core/renderers/webgl/managers/FilterManager.js +++ b/src/core/renderers/webgl/managers/FilterManager.js @@ -2,6 +2,7 @@ import RenderTarget from '../utils/RenderTarget'; import Quad from '../utils/Quad'; import { Rectangle } from '../../../math'; +import Shader from '../../../Shader'; import * as filterTransforms from '../filters/filterTransforms'; import bitTwiddle from 'bit-twiddle'; @@ -22,8 +23,6 @@ this.filters = []; this.target = null; this.resolution = 1; - - this.firstRun = true; } } @@ -155,7 +154,7 @@ if (filters.length === 1) { - filters[0].apply(this, currentState.renderTarget, lastState.renderTarget, false); + filters[0].apply(this, currentState.renderTarget, lastState.renderTarget, false, currentState); this.freePotRenderTarget(currentState.renderTarget); } else @@ -177,7 +176,7 @@ for (i = 0; i < filters.length - 1; ++i) { - filters[i].apply(this, flip, flop, true); + filters[i].apply(this, flip, flop, true, currentState); const t = flip; @@ -185,7 +184,7 @@ flop = t; } - filters[i].apply(this, flip, lastState.renderTarget, false); + filters[i].apply(this, flip, lastState.renderTarget, false, currentState); this.freePotRenderTarget(flip); this.freePotRenderTarget(flop); @@ -212,16 +211,28 @@ const renderer = this.renderer; const gl = renderer.gl; - renderer.bindShader(filter, true); + let shader = filter.glShaders[renderer.CONTEXT_UID]; - // TODO theres a better way! - // quad can be a mesh and we then should be able to pull this off without accessing the shader directly - const shader = renderer.shaderManager.getGLShader(); - - // TODO change quad to mesh.. - if (!this.firstRun) + // cacheing.. + if (!shader) { - this.firstRun = true; + if (filter.glShaderKey) + { + shader = this.shaderCache[filter.glShaderKey]; + + if (!shader) + { + shader = new Shader(this.gl, filter.vertexSrc, filter.fragmentSrc); + + filter.glShaders[renderer.CONTEXT_UID] = this.shaderCache[filter.glShaderKey] = shader; + } + } + else + { + shader = filter.glShaders[renderer.CONTEXT_UID] = new Shader(this.gl, filter.vertexSrc, filter.fragmentSrc); + } + + // TODO - this only needs to be done once? renderer.bindVao(null); this.quad.initVao(shader); @@ -244,58 +255,146 @@ renderer.maskManager.pushScissorMask(null, renderer.maskManager.scissorData); } - // renderer._bindGLShader(shader); + renderer.bindShader(shader); - let currentState; - const uniforms = filter.uniforms; + // free unit 0 for us, doesn't matter what was there + // don't try to restore it, because syncUniforms can upload it to another slot + // and it'll be a problem + const tex = this.renderer.emptyTextures[0]; - if (uniforms.filterArea) - { - currentState = this.filterData.stack[this.filterData.index]; - - const filterArea = uniforms.filterArea; - - filterArea[0] = currentState.renderTarget.size.width; - filterArea[1] = currentState.renderTarget.size.height; - filterArea[2] = currentState.sourceFrame.x; - filterArea[3] = currentState.sourceFrame.y; - - filter.uniforms.filterArea = filterArea; - } - - // use this to clamp displaced texture coords so they belong to filterArea - // see displacementFilter fragment shader for an example - if (uniforms.filterClamp) - { - currentState = this.filterData.stack[this.filterData.index]; - - const filterClamp = uniforms.filterClamp; - - filterClamp[0] = 0; - filterClamp[1] = 0; - filterClamp[2] = (currentState.sourceFrame.width - 1) / currentState.renderTarget.size.width; - filterClamp[3] = (currentState.sourceFrame.height - 1) / currentState.renderTarget.size.height; - - filter.uniforms.filterClamp = filterClamp; - } - - renderer.shaderManager.setUniforms(filter.uniforms); + this.renderer.boundTextures[0] = tex; + // this syncs the pixi filters uniforms with glsl uniforms + this.syncUniforms(shader, filter); renderer.state.setBlendMode(filter.blendMode); - // temporary bypass cache.. - const tex = this.renderer.boundTextures[0]; - gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, input.texture.texture); this.quad.vao.draw(this.renderer.gl.TRIANGLES, 6, 0); - // restore cache. gl.bindTexture(gl.TEXTURE_2D, tex._glTextures[this.renderer.CONTEXT_UID].texture); } /** + * Uploads the uniforms of the filter. + * + * @param {GLShader} shader - The underlying gl shader. + * @param {PIXI.Filter} filter - The filter we are synchronizing. + */ + syncUniforms(shader, filter) + { + const uniformData = filter.uniformData; + const uniforms = filter.uniforms; + + // 0 is reserved for the pixi texture so we start at 1! + let textureCount = 1; + let currentState; + + // filterArea and filterClamp that are handled by FilterManager directly + // they must not appear in uniformData + + if (shader.uniforms.filterArea) + { + currentState = this.filterData.stack[this.filterData.index]; + + const filterArea = shader.uniforms.filterArea; + + filterArea[0] = currentState.renderTarget.size.width; + filterArea[1] = currentState.renderTarget.size.height; + filterArea[2] = currentState.sourceFrame.x; + filterArea[3] = currentState.sourceFrame.y; + + shader.uniforms.filterArea = filterArea; + } + + // use this to clamp displaced texture coords so they belong to filterArea + // see displacementFilter fragment shader for an example + if (shader.uniforms.filterClamp) + { + currentState = currentState || this.filterData.stack[this.filterData.index]; + + const filterClamp = shader.uniforms.filterClamp; + + filterClamp[0] = 0; + filterClamp[1] = 0; + filterClamp[2] = (currentState.sourceFrame.width - 1) / currentState.renderTarget.size.width; + filterClamp[3] = (currentState.sourceFrame.height - 1) / currentState.renderTarget.size.height; + + shader.uniforms.filterClamp = filterClamp; + } + + // TODO Cacheing layer.. + for (const i in uniformData) + { + if (uniformData[i].type === 'sampler2D' && uniforms[i] !== 0) + { + if (uniforms[i].baseTexture) + { + shader.uniforms[i] = this.renderer.bindTexture(uniforms[i].baseTexture, textureCount); + } + else + { + shader.uniforms[i] = textureCount; + + // TODO + // this is helpful as renderTargets can also be set. + // Although thinking about it, we could probably + // make the filter texture cache return a RenderTexture + // rather than a renderTarget + const gl = this.renderer.gl; + + this.renderer.boundTextures[textureCount] = this.renderer.emptyTextures[textureCount]; + gl.activeTexture(gl.TEXTURE0 + textureCount); + + uniforms[i].texture.bind(); + } + + textureCount++; + } + else if (uniformData[i].type === 'mat3') + { + // check if its pixi matrix.. + if (uniforms[i].a !== undefined) + { + shader.uniforms[i] = uniforms[i].toArray(true); + } + else + { + shader.uniforms[i] = uniforms[i]; + } + } + else if (uniformData[i].type === 'vec2') + { + // check if its a point.. + if (uniforms[i].x !== undefined) + { + const val = shader.uniforms[i] || new Float32Array(2); + + val[0] = uniforms[i].x; + val[1] = uniforms[i].y; + shader.uniforms[i] = val; + } + else + { + shader.uniforms[i] = uniforms[i]; + } + } + else if (uniformData[i].type === 'float') + { + if (shader.uniforms.data[i].value !== uniformData[i]) + { + shader.uniforms[i] = uniforms[i]; + } + } + else + { + shader.uniforms[i] = uniforms[i]; + } + } + } + + /** * Gets a render target from the pool, or creates a new one. * * @param {boolean} clear - Should we clear the render texture when we get it? @@ -442,7 +541,6 @@ renderTarget.resolution = resolution; renderTarget.defaultFrame.width = renderTarget.size.width = minWidth / resolution; renderTarget.defaultFrame.height = renderTarget.size.height = minHeight / resolution; - renderTarget.attachStencilBuffer(); return renderTarget; } diff --git a/src/core/settings.js b/src/core/settings.js index bb08cb0..f4dd087 100644 --- a/src/core/settings.js +++ b/src/core/settings.js @@ -77,11 +77,11 @@ * * @static * @memberof PIXI.settings - * @type {RegExp|string} + * @type {RegExp} * @example `@2x` - * @default /@(.+)x/ + * @default /@([0-9\.]+)x/ */ - RETINA_PREFIX: /@(.+)x/, + RETINA_PREFIX: /@([0-9\.]+)x/, /** * The default render options if none are supplied to {@link PIXI.WebGLRenderer} @@ -175,14 +175,24 @@ SCALE_MODE: 0, /** - * Default specify float precision in shaders. + * Default specify float precision in vertex shader. + * + * @static + * @memberof PIXI.settings + * @type {PIXI.PRECISION} + * @default PIXI.PRECISION.HIGH + */ + PRECISION_VERTEX: 'highp', + + /** + * Default specify float precision in fragment shader. * * @static * @memberof PIXI.settings * @type {PIXI.PRECISION} * @default PIXI.PRECISION.MEDIUM */ - PRECISION: 'mediump', + PRECISION_FRAGMENT: 'mediump', /** * Can we upload the same buffer in a single frame? diff --git a/src/core/sprites/Sprite.js b/src/core/sprites/Sprite.js index 28b0b18..121bf58 100644 --- a/src/core/sprites/Sprite.js +++ b/src/core/sprites/Sprite.js @@ -212,11 +212,11 @@ } else { - w0 = orig.width * (1 - anchor._x); - w1 = orig.width * -anchor._x; + w1 = -anchor._x * orig.width; + w0 = w1 + orig.width; - h0 = orig.height * (1 - anchor._y); - h1 = orig.height * -anchor._y; + h1 = -anchor._y * orig.height; + h0 = h1 + orig.height; } // xy @@ -269,11 +269,11 @@ const tx = wt.tx; const ty = wt.ty; - const w0 = (orig.width) * (1 - anchor._x); - const w1 = (orig.width) * -anchor._x; + const w1 = -anchor._x * orig.width; + const w0 = w1 + orig.width; - const h0 = orig.height * (1 - anchor._y); - const h1 = orig.height * -anchor._y; + const h1 = -anchor._y * orig.height; + const h0 = h1 + orig.height; // xy vertexData[0] = (a * w1) + (c * h1) + tx; @@ -440,7 +440,7 @@ * * @static * @param {number|string|PIXI.BaseTexture|HTMLCanvasElement|HTMLVideoElement} source Source to create texture from - * @return {PIXI.Sprite} The newly created texture + * @return {PIXI.Sprite} The newly created sprite */ static from(source) { @@ -538,8 +538,8 @@ } /** - * The tint applied to the sprite. This is a hex value. A value of - * 0xFFFFFF will remove any tint effect. + * The tint applied to the sprite. This is a hex value. + * A value of 0xFFFFFF will remove any tint effect. * * @member {number} * @default 0xFFFFFF diff --git a/src/core/sprites/canvas/CanvasTinter.js b/src/core/sprites/canvas/CanvasTinter.js index 1cf316b..27e5e0c 100644 --- a/src/core/sprites/canvas/CanvasTinter.js +++ b/src/core/sprites/canvas/CanvasTinter.js @@ -75,8 +75,8 @@ crop.width *= resolution; crop.height *= resolution; - canvas.width = crop.width; - canvas.height = crop.height; + canvas.width = Math.ceil(crop.width); + canvas.height = Math.ceil(crop.height); context.fillStyle = `#${(`00000${(color | 0).toString(16)}`).substr(-6)}`; @@ -130,8 +130,8 @@ crop.width *= resolution; crop.height *= resolution; - canvas.width = crop.width; - canvas.height = crop.height; + canvas.width = Math.ceil(crop.width); + canvas.height = Math.ceil(crop.height); context.globalCompositeOperation = 'copy'; context.fillStyle = `#${(`00000${(color | 0).toString(16)}`).substr(-6)}`; @@ -172,8 +172,8 @@ crop.width *= resolution; crop.height *= resolution; - canvas.width = crop.width; - canvas.height = crop.height; + canvas.width = Math.ceil(crop.width); + canvas.height = Math.ceil(crop.height); context.globalCompositeOperation = 'copy'; context.drawImage( diff --git a/src/core/sprites/webgl/SpriteRenderer.js b/src/core/sprites/webgl/SpriteRenderer.js index 2affcd5..7c352e2 100644 --- a/src/core/sprites/webgl/SpriteRenderer.js +++ b/src/core/sprites/webgl/SpriteRenderer.js @@ -30,7 +30,7 @@ /** * Number of values sent in the vertex buffer. - * positionX, positionY, colorR, colorG, colorB = 5 + * aVertexPosition(2), aTextureCoord(1), aColor(1), aTextureId(1) = 5 * * @member {number} */ @@ -75,7 +75,6 @@ this.shader = null; this.currentIndex = 0; - TICK = 0; this.groups = []; for (let k = 0; k < this.size; k++) diff --git a/src/core/textures/BaseTexture.js b/src/core/textures/BaseTexture.js index 853de7e..aeccf39 100644 --- a/src/core/textures/BaseTexture.js +++ b/src/core/textures/BaseTexture.js @@ -232,16 +232,24 @@ this.realWidth = this.source.naturalWidth || this.source.videoWidth || this.source.width; this.realHeight = this.source.naturalHeight || this.source.videoHeight || this.source.height; - this.width = this.realWidth / this.resolution; - this.height = this.realHeight / this.resolution; - - this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); + this._updateDimensions(); } this.emit('update', this); } /** + * Update dimensions from real values + */ + _updateDimensions() + { + this.width = this.realWidth / this.resolution; + this.height = this.realHeight / this.resolution; + + this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); + } + + /** * Load a source. * * If the source is not-immediately-available, such as an image that needs to be @@ -525,11 +533,7 @@ this.realWidth = Math.round(svgWidth * this.sourceScale); this.realHeight = Math.round(svgHeight * this.sourceScale); - this.width = this.realWidth / this.resolution; - this.height = this.realHeight / this.resolution; - - // Check pow2 after scale - this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); + this._updateDimensions(); // Create a canvas element const canvas = document.createElement('canvas'); @@ -690,4 +694,52 @@ return baseTexture; } + + /** + * Helper function that creates a base texture based on the source you provide. + * The source can be - image url, image element, canvas element. + * + * @static + * @param {string|HTMLImageElement|HTMLCanvasElement} source - The source to create base texture from. + * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values + * @param {number} [sourceScale=(auto)] - Scale for the original image, used with Svg images. + * @return {PIXI.BaseTexture} The new base texture. + */ + static from(source, scaleMode, sourceScale) + { + if (typeof source === 'string') + { + return BaseTexture.fromImage(source, undefined, scaleMode, sourceScale); + } + else if (source instanceof HTMLImageElement) + { + const imageUrl = source.src; + let baseTexture = BaseTextureCache[imageUrl]; + + if (!baseTexture) + { + baseTexture = new BaseTexture(source, scaleMode); + baseTexture.imageUrl = imageUrl; + + if (sourceScale) + { + baseTexture.sourceScale = sourceScale; + } + + // if there is an @2x at the end of the url we are going to assume its a highres image + baseTexture.resolution = getResolutionOfUrl(imageUrl); + + BaseTextureCache[imageUrl] = baseTexture; + } + + return baseTexture; + } + else if (source instanceof HTMLCanvasElement) + { + return BaseTexture.fromCanvas(source, scaleMode); + } + + // lets assume its a base texture! + return source; + } } diff --git a/src/core/textures/Spritesheet.js b/src/core/textures/Spritesheet.js new file mode 100644 index 0000000..d469c62 --- /dev/null +++ b/src/core/textures/Spritesheet.js @@ -0,0 +1,274 @@ +import { Rectangle, Texture } from '../'; +import { getResolutionOfUrl, TextureCache } from '../utils'; + +/** + * Utility class for maintaining reference to a collection + * of Textures on a single Spritesheet. + * + * @class + * @memberof PIXI + */ +export default class Spritesheet +{ + /** + * The maximum number of Textures to build per process. + * + * @type {number} + * @default 1000 + */ + static get BATCH_SIZE() + { + return 1000; + } + + /** + * @param {PIXI.BaseTexture} baseTexture Reference to the source BaseTexture object. + * @param {Object} data - Spritesheet image data. + * @param {string} [resolutionFilename] - The filename to consider when determining + * the resolution of the spritesheet. If not provided, the imageUrl will + * be used on the BaseTexture. + */ + constructor(baseTexture, data, resolutionFilename = null) + { + /** + * Reference to ths source texture + * @type {PIXI.BaseTexture} + */ + this.baseTexture = baseTexture; + + /** + * Map of spritesheet textures. + * @type {Object} + */ + this.textures = {}; + + /** + * Reference to the original JSON data. + * @type {Object} + */ + this.data = data; + + /** + * The resolution of the spritesheet. + * @type {number} + */ + this.resolution = this._updateResolution( + resolutionFilename || this.baseTexture.imageUrl + ); + + /** + * Map of spritesheet frames. + * @type {Object} + * @private + */ + this._frames = this.data.frames; + + /** + * Collection of frame names. + * @type {string[]} + * @private + */ + this._frameKeys = Object.keys(this._frames); + + /** + * Current batch index being processed. + * @type {number} + * @private + */ + this._batchIndex = 0; + + /** + * Callback when parse is completed. + * @type {Function} + * @private + */ + this._callback = null; + } + + /** + * Generate the resolution from the filename or fallback + * to the meta.scale field of the JSON data. + * + * @private + * @param {string} resolutionFilename - The filename to use for resolving + * the default resolution. + * @return {number} Resolution to use for spritesheet. + */ + _updateResolution(resolutionFilename) + { + const scale = this.data.meta.scale; + + // Use a defaultValue of `null` to check if a url-based resolution is set + let resolution = getResolutionOfUrl(resolutionFilename, null); + + // No resolution found via URL + if (resolution === null) + { + // Use the scale value or default to 1 + resolution = scale !== undefined ? parseFloat(scale) : 1; + } + + // For non-1 resolutions, update baseTexture + if (resolution !== 1) + { + this.baseTexture.resolution = resolution; + this.baseTexture.update(); + } + + return resolution; + } + + /** + * Parser spritesheet from loaded data. This is done asynchronously + * to prevent creating too many Texture within a single process. + * + * @param {Function} callback - Callback when complete returns + * a map of the Textures for this spritesheet. + */ + parse(callback) + { + this._batchIndex = 0; + this._callback = callback; + + if (this._frameKeys.length <= Spritesheet.BATCH_SIZE) + { + this._processFrames(0); + this._parseComplete(); + } + else + { + this._nextBatch(); + } + } + + /** + * Process a batch of frames + * + * @private + * @param {number} initialFrameIndex - The index of frame to start. + */ + _processFrames(initialFrameIndex) + { + let frameIndex = initialFrameIndex; + const maxFrames = Spritesheet.BATCH_SIZE; + + while (frameIndex - initialFrameIndex < maxFrames && frameIndex < this._frameKeys.length) + { + const i = this._frameKeys[frameIndex]; + const rect = this._frames[i].frame; + + if (rect) + { + let frame = null; + let trim = null; + const orig = new Rectangle( + 0, + 0, + this._frames[i].sourceSize.w / this.resolution, + this._frames[i].sourceSize.h / this.resolution + ); + + if (this._frames[i].rotated) + { + frame = new Rectangle( + rect.x / this.resolution, + rect.y / this.resolution, + rect.h / this.resolution, + rect.w / this.resolution + ); + } + else + { + frame = new Rectangle( + rect.x / this.resolution, + rect.y / this.resolution, + rect.w / this.resolution, + rect.h / this.resolution + ); + } + + // Check to see if the sprite is trimmed + if (this._frames[i].trimmed) + { + trim = new Rectangle( + this._frames[i].spriteSourceSize.x / this.resolution, + this._frames[i].spriteSourceSize.y / this.resolution, + rect.w / this.resolution, + rect.h / this.resolution + ); + } + + this.textures[i] = new Texture( + this.baseTexture, + frame, + orig, + trim, + this._frames[i].rotated ? 2 : 0 + ); + + // lets also add the frame to pixi's global cache for fromFrame and fromImage functions + TextureCache[i] = this.textures[i]; + } + + frameIndex++; + } + } + + /** + * The parse has completed. + * + * @private + */ + _parseComplete() + { + const callback = this._callback; + + this._callback = null; + this._batchIndex = 0; + callback.call(this, this.textures); + } + + /** + * Begin the next batch of textures. + * + * @private + */ + _nextBatch() + { + this._processFrames(this._batchIndex * Spritesheet.BATCH_SIZE); + this._batchIndex++; + setTimeout(() => + { + if (this._batchIndex * Spritesheet.BATCH_SIZE < this._frameKeys.length) + { + this._nextBatch(); + } + else + { + this._parseComplete(); + } + }, 0); + } + + /** + * Destroy Spritesheet and don't use after this. + * + * @param {boolean} [destroyBase=false] Whether to destroy the base texture as well + */ + destroy(destroyBase = false) + { + for (const i in this.textures) + { + this.textures[i].destroy(); + } + this._frames = null; + this._frameKeys = null; + this.data = null; + this.textures = null; + if (destroyBase) + { + this.baseTexture.destroy(); + } + this.baseTexture = null; + } +} diff --git a/src/core/textures/Texture.js b/src/core/textures/Texture.js index 6403d4f..afe4254 100644 --- a/src/core/textures/Texture.js +++ b/src/core/textures/Texture.js @@ -3,7 +3,7 @@ import TextureUvs from './TextureUvs'; import EventEmitter from 'eventemitter3'; import { Rectangle } from '../math'; -import { TextureCache, BaseTextureCache } from '../utils'; +import { TextureCache, BaseTextureCache, getResolutionOfUrl } from '../utils'; /** * A texture stores the information that represents an image or part of an image. It cannot be added @@ -365,7 +365,8 @@ * The source can be - frame id, image url, video url, canvas element, video element, base texture * * @static - * @param {number|string|PIXI.BaseTexture|HTMLCanvasElement|HTMLVideoElement} source - Source to create texture from + * @param {number|string|HTMLImageElement|HTMLCanvasElement|HTMLVideoElement|PIXI.BaseTexture} + * source - Source to create texture from * @return {PIXI.Texture} The newly created texture */ static from(source) @@ -393,7 +394,7 @@ } else if (source instanceof HTMLImageElement) { - return new Texture(new BaseTexture(source)); + return new Texture(BaseTexture.from(source)); } else if (source instanceof HTMLCanvasElement) { @@ -413,6 +414,43 @@ } /** + * Create a texture from a source and add to the cache. + * + * @static + * @param {HTMLImageElement|HTMLCanvasElement} source - The input source. + * @param {String} imageUrl - File name of texture, for cache and resolving resolution. + * @param {String} [name] - Human readible name for the texture cache. If no name is + * specified, only `imageUrl` will be used as the cache ID. + * @return {PIXI.Texture} Output texture + */ + static fromLoader(source, imageUrl, name) + { + const baseTexture = new BaseTexture(source, null, getResolutionOfUrl(imageUrl)); + const texture = new Texture(baseTexture); + + baseTexture.imageUrl = imageUrl; + + // No name, use imageUrl instead + if (!name) + { + name = imageUrl; + } + + // lets also add the frame to pixi's global cache for fromFrame and fromImage fucntions + BaseTextureCache[name] = baseTexture; + TextureCache[name] = texture; + + // also add references by url if they are different. + if (name !== imageUrl) + { + BaseTextureCache[imageUrl] = baseTexture; + TextureCache[imageUrl] = texture; + } + + return texture; + } + + /** * Adds a texture to the global TextureCache. This cache is shared across the whole PIXI object. * * @static @@ -522,6 +560,29 @@ } } +function createWhiteTexture() +{ + const canvas = document.createElement('canvas'); + + canvas.width = 10; + canvas.height = 10; + + const context = canvas.getContext('2d'); + + context.fillStyle = 'white'; + context.fillRect(0, 0, 10, 10); + + return new Texture(new BaseTexture(canvas)); +} + +function removeAllHandlers(tex) +{ + tex.destroy = function _emptyDestroy() { /* empty */ }; + tex.on = function _emptyOn() { /* empty */ }; + tex.once = function _emptyOnce() { /* empty */ }; + tex.emit = function _emptyEmit() { /* empty */ }; +} + /** * An empty texture, used often to not have to create multiple empty textures. * Can not be destroyed. @@ -530,7 +591,14 @@ * @constant */ Texture.EMPTY = new Texture(new BaseTexture()); -Texture.EMPTY.destroy = function _emptyDestroy() { /* empty */ }; -Texture.EMPTY.on = function _emptyOn() { /* empty */ }; -Texture.EMPTY.once = function _emptyOnce() { /* empty */ }; -Texture.EMPTY.emit = function _emptyEmit() { /* empty */ }; +removeAllHandlers(Texture.EMPTY); + +/** + * A white texture of 10x10 size, used for graphics and other things + * Can not be destroyed. + * + * @static + * @constant + */ +Texture.WHITE = createWhiteTexture(); +removeAllHandlers(Texture.WHITE); diff --git a/src/core/ticker/Ticker.js b/src/core/ticker/Ticker.js index d98a1f0..17a1517 100644 --- a/src/core/ticker/Ticker.js +++ b/src/core/ticker/Ticker.js @@ -67,11 +67,12 @@ * is based, this value is neither capped nor scaled. * If the platform supports DOMHighResTimeStamp, * this value will have a precision of 1 µs. + * Defaults to target frame time * * @member {number} - * @default 1 / TARGET_FPMS + * @default 16.66 */ - this.elapsedMS = 1 / settings.TARGET_FPMS; // default to target frame time + this.elapsedMS = 1 / settings.TARGET_FPMS; /** * The last time {@link PIXI.ticker.Ticker#update} was invoked. diff --git a/src/core/utils/index.js b/src/core/utils/index.js index 6ff609f..be44750 100644 --- a/src/core/utils/index.js +++ b/src/core/utils/index.js @@ -2,6 +2,7 @@ import settings from '../settings'; import EventEmitter from 'eventemitter3'; import pluginTarget from './pluginTarget'; +import * as mixins from './mixin'; import * as isMobile from 'ismobilejs'; let nextUid = 0; @@ -33,6 +34,7 @@ * @type {mixin} */ pluginTarget, + mixins, }; /** @@ -93,7 +95,7 @@ */ export function rgb2hex(rgb) { - return (((rgb[0] * 255) << 16) + ((rgb[1] * 255) << 8) + (rgb[2] * 255)); + return (((rgb[0] * 255) << 16) + ((rgb[1] * 255) << 8) + (rgb[2] * 255 | 0)); } /** @@ -368,3 +370,43 @@ * @private */ export const ProgramCache = {}; + +/** + * Destroys all texture in the cache + * + * @memberof PIXI.utils + * @function destroyTextureCache + */ +export function destroyTextureCache() +{ + let key; + + for (key in TextureCache) + { + TextureCache[key].destroy(); + } + for (key in BaseTextureCache) + { + BaseTextureCache[key].destroy(); + } +} + +/** + * Removes all textures from cache, but does not destroy them + * + * @memberof PIXI.utils + * @function clearTextureCache + */ +export function clearTextureCache() +{ + let key; + + for (key in TextureCache) + { + delete TextureCache[key]; + } + for (key in BaseTextureCache) + { + delete BaseTextureCache[key]; + } +} diff --git a/src/core/utils/mixin.js b/src/core/utils/mixin.js new file mode 100644 index 0000000..114de2a --- /dev/null +++ b/src/core/utils/mixin.js @@ -0,0 +1,59 @@ +/** + * Mixes all enumerable properties and methods from a source object to a target object. + * + * @memberof PIXI.utils.mixins + * @function mixin + * @param {object} target The prototype or instance that properties and methods should be added to. + * @param {object} source The source of properties and methods to mix in. + */ +export function mixin(target, source) +{ + if (!target || !source) return; + // in ES8/ES2017, this would be really easy: + // Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); + + // get all the enumerable property keys + const keys = Object.keys(source); + + // loop through properties + for (let i = 0; i < keys.length; ++i) + { + const propertyName = keys[i]; + + // Set the property using the property descriptor - this works for accessors and normal value properties + Object.defineProperty(target, propertyName, Object.getOwnPropertyDescriptor(source, propertyName)); + } +} + +const mixins = []; + +/** + * Queues a mixin to be handled towards the end of the initialization of PIXI, so that deprecation + * can take effect. + * + * @memberof PIXI.utils.mixins + * @function delayMixin + * @private + * @param {object} target The prototype or instance that properties and methods should be added to. + * @param {object} source The source of properties and methods to mix in. + */ +export function delayMixin(target, source) +{ + mixins.push(target, source); +} + +/** + * Handles all mixins queued via delayMixin(). + * + * @memberof PIXI.utils.mixins + * @function performMixins + * @private + */ +export function performMixins() +{ + for (let i = 0; i < mixins.length; i += 2) + { + mixin(mixins[i], mixins[i + 1]); + } + mixins.length = 0; +} diff --git a/src/deprecation.js b/src/deprecation.js index 045a19f..b9c73b3 100644 --- a/src/deprecation.js +++ b/src/deprecation.js @@ -5,6 +5,7 @@ import * as filters from './filters'; import * as prepare from './prepare'; import * as loaders from './loaders'; +import * as interaction from './interaction'; // provide method to give a stack track for warnings // useful for tracking-down where deprecated methods/properties/classes @@ -534,7 +535,7 @@ { parent: 'GC_MODES', target: 'GC_MODE' }, { parent: 'WRAP_MODES', target: 'WRAP_MODE' }, { parent: 'SCALE_MODES', target: 'SCALE_MODE' }, - { parent: 'PRECISION', target: 'PRECISION' }, + { parent: 'PRECISION', target: 'PRECISION_FRAGMENT' }, ]; for (let i = 0; i < defaults.length; i++) @@ -558,6 +559,32 @@ }); } +Object.defineProperties(core.settings, { + + /** + * @static + * @name PRECISION + * @memberof PIXI.settings + * @see PIXI.PRECISION + * @deprecated since version 4.4.0 + */ + PRECISION: { + enumerable: true, + get() + { + warn('PIXI.settings.PRECISION has been deprecated, please use PIXI.settings.PRECISION_FRAGMENT'); + + return core.settings.PRECISION_FRAGMENT; + }, + set(value) + { + warn('PIXI.settings.PRECISION has been deprecated, please use PIXI.settings.PRECISION_FRAGMENT'); + + core.settings.PRECISION_FRAGMENT = value; + }, + }, +}); + Object.defineProperties(extras, { /** @@ -694,7 +721,7 @@ warn('determineFontProperties is now deprecated, please use the static calculateFontProperties method, ' + 'e.g : Text.calculateFontProperties(fontStyle);'); - return Text.calculateFontProperties(fontStyle); + return core.Text.calculateFontProperties(fontStyle); }; Object.defineProperties(core.TextStyle.prototype, { @@ -1003,3 +1030,69 @@ }, }, }); + +/** + * @name PIXI.interaction.interactiveTarget#defaultCursor + * @static + * @type {number} + * @see PIXI.interaction.interactiveTarget#cursor + * @deprecated since 4.3.0 + */ +Object.defineProperty(interaction.interactiveTarget, 'defaultCursor', { + set(value) + { + warn('Property defaultCursor has been replaced with \'cursor\'. '); + this.cursor = value; + }, + get() + { + warn('Property defaultCursor has been replaced with \'cursor\'. '); + + return this.cursor; + }, + enumerable: true, +}); + +/** + * @name PIXI.interaction.InteractionManager#defaultCursorStyle + * @static + * @type {string} + * @see PIXI.interaction.InteractionManager#cursorStyles + * @deprecated since 4.3.0 + */ +Object.defineProperty(interaction.InteractionManager, 'defaultCursorStyle', { + set(value) + { + warn('Property defaultCursorStyle has been replaced with \'cursorStyles.default\'. '); + this.cursorStyles.default = value; + }, + get() + { + warn('Property defaultCursorStyle has been replaced with \'cursorStyles.default\'. '); + + return this.cursorStyles.default; + }, +}); + +/** + * @name PIXI.interaction.InteractionManager#currentCursorStyle + * @static + * @type {string} + * @see PIXI.interaction.InteractionManager#cursorStyles + * @deprecated since 4.3.0 + */ +Object.defineProperty(interaction.InteractionManager, 'currentCursorStyle', { + set(value) + { + warn('Property currentCursorStyle has been removed.' + + 'See the currentCursorMode property, which works differently.'); + this.currentCursorMode = value; + }, + get() + { + warn('Property currentCursorStyle has been removed.' + + 'See the currentCursorMode property, which works differently.'); + + return this.currentCursorMode; + }, +}); diff --git a/src/extras/BitmapText.js b/src/extras/BitmapText.js index fb6c343..0a0e150 100644 --- a/src/extras/BitmapText.js +++ b/src/extras/BitmapText.js @@ -455,6 +455,73 @@ return this._textHeight; } + + /** + * Register a bitmap font with data and a texture. + * + * @static + * @param {XMLDocument} xml - The XML document data. + * @param {PIXI.Texture} texture - Texture with all symbols. + * @return {Object} Result font object with font, size, lineHeight and char fields. + */ + static registerFont(xml, texture) + { + const data = {}; + const info = xml.getElementsByTagName('info')[0]; + const common = xml.getElementsByTagName('common')[0]; + + data.font = info.getAttribute('face'); + data.size = parseInt(info.getAttribute('size'), 10); + data.lineHeight = parseInt(common.getAttribute('lineHeight'), 10); + data.chars = {}; + + // parse letters + const letters = xml.getElementsByTagName('char'); + + for (let i = 0; i < letters.length; i++) + { + const letter = letters[i]; + const charCode = parseInt(letter.getAttribute('id'), 10); + + const textureRect = new core.Rectangle( + parseInt(letter.getAttribute('x'), 10) + texture.frame.x, + parseInt(letter.getAttribute('y'), 10) + texture.frame.y, + parseInt(letter.getAttribute('width'), 10), + parseInt(letter.getAttribute('height'), 10) + ); + + data.chars[charCode] = { + xOffset: parseInt(letter.getAttribute('xoffset'), 10), + yOffset: parseInt(letter.getAttribute('yoffset'), 10), + xAdvance: parseInt(letter.getAttribute('xadvance'), 10), + kerning: {}, + texture: new core.Texture(texture.baseTexture, textureRect), + + }; + } + + // parse kernings + const kernings = xml.getElementsByTagName('kerning'); + + for (let i = 0; i < kernings.length; i++) + { + const kerning = kernings[i]; + const first = parseInt(kerning.getAttribute('first'), 10); + const second = parseInt(kerning.getAttribute('second'), 10); + const amount = parseInt(kerning.getAttribute('amount'), 10); + + if (data.chars[second]) + { + data.chars[second].kerning[first] = amount; + } + } + + // I'm leaving this as a temporary fix so we can test the bitmap fonts in v3 + // but it's very likely to change + BitmapText.fonts[data.font] = data; + + return data; + } } BitmapText.fonts = {}; diff --git a/src/index.js b/src/index.js index 59356cf..c553427 100644 --- a/src/index.js +++ b/src/index.js @@ -16,6 +16,10 @@ import * as particles from './particles'; import * as prepare from './prepare'; +// handle mixins now, after all code has been added, including deprecation +import { utils } from './core'; +utils.mixins.performMixins(); + export { accessibility, extract, diff --git a/src/interaction/InteractionManager.js b/src/interaction/InteractionManager.js index 7d51134..8baf13a 100644 --- a/src/interaction/InteractionManager.js +++ b/src/interaction/InteractionManager.js @@ -1,18 +1,20 @@ 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'; -// Mix interactiveTarget into core.DisplayObject.prototype -Object.assign( +// Mix interactiveTarget into core.DisplayObject.prototype, after deprecation has been handled +core.utils.mixins.delayMixin( core.DisplayObject.prototype, 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 +70,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 +101,6 @@ this.eventData = new InteractionEvent(); /** - * Tiny little interactiveData pool ! - * - * @member {PIXI.interaction.InteractionData[]} - */ - this.interactiveDataPool = []; - - /** * The DOM element to bind to. * * @private @@ -153,66 +155,12 @@ */ this.supportsPointerEvents = !!window.PointerEvent; - /** - * Are touch events being 'normalized' and converted into pointer events if pointer events are not supported - * For example, on a touch screen mobile device, a touchstart would also be emitted as a pointerdown - * - * @private - * @readonly - * @member {boolean} - */ - this.normalizeTouchEvents = !this.supportsPointerEvents && this.supportsTouchEvents; - - /** - * Are mouse events being 'normalized' and converted into pointer events if pointer events are not supported - * For example, on a desktop pc, a mousedown would also be emitted as a pointerdown - * - * @private - * @readonly - * @member {boolean} - */ - this.normalizeMouseEvents = !this.supportsPointerEvents && !MobileDevice.any; - // this will make it so that you don't have to call bind all the time /** * @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 +168,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,41 +199,32 @@ this.onPointerOver = this.onPointerOver.bind(this); /** - * @private - * @member {Function} + * Dictionary of how different cursor modes are handled. Strings are handled as CSS cursor + * values, objects are handled as dictionaries of CSS values for interactionDOMElement, + * and functions are called instead of changing the CSS. + * Default CSS cursor values are provided for 'default' and 'pointer' modes. + * @member {Object.)>} */ - this.onTouchStart = this.onTouchStart.bind(this); - this.processTouchStart = this.processTouchStart.bind(this); + this.cursorStyles = { + default: 'inherit', + pointer: 'pointer', + }; /** - * @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. - * - * @member {string} - * @default 'inherit' - */ - this.defaultCursorStyle = 'inherit'; - - /** - * The css style of the cursor that is being used. + * The mode of the cursor that is being used. + * The value of this is a key from the cursorStyles dictionary. * * @member {string} */ - this.currentCursorStyle = 'inherit'; + this.currentCursorMode = null; + + /** + * Internal cached let. + * + * @private + * @member {string} + */ + this.cursor = null; /** * Internal cached let. @@ -303,6 +249,7 @@ * object. * * @event mousedown + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -311,6 +258,7 @@ * on the display object. * * @event rightdown + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -319,6 +267,7 @@ * object. * * @event mouseup + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -327,6 +276,7 @@ * over the display object. * * @event rightup + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -335,6 +285,7 @@ * the display object. * * @event click + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -343,6 +294,7 @@ * and released on the display object. * * @event rightclick + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -352,6 +304,7 @@ * [mousedown]{@link PIXI.interaction.InteractionManager#event:mousedown}. * * @event mouseupoutside + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -361,6 +314,7 @@ * [rightdown]{@link PIXI.interaction.InteractionManager#event:rightdown}. * * @event rightupoutside + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -368,6 +322,7 @@ * Fired when a pointer device (usually a mouse) is moved while over the display object * * @event mousemove + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -375,6 +330,7 @@ * Fired when a pointer device (usually a mouse) is moved onto the display object * * @event mouseover + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -382,6 +338,7 @@ * Fired when a pointer device (usually a mouse) is moved off the display object * * @event mouseout + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -389,6 +346,7 @@ * Fired when a pointer device button is pressed on the display object. * * @event pointerdown + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -396,6 +354,14 @@ * Fired when a pointer device button is released over the display object. * * @event pointerup + * @type {PIXI.interaction.InteractionData} + * @memberof PIXI.interaction.InteractionManager# + */ + + /** + * Fired when the operating system cancels a pointer event + * + * @event pointercancel * @memberof PIXI.interaction.InteractionManager# */ @@ -403,6 +369,7 @@ * Fired when a pointer device button is pressed and released on the display object. * * @event pointertap + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -411,6 +378,7 @@ * registered a [pointerdown]{@link PIXI.interaction.InteractionManager#event:pointerdown}. * * @event pointerupoutside + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -418,6 +386,7 @@ * Fired when a pointer device is moved while over the display object * * @event pointermove + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -425,6 +394,7 @@ * Fired when a pointer device is moved onto the display object * * @event pointerover + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -432,6 +402,7 @@ * Fired when a pointer device is moved off the display object * * @event pointerout + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -439,6 +410,7 @@ * Fired when a touch point is placed on the display object. * * @event touchstart + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -446,6 +418,14 @@ * Fired when a touch point is removed from the display object. * * @event touchend + * @type {PIXI.interaction.InteractionData} + * @memberof PIXI.interaction.InteractionManager# + */ + + /** + * Fired when the operating system cancels a touch + * + * @event touchcancel * @memberof PIXI.interaction.InteractionManager# */ @@ -453,6 +433,7 @@ * Fired when a touch point is placed and removed from the display object. * * @event tap + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -461,6 +442,7 @@ * registered a [touchstart]{@link PIXI.interaction.InteractionManager#event:touchstart}. * * @event touchendoutside + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -468,6 +450,7 @@ * Fired when a touch point is moved along the display object. * * @event touchmove + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ } @@ -524,45 +507,30 @@ { window.document.addEventListener('pointermove', this.onPointerMove, true); this.interactionDOMElement.addEventListener('pointerdown', this.onPointerDown, true); - this.interactionDOMElement.addEventListener('pointerout', this.onPointerOut, true); + // pointerout is fired in addition to pointerup (for touch events) and pointercancel + // we already handle those, so for the purposes of what we do in onPointerOut, we only + // care about the pointerleave event + this.interactionDOMElement.addEventListener('pointerleave', 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; @@ -596,45 +564,26 @@ { window.document.removeEventListener('pointermove', this.onPointerMove, true); this.interactionDOMElement.removeEventListener('pointerdown', this.onPointerDown, true); - this.interactionDOMElement.removeEventListener('pointerout', this.onPointerOut, true); + this.interactionDOMElement.removeEventListener('pointerleave', 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) + 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.onPointerDown, true); + this.interactionDOMElement.removeEventListener('touchcancel', this.onPointerCancel, 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); - - 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 = null; @@ -664,7 +613,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; @@ -672,25 +621,80 @@ return; } - this.cursor = this.defaultCursorStyle; + this.cursor = null; // 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(); - - this.processInteractive(this.mouse.global, this.renderer._lastObjectRendered, this.processMouseOverOut, true); - - if (this.currentCursorStyle !== this.cursor) + for (const k in this.activeInteractionData) { - this.currentCursorStyle = this.cursor; - this.interactionDOMElement.style.cursor = this.cursor; + // eslint-disable-next-line no-prototype-builtins + if (this.activeInteractionData.hasOwnProperty(k)) + { + const interactionData = this.activeInteractionData[k]; + + if (interactionData.originalEvent && interactionData.pointerType !== 'touch') + { + const interactionEvent = this.configureInteractionEventForDOMEvent( + this.eventData, + interactionData.originalEvent, + interactionData + ); + + this.processInteractive( + interactionEvent, + this.renderer._lastObjectRendered, + this.processPointerOverOut, + true + ); + } + } } + this.setCursorMode(this.cursor); + // TODO } /** + * Sets the current cursor mode, handling any callbacks or CSS style changes. + * + * @param {string} mode - cursor mode, a key from the cursorStyles dictionary + */ + setCursorMode(mode) + { + mode = mode || 'default'; + // if the mode didn't actually change, bail early + if (this.currentCursorMode === mode) + { + return; + } + this.currentCursorMode = mode; + const style = this.cursorStyles[mode]; + + // only do things if there is a cursor style for it + if (style) + { + switch (typeof style) + { + case 'string': + // string styles are handled as cursor CSS + this.interactionDOMElement.style.cursor = style; + break; + case 'function': + // functions are just called, and passed the cursor mode + style(mode); + break; + case 'object': + // if it is an object, assume that it is a dictionary of CSS styles, + // apply it to the interactionDOMElement + Object.assign(this.interactionDOMElement.style, style); + break; + } + } + } + + /** * Dispatches an event on the display object that was interacted with * * @param {PIXI.Container|PIXI.Sprite|PIXI.extras.TilingSprite} displayObject - the display object in question @@ -748,22 +752,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 @@ -787,9 +795,8 @@ { interactiveParent = false; } - - // it has a mask! Then lets hit test that before continuing.. - if (hitTest && displayObject._mask) + // it has a mask! Then lets hit test that before continuing + else if (hitTest && displayObject._mask) { if (!displayObject._mask.containsPoint(point)) { @@ -797,14 +804,7 @@ } } - // it has a filterArea! Same as mask but easier, its a rectangle - if (hitTest && displayObject.filterArea) - { - if (!displayObject.filterArea.contains(point.x, point.y)) - { - hitTest = false; - } - } + let keepHitTestingAfterChildren = hitTest; // ** FREE TIP **! If an object is not interactive or has no buttons in it // (such as a game scene!) set interactiveChildren to false for that displayObject. @@ -818,7 +818,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 @@ -838,15 +838,20 @@ // This means we no longer need to hit test anything else. We still need to run // through all objects, but we don't need to perform any hit tests. - // { - hitTest = false; - // } + keepHitTestingAfterChildren = false; + + if (child.interactive) + { + hitTest = false; + } // we can break now as we have hit an object. } } } + hitTest = keepHitTestingAfterChildren; + // no point running this if the item is not interactive or does not have an interactive parent. if (interactive) { @@ -868,14 +873,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 +886,175 @@ } /** - * 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].isNormalized) { - 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); + if (!displayObject.trackedPointers[id]) + { + displayObject.trackedPointers[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.trackedPointers[id].rightDown = true; + } + else + { + displayObject.trackedPointers[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.trackedPointers[id] !== undefined) + { + delete displayObject.trackedPointers[id]; + this.dispatchEvent(displayObject, 'pointercancel', interactionEvent); + + if (e.type === 'touchcancel' || e.pointerType === 'touch') + { + this.dispatchEvent(displayObject, 'touchcancel', interactionEvent); + } } } @@ -1159,42 +1066,94 @@ */ 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) { - if (hit) - { - this.dispatchEvent(displayObject, 'pointerup', this.eventData); + const e = interactionEvent.data.originalEvent; - if (displayObject._pointerDown) + const id = interactionEvent.data.identifier; + + const trackingData = displayObject.trackedPointers[id]; + + const isTouch = (e.type === 'touchend' || e.pointerType === 'touch'); + + const isMouse = (e.type.indexOf('mouse') === 0 || e.pointerType === 'mouse'); + + // 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) { - displayObject._pointerDown = false; - this.dispatchEvent(displayObject, 'pointertap', this.eventData); + 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); + } + // update the down state of the tracking data + if (trackingData) + { + if (isRightButton) + { + trackingData.rightDown = hit; + } + else + { + trackingData.leftDown = hit; + } } } - else if (displayObject._pointerDown) + + // Pointers and Touches, and Mouse + if (hit) { - displayObject._pointerDown = false; - this.dispatchEvent(displayObject, 'pointerupoutside', this.eventData); + this.dispatchEvent(displayObject, 'pointerup', interactionEvent); + if (isTouch) this.dispatchEvent(displayObject, 'touchend', interactionEvent); + + if (trackingData) + { + this.dispatchEvent(displayObject, 'pointertap', interactionEvent); + if (isTouch) + { + this.dispatchEvent(displayObject, 'tap', interactionEvent); + // touches are no longer over (if they ever were) when we get the touchend + // so we should ensure that we don't keep pretending that they are + trackingData.over = false; + } + } + } + else if (trackingData) + { + this.dispatchEvent(displayObject, 'pointerupoutside', interactionEvent); + if (isTouch) this.dispatchEvent(displayObject, 'touchendoutside', interactionEvent); + } + // Only remove the tracking data if there is no over/down state still associated with it + if (trackingData && trackingData.none) + { + delete displayObject.trackedPointers[id]; } } @@ -1202,39 +1161,78 @@ * 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 = null; + } - 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') + { + this.setCursorMode(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 (isMouse) { - 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 +1240,94 @@ * 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.setCursorMode(null); + } - 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'); + + let trackingData = displayObject.trackedPointers[id]; + + // if we just moused over the display object, then we need to track that state + if (hit && !trackingData) + { + trackingData = displayObject.trackedPointers[id] = new InteractionTrackingData(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); + } + } + + // only change the cursor if it has not already been changed (by something deeper in the + // display tree) + if (isMouse && this.cursor === null) + { + this.cursor = displayObject.cursor; } } - 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); + } + // if there is no mouse down information for the pointer, then it is safe to delete + if (trackingData.none) + { + delete displayObject.trackedPointers[id]; + } } } @@ -1287,253 +1335,154 @@ * 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 && pointerEvent.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; + + // mark the touch as normalized, just so that we know we did it + touch.isNormalized = true; + + normalizedEvents.push(touch); + } } - else if (this.normalizeMouseEvents) + // apparently PointerEvent subclasses MouseEvent, so yay + else if (event instanceof MouseEvent && (!this.supportsPointerEvents || !(event instanceof window.PointerEvent))) { if (typeof event.isPrimary === 'undefined') event.isPrimary = true; if (typeof event.width === 'undefined') event.width = 1; @@ -1541,10 +1490,21 @@ 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; + + // mark the mouse event as normalized, just so that we know we did it + event.isNormalized = true; + + normalizedEvents.push(event); } + else + { + normalizedEvents.push(event); + } + + return normalizedEvents; } /** @@ -1563,30 +1523,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 +1542,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..6a1f69e --- /dev/null +++ b/src/interaction/InteractionTrackingData.js @@ -0,0 +1,147 @@ +/** + * 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 inactive (not over or down)? + * + * @member {number} + * @memberof PIXI.interaction.InteractionTrackingData# + */ + get none() + { + return this._flags === this.constructor.FLAGS.NONE; + } + + /** + * 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..d252fba 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 * ); */ @@ -38,68 +38,56 @@ hitArea: null, /** - * If enabled, the mouse cursor will change when hovered over the displayObject if it is interactive + * If enabled, the mouse cursor use the pointer behavior when hovered over the displayObject if it is interactive + * Setting this changes the 'cursor' property to `'pointer'`. * - * @inner {boolean} + * @member {boolean} + * @memberof PIXI.interaction.interactiveTarget# */ - buttonMode: false, + get buttonMode() + { + return this.cursor === 'pointer'; + }, + set buttonMode(value) + { + if (value) + { + this.cursor = 'pointer'; + } + else if (this.cursor === 'pointer') + { + this.cursor = null; + } + }, /** - * If buttonMode is enabled, this defines what CSS cursor property is used when the mouse cursor - * is hovered over the displayObject + * This defines what cursor mode is used when the mouse cursor + * is hovered over the displayObject. * * @see https://developer.mozilla.org/en/docs/Web/CSS/cursor * * @inner {string} */ - defaultCursor: 'pointer', - - // some internal checks.. - /** - * Internal check to detect if the mouse cursor is hovered over the displayObject - * - * @inner {boolean} - * @private - */ - _over: false, + cursor: null, /** - * Internal check to detect if the left mouse button is pressed on the displayObject + * Internal set of all active pointers, by identifier * - * @inner {boolean} + * @member {Map} + * @memberof PIXI.interaction.interactiveTarget# * @private */ - _isLeftDown: false, + get trackedPointers() + { + if (this._trackedPointers === undefined) this._trackedPointers = {}; + + return this._trackedPointers; + }, /** - * Internal check to detect if the right mouse button is pressed on the displayObject + * Map of all tracked pointers, by identifier. Use trackedPointers to access. * - * @inner {boolean} - * @private + * @private {Map} */ - _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, + _trackedPointers: undefined, }; diff --git a/src/loaders/bitmapFontParser.js b/src/loaders/bitmapFontParser.js index cc2618c..f4cf996 100644 --- a/src/loaders/bitmapFontParser.js +++ b/src/loaders/bitmapFontParser.js @@ -1,63 +1,19 @@ import * as path from 'path'; -import { Rectangle, Texture, utils } from '../core'; +import { utils } from '../core'; import { Resource } from 'resource-loader'; import { BitmapText } from '../extras'; +/** + * Register a BitmapText font from loader resource. + * + * @function parseBitmapFontData + * @memberof PIXI.loaders + * @param {PIXI.loaders.Resource} resource - Loader resource. + * @param {PIXI.Texture} texture - Reference to texture. + */ export function parse(resource, texture) { - const data = {}; - const info = resource.data.getElementsByTagName('info')[0]; - const common = resource.data.getElementsByTagName('common')[0]; - - data.font = info.getAttribute('face'); - data.size = parseInt(info.getAttribute('size'), 10); - data.lineHeight = parseInt(common.getAttribute('lineHeight'), 10); - data.chars = {}; - - // parse letters - const letters = resource.data.getElementsByTagName('char'); - - for (let i = 0; i < letters.length; i++) - { - const charCode = parseInt(letters[i].getAttribute('id'), 10); - - const textureRect = new Rectangle( - parseInt(letters[i].getAttribute('x'), 10) + texture.frame.x, - parseInt(letters[i].getAttribute('y'), 10) + texture.frame.y, - parseInt(letters[i].getAttribute('width'), 10), - parseInt(letters[i].getAttribute('height'), 10) - ); - - data.chars[charCode] = { - xOffset: parseInt(letters[i].getAttribute('xoffset'), 10), - yOffset: parseInt(letters[i].getAttribute('yoffset'), 10), - xAdvance: parseInt(letters[i].getAttribute('xadvance'), 10), - kerning: {}, - texture: new Texture(texture.baseTexture, textureRect), - - }; - } - - // parse kernings - const kernings = resource.data.getElementsByTagName('kerning'); - - for (let i = 0; i < kernings.length; i++) - { - const first = parseInt(kernings[i].getAttribute('first'), 10); - const second = parseInt(kernings[i].getAttribute('second'), 10); - const amount = parseInt(kernings[i].getAttribute('amount'), 10); - - if (data.chars[second]) - { - data.chars[second].kerning[first] = amount; - } - } - - resource.bitmapFont = data; - - // I'm leaving this as a temporary fix so we can test the bitmap fonts in v3 - // but it's very likely to change - BitmapText.fonts[data.font] = data; + resource.bitmapFont = BitmapText.registerFont(resource.data, texture); } export default function () diff --git a/src/loaders/index.js b/src/loaders/index.js index 8942436..5189793 100644 --- a/src/loaders/index.js +++ b/src/loaders/index.js @@ -5,4 +5,11 @@ export { default as bitmapFontParser, parse as parseBitmapFontData } from './bitmapFontParser'; export { default as spritesheetParser } from './spritesheetParser'; export { default as textureParser } from './textureParser'; + +/** + * Reference to **resource-loader**'s Resource class. + * See https://github.com/englercj/resource-loader + * @class Resource + * @memberof PIXI.loaders + */ export { Resource } from 'resource-loader'; diff --git a/src/loaders/loader.js b/src/loaders/loader.js index 0a670cb..767a411 100644 --- a/src/loaders/loader.js +++ b/src/loaders/loader.js @@ -7,20 +7,47 @@ /** * - * The new loader, extends Resource Loader by Chad Engler : https://github.com/englercj/resource-loader + * The new loader, extends Resource Loader by Chad Engler: https://github.com/englercj/resource-loader * * ```js - * let loader = PIXI.loader; // pixi exposes a premade instance for you to use. + * const loader = PIXI.loader; // pixi exposes a premade instance for you to use. * //or - * let loader = new PIXI.loaders.Loader(); // you can also create your own if you want + * const loader = new PIXI.loaders.Loader(); // you can also create your own if you want * - * loader.add('bunny', 'data/bunny.png'); - * loader.add('spaceship', 'assets/spritesheet.json'); + * const sprites = {}; + * + * // Chainable `add` to enqueue a resource + * loader.add('bunny', 'data/bunny.png') + * .add('spaceship', 'assets/spritesheet.json'); * loader.add('scoreFont', 'assets/score.fnt'); * - * loader.once('complete',onAssetsLoaded); + * // Chainable `pre` to add a middleware that runs for each resource, *before* loading that resource. + * // This is useful to implement custom caching modules (using filesystem, indexeddb, memory, etc). + * loader.pre(cachingMiddleware); * - * loader.load(); + * // Chainable `use` to add a middleware that runs for each resource, *after* loading that resource. + * // This is useful to implement custom parsing modules (like spritesheet parsers, spine parser, etc). + * loader.use(parsingMiddleware); + * + * // The `load` method loads the queue of resources, and calls the passed in callback called once all + * // resources have loaded. + * loader.load((loader, resources) => { + * // resources is an object where the key is the name of the resource loaded and the value is the resource object. + * // They have a couple default properties: + * // - `url`: The URL that the resource was loaded from + * // - `error`: The error that happened when trying to load (if any) + * // - `data`: The raw data that was loaded + * // also may contain other properties based on the middleware that runs. + * sprites.bunny = new PIXI.TilingSprite(resources.bunny.texture); + * sprites.spaceship = new PIXI.TilingSprite(resources.spaceship.texture); + * sprites.scoreFont = new PIXI.TilingSprite(resources.scoreFont.texture); + * }); + * + * // throughout the process multiple signals can be dispatched. + * loader.onProgress.add(() => {}); // called once per loaded/errored file + * loader.onError.add(() => {}); // called once per errored file + * loader.onLoad.add(() => {}); // called once per loaded file + * loader.onComplete.add(() => {}); // called once when the queued resources all load. * ``` * * @see https://github.com/englercj/resource-loader diff --git a/src/loaders/spritesheetParser.js b/src/loaders/spritesheetParser.js index 7570332..e57dfb2 100644 --- a/src/loaders/spritesheetParser.js +++ b/src/loaders/spritesheetParser.js @@ -1,8 +1,6 @@ import { Resource } from 'resource-loader'; import path from 'path'; -import * as core from '../core'; - -const BATCH_SIZE = 1000; +import { Spritesheet } from '../core'; export default function () { @@ -43,134 +41,18 @@ // load the image for this sheet this.add(imageResourceName, resourcePath, loadOptions, function onImageLoad(res) { - resource.textures = {}; + const spritesheet = new Spritesheet( + res.texture.baseTexture, + resource.data, + resource.url + ); - const frames = resource.data.frames; - const frameKeys = Object.keys(frames); - const baseTexture = res.texture.baseTexture; - const scale = resource.data.meta.scale; - - // Use a defaultValue of `null` to check if a url-based resolution is set - let resolution = core.utils.getResolutionOfUrl(resource.url, null); - - // No resolution found via URL - if (resolution === null) + spritesheet.parse(() => { - // Use the scale value or default to 1 - resolution = scale !== undefined ? scale : 1; - } - - // For non-1 resolutions, update baseTexture - if (resolution !== 1) - { - baseTexture.resolution = resolution; - baseTexture.update(); - } - - let batchIndex = 0; - - function processFrames(initialFrameIndex, maxFrames) - { - let frameIndex = initialFrameIndex; - - while (frameIndex - initialFrameIndex < maxFrames && frameIndex < frameKeys.length) - { - const i = frameKeys[frameIndex]; - const rect = frames[i].frame; - - if (rect) - { - let frame = null; - let trim = null; - const orig = new core.Rectangle( - 0, - 0, - frames[i].sourceSize.w / resolution, - frames[i].sourceSize.h / resolution - ); - - if (frames[i].rotated) - { - frame = new core.Rectangle( - rect.x / resolution, - rect.y / resolution, - rect.h / resolution, - rect.w / resolution - ); - } - else - { - frame = new core.Rectangle( - rect.x / resolution, - rect.y / resolution, - rect.w / resolution, - rect.h / resolution - ); - } - - // Check to see if the sprite is trimmed - if (frames[i].trimmed) - { - trim = new core.Rectangle( - frames[i].spriteSourceSize.x / resolution, - frames[i].spriteSourceSize.y / resolution, - rect.w / resolution, - rect.h / resolution - ); - } - - resource.textures[i] = new core.Texture( - baseTexture, - frame, - orig, - trim, - frames[i].rotated ? 2 : 0 - ); - - // lets also add the frame to pixi's global cache for fromFrame and fromImage functions - core.utils.TextureCache[i] = resource.textures[i]; - } - - frameIndex++; - } - } - - function shouldProcessNextBatch() - { - return batchIndex * BATCH_SIZE < frameKeys.length; - } - - function processNextBatch(done) - { - processFrames(batchIndex * BATCH_SIZE, BATCH_SIZE); - batchIndex++; - setTimeout(done, 0); - } - - function iteration() - { - processNextBatch(() => - { - if (shouldProcessNextBatch()) - { - iteration(); - } - else - { - next(); - } - }); - } - - if (frameKeys.length <= BATCH_SIZE) - { - processFrames(0, BATCH_SIZE); + resource.spritesheet = spritesheet; + resource.textures = spritesheet.textures; next(); - } - else - { - iteration(); - } + }); }); }; } diff --git a/src/loaders/textureParser.js b/src/loaders/textureParser.js index 5398a7f..480edc1 100644 --- a/src/loaders/textureParser.js +++ b/src/loaders/textureParser.js @@ -1,5 +1,5 @@ -import * as core from '../core'; import { Resource } from 'resource-loader'; +import Texture from '../core/textures/Texture'; export default function () { @@ -8,23 +8,12 @@ // create a new texture if the data is an Image object if (resource.data && resource.type === Resource.TYPE.IMAGE) { - const baseTexture = new core.BaseTexture(resource.data, null, core.utils.getResolutionOfUrl(resource.url)); - - baseTexture.imageUrl = resource.url; - resource.texture = new core.Texture(baseTexture); - - // lets also add the frame to pixi's global cache for fromFrame and fromImage fucntions - core.utils.BaseTextureCache[resource.name] = baseTexture; - core.utils.TextureCache[resource.name] = resource.texture; - - // also add references by url if they are different. - if (resource.name !== resource.url) - { - core.utils.BaseTextureCache[resource.url] = baseTexture; - core.utils.TextureCache[resource.url] = resource.texture; - } + resource.texture = Texture.fromLoader( + resource.data, + resource.url, + resource.name + ); } - next(); }; } diff --git a/test/core/Container.js b/test/core/Container.js index 8a10b40..d11be33 100644 --- a/test/core/Container.js +++ b/test/core/Container.js @@ -1,5 +1,33 @@ 'use strict'; +function testAddChild(fn) +{ + fn(function (container, obj) + { + container.addChild(obj); + }); + fn(function (container, obj) + { + container.addChildAt(obj); + }); +} + +function testRemoveChild(fn) +{ + fn(function (container, obj) + { + container.removeChild(obj); + }); + fn(function (container, obj) + { + container.removeChildAt(container.children.indexOf(obj)); + }); + fn(function (container, obj) + { + container.removeChildren(container.children.indexOf(obj), container.children.indexOf(obj) + 1); + }); +} + describe('PIXI.Container', function () { describe('parent', function () @@ -70,6 +98,49 @@ expect(spy).to.have.been.called; expect(spy).to.have.been.calledWith(0); }); + + it('should flag child transform and container bounds for recalculation', testAddChild(function (mockAddChild) + { + const container = new PIXI.Container(); + const child = new PIXI.Container(); + + container.getBounds(); + child.getBounds(); + + const boundsID = container._boundsID; + const childParentID = child.transform._parentID; + + mockAddChild(container, child); + + expect(boundsID).to.not.be.equals(container._boundsID); + expect(childParentID).to.not.be.equals(child.transform._parentID); + })); + + it('should recalculate added child correctly', testAddChild(function (mockAddChild) + { + const parent = new PIXI.Container(); + const container = new PIXI.Container(); + const graphics = new PIXI.Graphics(); + + parent.addChild(container); + + graphics.drawRect(0, 0, 10, 10); + container.position.set(100, 200); + container.updateTransform(); + + graphics.getBounds(); + // Oops, that can happen sometimes! + graphics.transform._parentID = container.transform._worldID + 1; + + mockAddChild(container, graphics); + + const bounds = graphics.getBounds(); + + expect(bounds.x).to.be.equal(100); + expect(bounds.y).to.be.equal(200); + expect(bounds.width).to.be.equal(10); + expect(bounds.height).to.be.equal(10); + })); }); describe('removeChildAt', function () @@ -202,6 +273,44 @@ expect(spy).to.have.been.called; expect(spy).to.have.been.calledWith(0); }); + + it('should flag transform for recalculation', testRemoveChild(function (mockRemoveChild) + { + const container = new PIXI.Container(); + const child = new PIXI.Container(); + + container.addChild(child); + container.getBounds(); + + const childParentID = child.transform._parentID; + const boundsID = container._boundsID; + + mockRemoveChild(container, child); + + expect(childParentID).to.not.be.equals(child.transform._parentID); + expect(boundsID).to.not.be.equals(container._boundsID); + })); + + it('should recalculate removed child correctly', testRemoveChild(function (mockRemoveChild) + { + const parent = new PIXI.Container(); + const container = new PIXI.Container(); + const graphics = new PIXI.Graphics(); + + parent.addChild(container); + + graphics.drawRect(0, 0, 10, 10); + container.position.set(100, 200); + container.addChild(graphics); + graphics.getBounds(); + + mockRemoveChild(container, graphics); + + const bounds = graphics.getBounds(); + + expect(bounds.x).to.be.equal(0); + expect(bounds.y).to.be.equal(0); + })); }); describe('getChildIndex', function () diff --git a/test/core/Graphics.js b/test/core/Graphics.js index 98c7584..11e47a0 100644 --- a/test/core/Graphics.js +++ b/test/core/Graphics.js @@ -1,5 +1,8 @@ 'use strict'; +const MockPointer = require('../interaction/MockPointer'); +const withGL = require('../withGL'); + describe('PIXI.Graphics', function () { describe('constructor', function () @@ -218,4 +221,153 @@ expect(spy).to.have.been.calledOnce; }); }); + + describe('mask', function () + { + it('should trigger interaction callback when no mask present', function () + { + const stage = new PIXI.Container(); + const pointer = new MockPointer(stage); + const graphics = new PIXI.Graphics(); + const mask = new PIXI.Graphics(); + const spy = sinon.spy(); + + graphics.interactive = true; + graphics.beginFill(0xFF0000); + graphics.drawRect(0, 0, 50, 50); + graphics.on('click', spy); + stage.addChild(graphics); + mask.beginFill(); + mask.drawRect(0, 0, 50, 50); + graphics.mask = mask; + + pointer.click(10, 10); + + expect(spy).to.have.been.calledOnce; + }); + it('should trigger interaction callback when mask uses beginFill', function () + { + const stage = new PIXI.Container(); + const pointer = new MockPointer(stage); + const graphics = new PIXI.Graphics(); + const mask = new PIXI.Graphics(); + const spy = sinon.spy(); + + graphics.interactive = true; + graphics.beginFill(0xFF0000); + graphics.drawRect(0, 0, 50, 50); + graphics.on('click', spy); + stage.addChild(graphics); + mask.beginFill(); + mask.drawRect(0, 0, 50, 50); + graphics.mask = mask; + + pointer.click(10, 10); + + expect(spy).to.have.been.calledOnce; + }); + + it('should not trigger interaction callback when mask doesn\'t use beginFill', function () + { + const stage = new PIXI.Container(); + const pointer = new MockPointer(stage); + const graphics = new PIXI.Graphics(); + const mask = new PIXI.Graphics(); + const spy = sinon.spy(); + + graphics.interactive = true; + graphics.beginFill(0xFF0000); + graphics.drawRect(0, 0, 50, 50); + graphics.on('click', spy); + stage.addChild(graphics); + mask.drawRect(0, 0, 50, 50); + graphics.mask = mask; + + pointer.click(10, 10); + + expect(spy).to.have.not.been.called; + }); + + it('should trigger interaction callback when mask doesn\'t use beginFill but hitArea is defined', function () + { + const stage = new PIXI.Container(); + const pointer = new MockPointer(stage); + const graphics = new PIXI.Graphics(); + const mask = new PIXI.Graphics(); + const spy = sinon.spy(); + + graphics.interactive = true; + graphics.beginFill(0xFF0000); + graphics.hitArea = new PIXI.Rectangle(0, 0, 50, 50); + graphics.drawRect(0, 0, 50, 50); + graphics.on('click', spy); + stage.addChild(graphics); + mask.drawRect(0, 0, 50, 50); + graphics.mask = mask; + + pointer.click(10, 10); + + expect(spy).to.have.been.calledOnce; + }); + + it('should trigger interaction callback when mask is a sprite', function () + { + const stage = new PIXI.Container(); + const pointer = new MockPointer(stage); + const graphics = new PIXI.Graphics(); + const mask = new PIXI.Graphics(); + const spy = sinon.spy(); + + graphics.interactive = true; + graphics.beginFill(0xFF0000); + graphics.drawRect(0, 0, 50, 50); + graphics.on('click', spy); + stage.addChild(graphics); + mask.drawRect(0, 0, 50, 50); + graphics.mask = new PIXI.Sprite(mask.generateCanvasTexture()); + + pointer.click(10, 10); + + expect(spy).to.have.been.calledOnce; + }); + + it('should calculate tint, alpha and blendMode of fastRect correctly', withGL(function () + { + const renderer = new PIXI.WebGLRenderer(200, 200, {}); + + try + { + const graphics = new PIXI.Graphics(); + + graphics.beginFill(0x102030, 0.6); + graphics.drawRect(2, 3, 100, 100); + graphics.endFill(); + graphics.tint = 0x101010; + graphics.blendMode = 2; + graphics.alpha = 0.3; + + renderer.render(graphics); + + expect(graphics.isFastRect()).to.be.true; + + const sprite = graphics._spriteRect; + + expect(sprite).to.not.be.equals(null); + expect(sprite.worldAlpha).to.equals(0.18); + expect(sprite.blendMode).to.equals(2); + expect(sprite.tint).to.equals(0x010203); + + const bounds = sprite.getBounds(); + + expect(bounds.x).to.equals(2); + expect(bounds.y).to.equals(3); + expect(bounds.width).to.equals(100); + expect(bounds.height).to.equals(100); + } + finally + { + renderer.destroy(); + } + })); + }); }); diff --git a/test/core/Spritesheet.js b/test/core/Spritesheet.js new file mode 100644 index 0000000..283adf5 --- /dev/null +++ b/test/core/Spritesheet.js @@ -0,0 +1,89 @@ +'use strict'; + +const path = require('path'); + +describe('PIXI.Spritesheet', function () +{ + before(function () + { + this.resources = path.join(__dirname, 'resources'); + this.validate = function (spritesheet, done) + { + spritesheet.parse(function (textures) + { + const id = 'goldmine_10_5.png'; + + expect(Object.keys(textures).length).to.equal(1); + expect(Object.keys(spritesheet.textures).length).to.equal(1); + expect(textures[id]).to.be.an.instanceof(PIXI.Texture); + expect(textures[id].width).to.equal(spritesheet.data.frames[id].frame.w / spritesheet.resolution); + expect(textures[id].height).to.equal(spritesheet.data.frames[id].frame.h / spritesheet.resolution); + spritesheet.destroy(true); + expect(spritesheet.textures).to.be.null; + expect(spritesheet.baseTexture).to.be.null; + done(); + }); + }; + }); + + it('should exist on PIXI', function () + { + expect(PIXI.Spritesheet).to.be.a.function; + expect(PIXI.Spritesheet.BATCH_SIZE).to.be.a.number; + }); + + it('should create an instance', function () + { + const baseTexture = new PIXI.BaseTexture(); + const data = { + frames: {}, + meta: {}, + }; + const spritesheet = new PIXI.Spritesheet(baseTexture, data); + + expect(spritesheet.data).to.equal(data); + expect(spritesheet.baseTexture).to.equal(baseTexture); + expect(spritesheet.resolution).to.equal(1); + + spritesheet.destroy(true); + }); + + it('should create instance with scale resolution', function (done) + { + const data = require(path.resolve(this.resources, 'building1.json')); // eslint-disable-line global-require + const image = new Image(); + + image.src = path.join(this.resources, data.meta.image); + image.onload = () => + { + const baseTexture = new PIXI.BaseTexture(image, null, 1); + const spritesheet = new PIXI.Spritesheet(baseTexture, data); + + expect(data).to.be.an.object; + expect(data.meta.image).to.equal('building1.png'); + expect(spritesheet.resolution).to.equal(0.5); + + this.validate(spritesheet, done); + }; + }); + + it('should create instance with filename resolution', function (done) + { + const uri = path.resolve(this.resources, 'building1@2x.json'); + const data = require(uri); // eslint-disable-line global-require + const image = new Image(); + + image.src = path.join(this.resources, data.meta.image); + image.onload = () => + { + const baseTexture = new PIXI.BaseTexture(image, null, 1); + const spritesheet = new PIXI.Spritesheet(baseTexture, data, uri); + + expect(data).to.be.an.object; + expect(data.meta.image).to.equal('building1@2x.png'); + expect(spritesheet.resolution).to.equal(2); + + this.validate(spritesheet, done); + }; + }); +}); diff --git a/package.json b/package.json index 6a366ba..71ade15 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pixi.js", - "version": "4.3.4", + "version": "4.3.5", "description": "Pixi.js is a fast lightweight 2D library that works across all devices.", "author": "Mat Groves", "contributors": [ @@ -61,7 +61,7 @@ "ismobilejs": "^0.4.0", "object-assign": "^4.0.1", "pixi-gl-core": "^1.0.3", - "resource-loader": "^2.0.4" + "resource-loader": "^2.0.6" }, "devDependencies": { "babel-cli": "^6.18.0", @@ -70,9 +70,9 @@ "babel-preset-es2015": "^6.14.0", "babelify": "^7.3.0", "del": "^2.2.0", - "electron-prebuilt": "^1.3.2", + "electron": "^1.4.15", "eslint": "^3.5.0", - "floss": "^1.2.0", + "floss": "^2.0.1", "gh-pages": "^0.11.0", "jaguarjs-jsdoc": "^1.0.1", "js-md5": "^0.4.1", diff --git a/src/accessibility/AccessibilityManager.js b/src/accessibility/AccessibilityManager.js index df2716a..f16bcb1 100644 --- a/src/accessibility/AccessibilityManager.js +++ b/src/accessibility/AccessibilityManager.js @@ -3,7 +3,7 @@ import accessibleTarget from './accessibleTarget'; // add some extra variables to the container.. -Object.assign( +core.utils.mixins.delayMixin( core.DisplayObject.prototype, accessibleTarget ); diff --git a/src/core/Application.js b/src/core/Application.js index ea0f058..5340578 100644 --- a/src/core/Application.js +++ b/src/core/Application.js @@ -1,6 +1,6 @@ import { autoDetectRenderer } from './autoDetectRenderer'; import Container from './display/Container'; -import Ticker from './ticker/Ticker'; +import { shared, Ticker } from './ticker'; /** * Convenience class to create a new PIXI application. @@ -35,9 +35,9 @@ * @param {boolean} [noWebGL=false] - prevents selection of WebGL renderer, even if such is present * @param {boolean} [options.legacy=false] - If true Pixi will aim to ensure compatibility * with older / less advanced devices. If you experience unexplained flickering try setting this to true. - + * @param {boolean} [useSharedTicker=false] - `true` to use PIXI.ticker.shared, `false` to create new ticker. */ - constructor(width, height, options, noWebGL) + constructor(width, height, options, noWebGL, useSharedTicker = false) { /** * WebGL renderer if available, otherwise CanvasRenderer @@ -46,23 +46,46 @@ this.renderer = autoDetectRenderer(width, height, options, noWebGL); /** - * The root display container that's renderered. + * The root display container that's rendered. * @member {PIXI.Container} */ this.stage = new Container(); /** + * Internal reference to the ticker + * @member {PIXI.ticker.Ticker} + * @private + */ + this._ticker = null; + + /** * Ticker for doing render updates. * @member {PIXI.ticker.Ticker} + * @default PIXI.ticker.shared */ - this.ticker = new Ticker(); - - this.ticker.add(this.render, this); + this.ticker = useSharedTicker ? shared : new Ticker(); // Start the rendering this.start(); } + set ticker(ticker) // eslint-disable-line require-jsdoc + { + if (this._ticker) + { + this._ticker.remove(this.render, this); + } + this._ticker = ticker; + if (ticker) + { + ticker.add(this.render, this); + } + } + get ticker() // eslint-disable-line require-jsdoc + { + return this._ticker; + } + /** * Render the current stage. */ @@ -76,7 +99,7 @@ */ stop() { - this.ticker.stop(); + this._ticker.stop(); } /** @@ -84,7 +107,7 @@ */ start() { - this.ticker.start(); + this._ticker.start(); } /** @@ -98,13 +121,22 @@ } /** + * Reference to the renderer's screen rectangle. Its safe to use as filterArea or hitArea for whole screen + * @member {PIXI.Rectangle} + * @readonly + */ + get screen() + { + return this.renderer.screen; + } + + /** * Destroy and don't use after this. * @param {Boolean} [removeView=false] Automatically remove canvas from DOM. */ destroy(removeView) { this.stop(); - this.ticker.remove(this.render, this); this.ticker = null; this.stage.destroy(); diff --git a/src/core/Shader.js b/src/core/Shader.js new file mode 100644 index 0000000..78b17e6 --- /dev/null +++ b/src/core/Shader.js @@ -0,0 +1,46 @@ +import { GLShader } from 'pixi-gl-core'; +import settings from './settings'; + +function checkPrecision(src, def) +{ + if (src instanceof Array) + { + if (src[0].substring(0, 9) !== 'precision') + { + const copy = src.slice(0); + + copy.unshift(`precision ${def} float;`); + + return copy; + } + } + else if (src.substring(0, 9) !== 'precision') + { + return `precision ${def} float;\n${src}`; + } + + return src; +} + +/** + * Wrapper class, webGL Shader for Pixi. + * Adds precision string if vertexSrc or fragmentSrc have no mention of it. + * + * @class + * @extends GLShader + * @memberof PIXI + */ +export default class Shader extends GLShader +{ + /** + * + * @param {WebGLRenderingContext} gl - The current WebGL rendering context + * @param {string|string[]} vertexSrc - The vertex shader source as an array of strings. + * @param {string|string[]} fragmentSrc - The fragment shader source as an array of strings. + */ + constructor(gl, vertexSrc, fragmentSrc) + { + super(gl, checkPrecision(vertexSrc, settings.PRECISION_VERTEX), + checkPrecision(fragmentSrc, settings.PRECISION_FRAGMENT)); + } +} diff --git a/src/core/const.js b/src/core/const.js index ef6f662..3de0efe 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -188,7 +188,7 @@ * The gc modes that are supported by pixi. * * The {@link PIXI.settings.GC_MODE} Garbage Collection mode for pixi textures is AUTO - * If set to GC_MODE, the renderer will occasianally check textures usage. If they are not + * If set to GC_MODE, the renderer will occasionally check textures usage. If they are not * used for a specified period of time they will be removed from the GPU. They will of course * be uploaded again when they are required. This is a silent behind the scenes process that * should ensure that the GPU does not get filled up. diff --git a/src/core/display/Container.js b/src/core/display/Container.js index ed8cd5f..17fde8d 100644 --- a/src/core/display/Container.js +++ b/src/core/display/Container.js @@ -73,11 +73,12 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.push(child); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -108,11 +109,12 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.splice(index, 0, child); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -225,10 +227,11 @@ if (index === -1) return null; child.parent = null; + // ensure child transform will be recalculated + child.transform._parentID = -1; removeItems(this.children, index, 1); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -249,11 +252,12 @@ { const child = this.getChildAt(index); + // ensure child transform will be recalculated.. child.parent = null; + child.transform._parentID = -1; removeItems(this.children, index, 1); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -284,13 +288,12 @@ for (let i = 0; i < removed.length; ++i) { removed[i].parent = null; + if (removed[i].transform) + { + removed[i].transform._parentID = -1; + } } - // ensure a transform will be recalculated.. - if (this.transform) - { - this.transform._parentID = -1; - } this._boundsID++; this.onChildrenChange(beginIndex); diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index f37f2f5..be4c4fb 100644 --- a/src/core/graphics/Graphics.js +++ b/src/core/graphics/Graphics.js @@ -28,8 +28,9 @@ { /** * + * @param {boolean} [nativeLines=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP */ - constructor() + constructor(nativeLines = false) { super(); @@ -50,6 +51,13 @@ this.lineWidth = 0; /** + * If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * + * @member {boolean} + */ + this.nativeLines = nativeLines; + + /** * The color of any lines drawn. * * @member {string} @@ -757,28 +765,14 @@ if (!this._spriteRect) { - if (!Graphics._SPRITE_TEXTURE) - { - Graphics._SPRITE_TEXTURE = RenderTexture.create(10, 10); - - const canvas = document.createElement('canvas'); - - canvas.width = 10; - canvas.height = 10; - - const context = canvas.getContext('2d'); - - context.fillStyle = 'white'; - context.fillRect(0, 0, 10, 10); - - Graphics._SPRITE_TEXTURE = Texture.fromCanvas(canvas); - } - - this._spriteRect = new Sprite(Graphics._SPRITE_TEXTURE); + this._spriteRect = new Sprite(new Texture(Texture.WHITE)); } + + const sprite = this._spriteRect; + if (this.tint === 0xffffff) { - this._spriteRect.tint = this.graphicsData[0].fillColor; + sprite.tint = this.graphicsData[0].fillColor; } else { @@ -792,20 +786,21 @@ t1[1] *= t2[1]; t1[2] *= t2[2]; - this._spriteRect.tint = rgb2hex(t1); + sprite.tint = rgb2hex(t1); } - this._spriteRect.alpha = this.graphicsData[0].fillAlpha; - this._spriteRect.worldAlpha = this.worldAlpha * this._spriteRect.alpha; + sprite.alpha = this.graphicsData[0].fillAlpha; + sprite.worldAlpha = this.worldAlpha * sprite.alpha; + sprite.blendMode = this.blendMode; - Graphics._SPRITE_TEXTURE._frame.width = rect.width; - Graphics._SPRITE_TEXTURE._frame.height = rect.height; + sprite.texture._frame.width = rect.width; + sprite.texture._frame.height = rect.height; - this._spriteRect.transform.worldTransform = this.transform.worldTransform; + sprite.transform.worldTransform = this.transform.worldTransform; - this._spriteRect.anchor.set(-rect.x / rect.width, -rect.y / rect.height); - this._spriteRect._onAnchorUpdate(); + sprite.anchor.set(-rect.x / rect.width, -rect.y / rect.height); + sprite._onAnchorUpdate(); - this._spriteRect._renderWebGL(renderer); + sprite._renderWebGL(renderer); } /** @@ -1031,6 +1026,7 @@ this.fillColor, this.fillAlpha, this.filling, + this.nativeLines, shape ); diff --git a/src/core/graphics/GraphicsData.js b/src/core/graphics/GraphicsData.js index f5a87ae..b6f03c0 100644 --- a/src/core/graphics/GraphicsData.js +++ b/src/core/graphics/GraphicsData.js @@ -14,14 +14,19 @@ * @param {number} fillColor - the color of the fill * @param {number} fillAlpha - the alpha of the fill * @param {boolean} fill - whether or not the shape is filled with a colour + * @param {boolean} nativeLines - the method for drawing lines * @param {PIXI.Circle|PIXI.Rectangle|PIXI.Ellipse|PIXI.Polygon} shape - The shape object to draw. */ - constructor(lineWidth, lineColor, lineAlpha, fillColor, fillAlpha, fill, shape) + constructor(lineWidth, lineColor, lineAlpha, fillColor, fillAlpha, fill, nativeLines, shape) { /** * @member {number} the width of the line to draw */ this.lineWidth = lineWidth; + /** + * @member {boolean} if true the liens will be draw using LINES instead of TRIANGLE_STRIP + */ + this.nativeLines = nativeLines; /** * @member {number} the color of the line to draw @@ -85,6 +90,7 @@ this.fillColor, this.fillAlpha, this.fill, + this.nativeLines, this.shape ); } diff --git a/src/core/graphics/webgl/GraphicsRenderer.js b/src/core/graphics/webgl/GraphicsRenderer.js index 14083fe..67f5e92 100644 --- a/src/core/graphics/webgl/GraphicsRenderer.js +++ b/src/core/graphics/webgl/GraphicsRenderer.js @@ -102,7 +102,15 @@ shaderTemp.uniforms.alpha = graphics.worldAlpha; renderer.bindVao(webGLData.vao); - webGLData.vao.draw(gl.TRIANGLE_STRIP, webGLData.indices.length); + + if (graphics.nativeLines) + { + gl.drawArrays(gl.LINES, 0, webGLData.points.length / 6); + } + else + { + webGLData.vao.draw(gl.TRIANGLE_STRIP, webGLData.indices.length); + } } } diff --git a/src/core/graphics/webgl/utils/buildLine.js b/src/core/graphics/webgl/utils/buildLine.js index ecf43f7..4d6862e 100644 --- a/src/core/graphics/webgl/utils/buildLine.js +++ b/src/core/graphics/webgl/utils/buildLine.js @@ -11,7 +11,29 @@ * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties * @param {object} webGLData - an object containing all the webGL-specific information to create this shape */ -export default function buildLine(graphicsData, webGLData) +export default function (graphicsData, webGLData) +{ + if (graphicsData.nativeLines) + { + buildNativeLine(graphicsData, webGLData); + } + else + { + buildLine(graphicsData, webGLData); + } +} + +/** + * Builds a line to draw using the poligon method. + * + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @private + * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties + * @param {object} webGLData - an object containing all the webGL-specific information to create this shape + */ +function buildLine(graphicsData, webGLData) { // TODO OPTIMISE! let points = graphicsData.points; @@ -224,3 +246,46 @@ indices.push(indexStart - 1); } + +/** + * Builds a line to draw using the gl.drawArrays(gl.LINES) method + * + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @private + * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties + * @param {object} webGLData - an object containing all the webGL-specific information to create this shape + */ +function buildNativeLine(graphicsData, webGLData) +{ + let i = 0; + const points = graphicsData.points; + + if (points.length === 0) return; + + const verts = webGLData.points; + const length = points.length / 2; + + // sort color + const color = hex2rgb(graphicsData.lineColor); + const alpha = graphicsData.lineAlpha; + const r = color[0] * alpha; + const g = color[1] * alpha; + const b = color[2] * alpha; + + for (i = 1; i < length; i++) + { + const p1x = points[(i - 1) * 2]; + const p1y = points[((i - 1) * 2) + 1]; + + const p2x = points[i * 2]; + const p2y = points[(i * 2) + 1]; + + verts.push(p1x, p1y); + verts.push(r, g, b, alpha); + + verts.push(p2x, p2y); + verts.push(r, g, b, alpha); + } +} diff --git a/src/core/graphics/webgl/utils/buildRoundedRectangle.js b/src/core/graphics/webgl/utils/buildRoundedRectangle.js index 9abc807..a6c8461 100644 --- a/src/core/graphics/webgl/utils/buildRoundedRectangle.js +++ b/src/core/graphics/webgl/utils/buildRoundedRectangle.js @@ -77,6 +77,26 @@ } /** + * Calculate a single point for a quadratic bezier curve. + * Utility function used by quadraticBezierCurve. + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @private + * @param {number} n1 - first number + * @param {number} n2 - second number + * @param {number} perc - percentage + * @return {number} the result + * + */ +function getPt(n1, n2, perc) +{ + const diff = n2 - n1; + + return n1 + (diff * perc); +} + +/** * Calculate the points for a quadratic bezier curve. (helper function..) * Based on: https://stackoverflow.com/questions/785097/how-do-i-implement-a-bezier-curve-in-c * @@ -105,13 +125,6 @@ let x = 0; let y = 0; - function getPt(n1, n2, perc) - { - const diff = n2 - n1; - - return n1 + (diff * perc); - } - for (let i = 0, j = 0; i <= n; ++i) { j = i / n; diff --git a/src/core/index.js b/src/core/index.js index f7a817c..4f414e8 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -30,6 +30,7 @@ export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; export { default as CanvasGraphicsRenderer } from './graphics/canvas/CanvasGraphicsRenderer'; +export { default as Spritesheet } from './textures/Spritesheet'; export { default as Texture } from './textures/Texture'; export { default as BaseTexture } from './textures/BaseTexture'; export { default as RenderTexture } from './textures/RenderTexture'; diff --git a/src/core/math/Matrix.js b/src/core/math/Matrix.js index 0b77477..3679860 100644 --- a/src/core/math/Matrix.js +++ b/src/core/math/Matrix.js @@ -13,45 +13,50 @@ export default class Matrix { /** - * + * @param {number} [a=1] - x scale + * @param {number} [b=0] - y skew + * @param {number} [c=0] - x skew + * @param {number} [d=1] - y scale + * @param {number} [tx=0] - x translation + * @param {number} [ty=0] - y translation */ - constructor() + constructor(a = 1, b = 0, c = 0, d = 1, tx = 0, ty = 0) { /** * @member {number} * @default 1 */ - this.a = 1; + this.a = a; /** * @member {number} * @default 0 */ - this.b = 0; + this.b = b; /** * @member {number} * @default 0 */ - this.c = 0; + this.c = c; /** * @member {number} * @default 1 */ - this.d = 1; + this.d = d; /** * @member {number} * @default 0 */ - this.tx = 0; + this.tx = tx; /** * @member {number} * @default 0 */ - this.ty = 0; + this.ty = ty; this.array = null; } diff --git a/src/core/renderers/SystemRenderer.js b/src/core/renderers/SystemRenderer.js index fa116a6..a24c775 100644 --- a/src/core/renderers/SystemRenderer.js +++ b/src/core/renderers/SystemRenderer.js @@ -1,5 +1,5 @@ import { sayHello, hex2string, hex2rgb } from '../utils'; -import { Matrix } from '../math'; +import { Matrix, Rectangle } from '../math'; import { RENDERER_TYPE } from '../const'; import settings from '../settings'; import Container from '../display/Container'; @@ -21,8 +21,8 @@ { /** * @param {string} system - The name of the system this renderer is for. - * @param {number} [width=800] - the width of the canvas view - * @param {number} [height=600] - the height of the canvas view + * @param {number} [screenWidth=800] - the width of the screen + * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false @@ -37,7 +37,7 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(system, width, height, options) + constructor(system, screenWidth, screenHeight, options) { super(); @@ -69,20 +69,13 @@ this.type = RENDERER_TYPE.UNKNOWN; /** - * The width of the canvas view + * Measurements of the screen. (0, 0, screenWidth, screenHeight) * - * @member {number} - * @default 800 - */ - this.width = width || 800; - - /** - * The height of the canvas view + * Its safe to use as filterArea or hitArea for whole stage * - * @member {number} - * @default 600 + * @member {PIXI.Rectangle} */ - this.height = height || 600; + this.screen = new Rectangle(0, 0, screenWidth || 800, screenHeight || 600); /** * The canvas element that everything is drawn to @@ -107,7 +100,7 @@ this.transparent = options.transparent; /** - * Whether the render view should be resized automatically + * Whether css dimensions of canvas view should be resized to screen dimensions automatically * * @member {boolean} */ @@ -192,23 +185,48 @@ } /** - * Resizes the canvas view to the specified width and height + * Same as view.width, actual number of pixels in the canvas by horizontal * - * @param {number} width - the new width of the canvas view - * @param {number} height - the new height of the canvas view + * @member {number} + * @readonly + * @default 800 */ - resize(width, height) + get width() { - this.width = width * this.resolution; - this.height = height * this.resolution; + return this.view.width; + } - this.view.width = this.width; - this.view.height = this.height; + /** + * Same as view.height, actual number of pixels in the canvas by vertical + * + * @member {number} + * @readonly + * @default 600 + */ + get height() + { + return this.view.height; + } + + /** + * Resizes the screen and canvas to the specified width and height + * Canvas dimensions are multiplied by resolution + * + * @param {number} screenWidth - the new width of the screen + * @param {number} screenHeight - the new height of the screen + */ + resize(screenWidth, screenHeight) + { + this.screen.width = screenWidth; + this.screen.height = screenHeight; + + this.view.width = screenWidth * this.resolution; + this.view.height = screenHeight * this.resolution; if (this.autoResize) { - this.view.style.width = `${this.width / this.resolution}px`; - this.view.style.height = `${this.height / this.resolution}px`; + this.view.style.width = `${screenWidth}px`; + this.view.style.height = `${screenHeight}px`; } } @@ -249,11 +267,10 @@ this.type = RENDERER_TYPE.UNKNOWN; - this.width = 0; - this.height = 0; - this.view = null; + this.screen = null; + this.resolution = 0; this.transparent = false; diff --git a/src/core/renderers/canvas/CanvasRenderer.js b/src/core/renderers/canvas/CanvasRenderer.js index 1bd2d5e..e84e49c 100644 --- a/src/core/renderers/canvas/CanvasRenderer.js +++ b/src/core/renderers/canvas/CanvasRenderer.js @@ -18,8 +18,8 @@ export default class CanvasRenderer extends SystemRenderer { /** - * @param {number} [width=800] - the width of the canvas view - * @param {number} [height=600] - the height of the canvas view + * @param {number} [screenWidth=800] - the width of the screen + * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false @@ -34,9 +34,9 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(width, height, options = {}) + constructor(screenWidth, screenHeight, options = {}) { - super('Canvas', width, height, options); + super('Canvas', screenWidth, screenHeight, options); this.type = RENDERER_TYPE.CANVAS; @@ -96,7 +96,7 @@ this.context = null; this.renderingToScreen = false; - this.resize(width, height); + this.resize(screenWidth, screenHeight); } /** @@ -283,12 +283,12 @@ * * @extends PIXI.SystemRenderer#resize * - * @param {number} width - The new width of the canvas view - * @param {number} height - The new height of the canvas view + * @param {number} screenWidth - the new width of the screen + * @param {number} screenHeight - the new height of the screen */ - resize(width, height) + resize(screenWidth, screenHeight) { - super.resize(width, height); + super.resize(screenWidth, screenHeight); // reset the scale mode.. oddly this seems to be reset when the canvas is resized. // surely a browser bug?? Let pixi fix that for you.. diff --git a/src/core/renderers/webgl/TextureManager.js b/src/core/renderers/webgl/TextureManager.js index b685c1a..8227289 100644 --- a/src/core/renderers/webgl/TextureManager.js +++ b/src/core/renderers/webgl/TextureManager.js @@ -61,7 +61,7 @@ * Updates and/or Creates a WebGL texture for the renderer's context. * * @param {PIXI.BaseTexture|PIXI.Texture} texture - the texture to update - * @param {Number} location - the location the texture will be bound to. + * @param {number} location - the location the texture will be bound to. * @return {GLTexture} The gl texture. */ updateTexture(texture, location) diff --git a/src/core/renderers/webgl/WebGLRenderer.js b/src/core/renderers/webgl/WebGLRenderer.js index e4cd8a0..830b382 100644 --- a/src/core/renderers/webgl/WebGLRenderer.js +++ b/src/core/renderers/webgl/WebGLRenderer.js @@ -31,8 +31,8 @@ { /** * - * @param {number} [width=0] - the width of the canvas view - * @param {number} [height=0] - the height of the canvas view + * @param {number} [screenWidth=800] - the width of the screen + * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false @@ -53,9 +53,9 @@ * @param {boolean} [options.legacy=false] - If true Pixi will aim to ensure compatibility * with older / less advanced devices. If you experiance unexplained flickering try setting this to true. */ - constructor(width, height, options = {}) + constructor(screenWidth, screenHeight, options = {}) { - super('WebGL', width, height, options); + super('WebGL', screenWidth, screenHeight, options); this.legacy = !!options.legacy; @@ -245,7 +245,7 @@ glCore._testingContext = gl; // setup the width/height properties and gl viewport - this.resize(this.width, this.height); + this.resize(this.screen.width, this.screen.height); } /** @@ -343,16 +343,16 @@ /** * Resizes the webGL view to the specified width and height. * - * @param {number} width - the new width of the webGL view - * @param {number} height - the new height of the webGL view + * @param {number} screenWidth - the new width of the screen + * @param {number} screenHeight - the new height of the screen */ - resize(width, height) + resize(screenWidth, screenHeight) { // if(width * this.resolution === this.width && height * this.resolution === this.height)return; - SystemRenderer.prototype.resize.call(this, width, height); + SystemRenderer.prototype.resize.call(this, screenWidth, screenHeight); - this.rootRenderTarget.resize(width, height); + this.rootRenderTarget.resize(screenWidth, screenHeight); if (this._activeRenderTarget === this.rootRenderTarget) { @@ -396,6 +396,26 @@ } /** + * Erases the render texture and fills the drawing area with a colour + * + * @param {PIXI.RenderTexture} renderTexture - The render texture to clear + * @param {number} [clearColor] - The colour + * @return {PIXI.WebGLRenderer} Returns itself. + */ + clearRenderTexture(renderTexture, clearColor) + { + const baseTexture = renderTexture.baseTexture; + const renderTarget = baseTexture._glRenderTargets[this.CONTEXT_UID]; + + if (renderTarget) + { + renderTarget.clear(clearColor); + } + + return this; + } + + /** * Binds a render texture for rendering * * @param {PIXI.RenderTexture} renderTexture - The render texture to render @@ -474,9 +494,10 @@ * Changes the current GLShader to the one given in parameter * * @param {PIXI.glCore.Shader} shader - the new glShader + * @param {boolean} [autoProject=true] - Whether automatically set the projection matrix * @return {PIXI.WebGLRenderer} Returns itself. */ - _bindGLShader(shader) + _bindGLShader(shader, autoProject) { // TODO cache if (this._activeShader !== shader) @@ -484,8 +505,14 @@ this._activeShader = shader; shader.bind(); - // automatically set the projection matrix - shader.uniforms.projectionMatrix = this._activeRenderTarget.projectionMatrix.toArray(true); + // `autoProject` normally would be a default parameter set to true + // but because of how Babel transpiles default parameters + // it hinders the performance of this method. + if (autoProject !== false) + { + // automatically set the projection matrix + shader.uniforms.projectionMatrix = this._activeRenderTarget.projectionMatrix.toArray(true); + } } return this; diff --git a/src/core/renderers/webgl/WebGLState.js b/src/core/renderers/webgl/WebGLState.js index ba6ecc5..6c846d5 100755 --- a/src/core/renderers/webgl/WebGLState.js +++ b/src/core/renderers/webgl/WebGLState.js @@ -82,18 +82,20 @@ push() { // next state.. - let state = this.stack[++this.stackIndex]; + let state = this.stack[this.stackIndex]; if (!state) { state = this.stack[this.stackIndex] = new Uint8Array(16); } + ++this.stackIndex; + // copy state.. // set active state so we can force overrides of gl state for (let i = 0; i < this.activeState.length; i++) { - this.activeState[i] = state[i]; + state[i] = this.activeState[i]; } } diff --git a/src/core/renderers/webgl/filters/Filter.js b/src/core/renderers/webgl/filters/Filter.js index 2c3eaad..4eb6128 100644 --- a/src/core/renderers/webgl/filters/Filter.js +++ b/src/core/renderers/webgl/filters/Filter.js @@ -9,7 +9,7 @@ * @memberof PIXI * @extends PIXI.Shader */ -class Filter extends Shader +export default class Filter extends Shader { /** * @param {string} [vertexSrc] - The source of the vertex shader. @@ -56,15 +56,16 @@ * @param {PIXI.RenderTarget} input - The input render target. * @param {PIXI.RenderTarget} output - The target to output to. * @param {boolean} clear - Should the output be cleared before rendering to it + * @param {object} [currentState] - It's current state of filter. + * There are some useful properties in the currentState : + * target, filters, sourceFrame, destinationFrame, renderTarget, resolution */ - apply(filterManager, input, output, clear) + apply(filterManager, input, output, clear, currentState) // eslint-disable-line no-unused-vars { // do as you please! - filterManager.applyFilter(this, input, output, clear); + filterManager.applyFilter(this, input, output, clear, currentState); // or just do a regular render.. } } - -export default Filter; diff --git a/src/core/renderers/webgl/filters/extractUniformsFromSrc.js b/src/core/renderers/webgl/filters/extractUniformsFromSrc.js index d78a0b1..4893214 100644 --- a/src/core/renderers/webgl/filters/extractUniformsFromSrc.js +++ b/src/core/renderers/webgl/filters/extractUniformsFromSrc.js @@ -12,7 +12,7 @@ function extractUniformsFromString(string) { - const maskRegex = new RegExp('^(projectionMatrix|uSampler|filterArea)$'); + const maskRegex = new RegExp('^(projectionMatrix|uSampler|filterArea|filterClamp)$'); const uniforms = {}; let nameSplit; diff --git a/src/core/renderers/webgl/managers/FilterManager.js b/src/core/renderers/webgl/managers/FilterManager.js index 43fc869..6c7422d 100644 --- a/src/core/renderers/webgl/managers/FilterManager.js +++ b/src/core/renderers/webgl/managers/FilterManager.js @@ -2,6 +2,7 @@ import RenderTarget from '../utils/RenderTarget'; import Quad from '../utils/Quad'; import { Rectangle } from '../../../math'; +import Shader from '../../../Shader'; import * as filterTransforms from '../filters/filterTransforms'; import bitTwiddle from 'bit-twiddle'; @@ -22,8 +23,6 @@ this.filters = []; this.target = null; this.resolution = 1; - - this.firstRun = true; } } @@ -155,7 +154,7 @@ if (filters.length === 1) { - filters[0].apply(this, currentState.renderTarget, lastState.renderTarget, false); + filters[0].apply(this, currentState.renderTarget, lastState.renderTarget, false, currentState); this.freePotRenderTarget(currentState.renderTarget); } else @@ -177,7 +176,7 @@ for (i = 0; i < filters.length - 1; ++i) { - filters[i].apply(this, flip, flop, true); + filters[i].apply(this, flip, flop, true, currentState); const t = flip; @@ -185,7 +184,7 @@ flop = t; } - filters[i].apply(this, flip, lastState.renderTarget, false); + filters[i].apply(this, flip, lastState.renderTarget, false, currentState); this.freePotRenderTarget(flip); this.freePotRenderTarget(flop); @@ -212,16 +211,28 @@ const renderer = this.renderer; const gl = renderer.gl; - renderer.bindShader(filter, true); + let shader = filter.glShaders[renderer.CONTEXT_UID]; - // TODO theres a better way! - // quad can be a mesh and we then should be able to pull this off without accessing the shader directly - const shader = renderer.shaderManager.getGLShader(); - - // TODO change quad to mesh.. - if (!this.firstRun) + // cacheing.. + if (!shader) { - this.firstRun = true; + if (filter.glShaderKey) + { + shader = this.shaderCache[filter.glShaderKey]; + + if (!shader) + { + shader = new Shader(this.gl, filter.vertexSrc, filter.fragmentSrc); + + filter.glShaders[renderer.CONTEXT_UID] = this.shaderCache[filter.glShaderKey] = shader; + } + } + else + { + shader = filter.glShaders[renderer.CONTEXT_UID] = new Shader(this.gl, filter.vertexSrc, filter.fragmentSrc); + } + + // TODO - this only needs to be done once? renderer.bindVao(null); this.quad.initVao(shader); @@ -244,58 +255,146 @@ renderer.maskManager.pushScissorMask(null, renderer.maskManager.scissorData); } - // renderer._bindGLShader(shader); + renderer.bindShader(shader); - let currentState; - const uniforms = filter.uniforms; + // free unit 0 for us, doesn't matter what was there + // don't try to restore it, because syncUniforms can upload it to another slot + // and it'll be a problem + const tex = this.renderer.emptyTextures[0]; - if (uniforms.filterArea) - { - currentState = this.filterData.stack[this.filterData.index]; - - const filterArea = uniforms.filterArea; - - filterArea[0] = currentState.renderTarget.size.width; - filterArea[1] = currentState.renderTarget.size.height; - filterArea[2] = currentState.sourceFrame.x; - filterArea[3] = currentState.sourceFrame.y; - - filter.uniforms.filterArea = filterArea; - } - - // use this to clamp displaced texture coords so they belong to filterArea - // see displacementFilter fragment shader for an example - if (uniforms.filterClamp) - { - currentState = this.filterData.stack[this.filterData.index]; - - const filterClamp = uniforms.filterClamp; - - filterClamp[0] = 0; - filterClamp[1] = 0; - filterClamp[2] = (currentState.sourceFrame.width - 1) / currentState.renderTarget.size.width; - filterClamp[3] = (currentState.sourceFrame.height - 1) / currentState.renderTarget.size.height; - - filter.uniforms.filterClamp = filterClamp; - } - - renderer.shaderManager.setUniforms(filter.uniforms); + this.renderer.boundTextures[0] = tex; + // this syncs the pixi filters uniforms with glsl uniforms + this.syncUniforms(shader, filter); renderer.state.setBlendMode(filter.blendMode); - // temporary bypass cache.. - const tex = this.renderer.boundTextures[0]; - gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, input.texture.texture); this.quad.vao.draw(this.renderer.gl.TRIANGLES, 6, 0); - // restore cache. gl.bindTexture(gl.TEXTURE_2D, tex._glTextures[this.renderer.CONTEXT_UID].texture); } /** + * Uploads the uniforms of the filter. + * + * @param {GLShader} shader - The underlying gl shader. + * @param {PIXI.Filter} filter - The filter we are synchronizing. + */ + syncUniforms(shader, filter) + { + const uniformData = filter.uniformData; + const uniforms = filter.uniforms; + + // 0 is reserved for the pixi texture so we start at 1! + let textureCount = 1; + let currentState; + + // filterArea and filterClamp that are handled by FilterManager directly + // they must not appear in uniformData + + if (shader.uniforms.filterArea) + { + currentState = this.filterData.stack[this.filterData.index]; + + const filterArea = shader.uniforms.filterArea; + + filterArea[0] = currentState.renderTarget.size.width; + filterArea[1] = currentState.renderTarget.size.height; + filterArea[2] = currentState.sourceFrame.x; + filterArea[3] = currentState.sourceFrame.y; + + shader.uniforms.filterArea = filterArea; + } + + // use this to clamp displaced texture coords so they belong to filterArea + // see displacementFilter fragment shader for an example + if (shader.uniforms.filterClamp) + { + currentState = currentState || this.filterData.stack[this.filterData.index]; + + const filterClamp = shader.uniforms.filterClamp; + + filterClamp[0] = 0; + filterClamp[1] = 0; + filterClamp[2] = (currentState.sourceFrame.width - 1) / currentState.renderTarget.size.width; + filterClamp[3] = (currentState.sourceFrame.height - 1) / currentState.renderTarget.size.height; + + shader.uniforms.filterClamp = filterClamp; + } + + // TODO Cacheing layer.. + for (const i in uniformData) + { + if (uniformData[i].type === 'sampler2D' && uniforms[i] !== 0) + { + if (uniforms[i].baseTexture) + { + shader.uniforms[i] = this.renderer.bindTexture(uniforms[i].baseTexture, textureCount); + } + else + { + shader.uniforms[i] = textureCount; + + // TODO + // this is helpful as renderTargets can also be set. + // Although thinking about it, we could probably + // make the filter texture cache return a RenderTexture + // rather than a renderTarget + const gl = this.renderer.gl; + + this.renderer.boundTextures[textureCount] = this.renderer.emptyTextures[textureCount]; + gl.activeTexture(gl.TEXTURE0 + textureCount); + + uniforms[i].texture.bind(); + } + + textureCount++; + } + else if (uniformData[i].type === 'mat3') + { + // check if its pixi matrix.. + if (uniforms[i].a !== undefined) + { + shader.uniforms[i] = uniforms[i].toArray(true); + } + else + { + shader.uniforms[i] = uniforms[i]; + } + } + else if (uniformData[i].type === 'vec2') + { + // check if its a point.. + if (uniforms[i].x !== undefined) + { + const val = shader.uniforms[i] || new Float32Array(2); + + val[0] = uniforms[i].x; + val[1] = uniforms[i].y; + shader.uniforms[i] = val; + } + else + { + shader.uniforms[i] = uniforms[i]; + } + } + else if (uniformData[i].type === 'float') + { + if (shader.uniforms.data[i].value !== uniformData[i]) + { + shader.uniforms[i] = uniforms[i]; + } + } + else + { + shader.uniforms[i] = uniforms[i]; + } + } + } + + /** * Gets a render target from the pool, or creates a new one. * * @param {boolean} clear - Should we clear the render texture when we get it? @@ -442,7 +541,6 @@ renderTarget.resolution = resolution; renderTarget.defaultFrame.width = renderTarget.size.width = minWidth / resolution; renderTarget.defaultFrame.height = renderTarget.size.height = minHeight / resolution; - renderTarget.attachStencilBuffer(); return renderTarget; } diff --git a/src/core/settings.js b/src/core/settings.js index bb08cb0..f4dd087 100644 --- a/src/core/settings.js +++ b/src/core/settings.js @@ -77,11 +77,11 @@ * * @static * @memberof PIXI.settings - * @type {RegExp|string} + * @type {RegExp} * @example `@2x` - * @default /@(.+)x/ + * @default /@([0-9\.]+)x/ */ - RETINA_PREFIX: /@(.+)x/, + RETINA_PREFIX: /@([0-9\.]+)x/, /** * The default render options if none are supplied to {@link PIXI.WebGLRenderer} @@ -175,14 +175,24 @@ SCALE_MODE: 0, /** - * Default specify float precision in shaders. + * Default specify float precision in vertex shader. + * + * @static + * @memberof PIXI.settings + * @type {PIXI.PRECISION} + * @default PIXI.PRECISION.HIGH + */ + PRECISION_VERTEX: 'highp', + + /** + * Default specify float precision in fragment shader. * * @static * @memberof PIXI.settings * @type {PIXI.PRECISION} * @default PIXI.PRECISION.MEDIUM */ - PRECISION: 'mediump', + PRECISION_FRAGMENT: 'mediump', /** * Can we upload the same buffer in a single frame? diff --git a/src/core/sprites/Sprite.js b/src/core/sprites/Sprite.js index 28b0b18..121bf58 100644 --- a/src/core/sprites/Sprite.js +++ b/src/core/sprites/Sprite.js @@ -212,11 +212,11 @@ } else { - w0 = orig.width * (1 - anchor._x); - w1 = orig.width * -anchor._x; + w1 = -anchor._x * orig.width; + w0 = w1 + orig.width; - h0 = orig.height * (1 - anchor._y); - h1 = orig.height * -anchor._y; + h1 = -anchor._y * orig.height; + h0 = h1 + orig.height; } // xy @@ -269,11 +269,11 @@ const tx = wt.tx; const ty = wt.ty; - const w0 = (orig.width) * (1 - anchor._x); - const w1 = (orig.width) * -anchor._x; + const w1 = -anchor._x * orig.width; + const w0 = w1 + orig.width; - const h0 = orig.height * (1 - anchor._y); - const h1 = orig.height * -anchor._y; + const h1 = -anchor._y * orig.height; + const h0 = h1 + orig.height; // xy vertexData[0] = (a * w1) + (c * h1) + tx; @@ -440,7 +440,7 @@ * * @static * @param {number|string|PIXI.BaseTexture|HTMLCanvasElement|HTMLVideoElement} source Source to create texture from - * @return {PIXI.Sprite} The newly created texture + * @return {PIXI.Sprite} The newly created sprite */ static from(source) { @@ -538,8 +538,8 @@ } /** - * The tint applied to the sprite. This is a hex value. A value of - * 0xFFFFFF will remove any tint effect. + * The tint applied to the sprite. This is a hex value. + * A value of 0xFFFFFF will remove any tint effect. * * @member {number} * @default 0xFFFFFF diff --git a/src/core/sprites/canvas/CanvasTinter.js b/src/core/sprites/canvas/CanvasTinter.js index 1cf316b..27e5e0c 100644 --- a/src/core/sprites/canvas/CanvasTinter.js +++ b/src/core/sprites/canvas/CanvasTinter.js @@ -75,8 +75,8 @@ crop.width *= resolution; crop.height *= resolution; - canvas.width = crop.width; - canvas.height = crop.height; + canvas.width = Math.ceil(crop.width); + canvas.height = Math.ceil(crop.height); context.fillStyle = `#${(`00000${(color | 0).toString(16)}`).substr(-6)}`; @@ -130,8 +130,8 @@ crop.width *= resolution; crop.height *= resolution; - canvas.width = crop.width; - canvas.height = crop.height; + canvas.width = Math.ceil(crop.width); + canvas.height = Math.ceil(crop.height); context.globalCompositeOperation = 'copy'; context.fillStyle = `#${(`00000${(color | 0).toString(16)}`).substr(-6)}`; @@ -172,8 +172,8 @@ crop.width *= resolution; crop.height *= resolution; - canvas.width = crop.width; - canvas.height = crop.height; + canvas.width = Math.ceil(crop.width); + canvas.height = Math.ceil(crop.height); context.globalCompositeOperation = 'copy'; context.drawImage( diff --git a/src/core/sprites/webgl/SpriteRenderer.js b/src/core/sprites/webgl/SpriteRenderer.js index 2affcd5..7c352e2 100644 --- a/src/core/sprites/webgl/SpriteRenderer.js +++ b/src/core/sprites/webgl/SpriteRenderer.js @@ -30,7 +30,7 @@ /** * Number of values sent in the vertex buffer. - * positionX, positionY, colorR, colorG, colorB = 5 + * aVertexPosition(2), aTextureCoord(1), aColor(1), aTextureId(1) = 5 * * @member {number} */ @@ -75,7 +75,6 @@ this.shader = null; this.currentIndex = 0; - TICK = 0; this.groups = []; for (let k = 0; k < this.size; k++) diff --git a/src/core/textures/BaseTexture.js b/src/core/textures/BaseTexture.js index 853de7e..aeccf39 100644 --- a/src/core/textures/BaseTexture.js +++ b/src/core/textures/BaseTexture.js @@ -232,16 +232,24 @@ this.realWidth = this.source.naturalWidth || this.source.videoWidth || this.source.width; this.realHeight = this.source.naturalHeight || this.source.videoHeight || this.source.height; - this.width = this.realWidth / this.resolution; - this.height = this.realHeight / this.resolution; - - this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); + this._updateDimensions(); } this.emit('update', this); } /** + * Update dimensions from real values + */ + _updateDimensions() + { + this.width = this.realWidth / this.resolution; + this.height = this.realHeight / this.resolution; + + this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); + } + + /** * Load a source. * * If the source is not-immediately-available, such as an image that needs to be @@ -525,11 +533,7 @@ this.realWidth = Math.round(svgWidth * this.sourceScale); this.realHeight = Math.round(svgHeight * this.sourceScale); - this.width = this.realWidth / this.resolution; - this.height = this.realHeight / this.resolution; - - // Check pow2 after scale - this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); + this._updateDimensions(); // Create a canvas element const canvas = document.createElement('canvas'); @@ -690,4 +694,52 @@ return baseTexture; } + + /** + * Helper function that creates a base texture based on the source you provide. + * The source can be - image url, image element, canvas element. + * + * @static + * @param {string|HTMLImageElement|HTMLCanvasElement} source - The source to create base texture from. + * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values + * @param {number} [sourceScale=(auto)] - Scale for the original image, used with Svg images. + * @return {PIXI.BaseTexture} The new base texture. + */ + static from(source, scaleMode, sourceScale) + { + if (typeof source === 'string') + { + return BaseTexture.fromImage(source, undefined, scaleMode, sourceScale); + } + else if (source instanceof HTMLImageElement) + { + const imageUrl = source.src; + let baseTexture = BaseTextureCache[imageUrl]; + + if (!baseTexture) + { + baseTexture = new BaseTexture(source, scaleMode); + baseTexture.imageUrl = imageUrl; + + if (sourceScale) + { + baseTexture.sourceScale = sourceScale; + } + + // if there is an @2x at the end of the url we are going to assume its a highres image + baseTexture.resolution = getResolutionOfUrl(imageUrl); + + BaseTextureCache[imageUrl] = baseTexture; + } + + return baseTexture; + } + else if (source instanceof HTMLCanvasElement) + { + return BaseTexture.fromCanvas(source, scaleMode); + } + + // lets assume its a base texture! + return source; + } } diff --git a/src/core/textures/Spritesheet.js b/src/core/textures/Spritesheet.js new file mode 100644 index 0000000..d469c62 --- /dev/null +++ b/src/core/textures/Spritesheet.js @@ -0,0 +1,274 @@ +import { Rectangle, Texture } from '../'; +import { getResolutionOfUrl, TextureCache } from '../utils'; + +/** + * Utility class for maintaining reference to a collection + * of Textures on a single Spritesheet. + * + * @class + * @memberof PIXI + */ +export default class Spritesheet +{ + /** + * The maximum number of Textures to build per process. + * + * @type {number} + * @default 1000 + */ + static get BATCH_SIZE() + { + return 1000; + } + + /** + * @param {PIXI.BaseTexture} baseTexture Reference to the source BaseTexture object. + * @param {Object} data - Spritesheet image data. + * @param {string} [resolutionFilename] - The filename to consider when determining + * the resolution of the spritesheet. If not provided, the imageUrl will + * be used on the BaseTexture. + */ + constructor(baseTexture, data, resolutionFilename = null) + { + /** + * Reference to ths source texture + * @type {PIXI.BaseTexture} + */ + this.baseTexture = baseTexture; + + /** + * Map of spritesheet textures. + * @type {Object} + */ + this.textures = {}; + + /** + * Reference to the original JSON data. + * @type {Object} + */ + this.data = data; + + /** + * The resolution of the spritesheet. + * @type {number} + */ + this.resolution = this._updateResolution( + resolutionFilename || this.baseTexture.imageUrl + ); + + /** + * Map of spritesheet frames. + * @type {Object} + * @private + */ + this._frames = this.data.frames; + + /** + * Collection of frame names. + * @type {string[]} + * @private + */ + this._frameKeys = Object.keys(this._frames); + + /** + * Current batch index being processed. + * @type {number} + * @private + */ + this._batchIndex = 0; + + /** + * Callback when parse is completed. + * @type {Function} + * @private + */ + this._callback = null; + } + + /** + * Generate the resolution from the filename or fallback + * to the meta.scale field of the JSON data. + * + * @private + * @param {string} resolutionFilename - The filename to use for resolving + * the default resolution. + * @return {number} Resolution to use for spritesheet. + */ + _updateResolution(resolutionFilename) + { + const scale = this.data.meta.scale; + + // Use a defaultValue of `null` to check if a url-based resolution is set + let resolution = getResolutionOfUrl(resolutionFilename, null); + + // No resolution found via URL + if (resolution === null) + { + // Use the scale value or default to 1 + resolution = scale !== undefined ? parseFloat(scale) : 1; + } + + // For non-1 resolutions, update baseTexture + if (resolution !== 1) + { + this.baseTexture.resolution = resolution; + this.baseTexture.update(); + } + + return resolution; + } + + /** + * Parser spritesheet from loaded data. This is done asynchronously + * to prevent creating too many Texture within a single process. + * + * @param {Function} callback - Callback when complete returns + * a map of the Textures for this spritesheet. + */ + parse(callback) + { + this._batchIndex = 0; + this._callback = callback; + + if (this._frameKeys.length <= Spritesheet.BATCH_SIZE) + { + this._processFrames(0); + this._parseComplete(); + } + else + { + this._nextBatch(); + } + } + + /** + * Process a batch of frames + * + * @private + * @param {number} initialFrameIndex - The index of frame to start. + */ + _processFrames(initialFrameIndex) + { + let frameIndex = initialFrameIndex; + const maxFrames = Spritesheet.BATCH_SIZE; + + while (frameIndex - initialFrameIndex < maxFrames && frameIndex < this._frameKeys.length) + { + const i = this._frameKeys[frameIndex]; + const rect = this._frames[i].frame; + + if (rect) + { + let frame = null; + let trim = null; + const orig = new Rectangle( + 0, + 0, + this._frames[i].sourceSize.w / this.resolution, + this._frames[i].sourceSize.h / this.resolution + ); + + if (this._frames[i].rotated) + { + frame = new Rectangle( + rect.x / this.resolution, + rect.y / this.resolution, + rect.h / this.resolution, + rect.w / this.resolution + ); + } + else + { + frame = new Rectangle( + rect.x / this.resolution, + rect.y / this.resolution, + rect.w / this.resolution, + rect.h / this.resolution + ); + } + + // Check to see if the sprite is trimmed + if (this._frames[i].trimmed) + { + trim = new Rectangle( + this._frames[i].spriteSourceSize.x / this.resolution, + this._frames[i].spriteSourceSize.y / this.resolution, + rect.w / this.resolution, + rect.h / this.resolution + ); + } + + this.textures[i] = new Texture( + this.baseTexture, + frame, + orig, + trim, + this._frames[i].rotated ? 2 : 0 + ); + + // lets also add the frame to pixi's global cache for fromFrame and fromImage functions + TextureCache[i] = this.textures[i]; + } + + frameIndex++; + } + } + + /** + * The parse has completed. + * + * @private + */ + _parseComplete() + { + const callback = this._callback; + + this._callback = null; + this._batchIndex = 0; + callback.call(this, this.textures); + } + + /** + * Begin the next batch of textures. + * + * @private + */ + _nextBatch() + { + this._processFrames(this._batchIndex * Spritesheet.BATCH_SIZE); + this._batchIndex++; + setTimeout(() => + { + if (this._batchIndex * Spritesheet.BATCH_SIZE < this._frameKeys.length) + { + this._nextBatch(); + } + else + { + this._parseComplete(); + } + }, 0); + } + + /** + * Destroy Spritesheet and don't use after this. + * + * @param {boolean} [destroyBase=false] Whether to destroy the base texture as well + */ + destroy(destroyBase = false) + { + for (const i in this.textures) + { + this.textures[i].destroy(); + } + this._frames = null; + this._frameKeys = null; + this.data = null; + this.textures = null; + if (destroyBase) + { + this.baseTexture.destroy(); + } + this.baseTexture = null; + } +} diff --git a/src/core/textures/Texture.js b/src/core/textures/Texture.js index 6403d4f..afe4254 100644 --- a/src/core/textures/Texture.js +++ b/src/core/textures/Texture.js @@ -3,7 +3,7 @@ import TextureUvs from './TextureUvs'; import EventEmitter from 'eventemitter3'; import { Rectangle } from '../math'; -import { TextureCache, BaseTextureCache } from '../utils'; +import { TextureCache, BaseTextureCache, getResolutionOfUrl } from '../utils'; /** * A texture stores the information that represents an image or part of an image. It cannot be added @@ -365,7 +365,8 @@ * The source can be - frame id, image url, video url, canvas element, video element, base texture * * @static - * @param {number|string|PIXI.BaseTexture|HTMLCanvasElement|HTMLVideoElement} source - Source to create texture from + * @param {number|string|HTMLImageElement|HTMLCanvasElement|HTMLVideoElement|PIXI.BaseTexture} + * source - Source to create texture from * @return {PIXI.Texture} The newly created texture */ static from(source) @@ -393,7 +394,7 @@ } else if (source instanceof HTMLImageElement) { - return new Texture(new BaseTexture(source)); + return new Texture(BaseTexture.from(source)); } else if (source instanceof HTMLCanvasElement) { @@ -413,6 +414,43 @@ } /** + * Create a texture from a source and add to the cache. + * + * @static + * @param {HTMLImageElement|HTMLCanvasElement} source - The input source. + * @param {String} imageUrl - File name of texture, for cache and resolving resolution. + * @param {String} [name] - Human readible name for the texture cache. If no name is + * specified, only `imageUrl` will be used as the cache ID. + * @return {PIXI.Texture} Output texture + */ + static fromLoader(source, imageUrl, name) + { + const baseTexture = new BaseTexture(source, null, getResolutionOfUrl(imageUrl)); + const texture = new Texture(baseTexture); + + baseTexture.imageUrl = imageUrl; + + // No name, use imageUrl instead + if (!name) + { + name = imageUrl; + } + + // lets also add the frame to pixi's global cache for fromFrame and fromImage fucntions + BaseTextureCache[name] = baseTexture; + TextureCache[name] = texture; + + // also add references by url if they are different. + if (name !== imageUrl) + { + BaseTextureCache[imageUrl] = baseTexture; + TextureCache[imageUrl] = texture; + } + + return texture; + } + + /** * Adds a texture to the global TextureCache. This cache is shared across the whole PIXI object. * * @static @@ -522,6 +560,29 @@ } } +function createWhiteTexture() +{ + const canvas = document.createElement('canvas'); + + canvas.width = 10; + canvas.height = 10; + + const context = canvas.getContext('2d'); + + context.fillStyle = 'white'; + context.fillRect(0, 0, 10, 10); + + return new Texture(new BaseTexture(canvas)); +} + +function removeAllHandlers(tex) +{ + tex.destroy = function _emptyDestroy() { /* empty */ }; + tex.on = function _emptyOn() { /* empty */ }; + tex.once = function _emptyOnce() { /* empty */ }; + tex.emit = function _emptyEmit() { /* empty */ }; +} + /** * An empty texture, used often to not have to create multiple empty textures. * Can not be destroyed. @@ -530,7 +591,14 @@ * @constant */ Texture.EMPTY = new Texture(new BaseTexture()); -Texture.EMPTY.destroy = function _emptyDestroy() { /* empty */ }; -Texture.EMPTY.on = function _emptyOn() { /* empty */ }; -Texture.EMPTY.once = function _emptyOnce() { /* empty */ }; -Texture.EMPTY.emit = function _emptyEmit() { /* empty */ }; +removeAllHandlers(Texture.EMPTY); + +/** + * A white texture of 10x10 size, used for graphics and other things + * Can not be destroyed. + * + * @static + * @constant + */ +Texture.WHITE = createWhiteTexture(); +removeAllHandlers(Texture.WHITE); diff --git a/src/core/ticker/Ticker.js b/src/core/ticker/Ticker.js index d98a1f0..17a1517 100644 --- a/src/core/ticker/Ticker.js +++ b/src/core/ticker/Ticker.js @@ -67,11 +67,12 @@ * is based, this value is neither capped nor scaled. * If the platform supports DOMHighResTimeStamp, * this value will have a precision of 1 µs. + * Defaults to target frame time * * @member {number} - * @default 1 / TARGET_FPMS + * @default 16.66 */ - this.elapsedMS = 1 / settings.TARGET_FPMS; // default to target frame time + this.elapsedMS = 1 / settings.TARGET_FPMS; /** * The last time {@link PIXI.ticker.Ticker#update} was invoked. diff --git a/src/core/utils/index.js b/src/core/utils/index.js index 6ff609f..be44750 100644 --- a/src/core/utils/index.js +++ b/src/core/utils/index.js @@ -2,6 +2,7 @@ import settings from '../settings'; import EventEmitter from 'eventemitter3'; import pluginTarget from './pluginTarget'; +import * as mixins from './mixin'; import * as isMobile from 'ismobilejs'; let nextUid = 0; @@ -33,6 +34,7 @@ * @type {mixin} */ pluginTarget, + mixins, }; /** @@ -93,7 +95,7 @@ */ export function rgb2hex(rgb) { - return (((rgb[0] * 255) << 16) + ((rgb[1] * 255) << 8) + (rgb[2] * 255)); + return (((rgb[0] * 255) << 16) + ((rgb[1] * 255) << 8) + (rgb[2] * 255 | 0)); } /** @@ -368,3 +370,43 @@ * @private */ export const ProgramCache = {}; + +/** + * Destroys all texture in the cache + * + * @memberof PIXI.utils + * @function destroyTextureCache + */ +export function destroyTextureCache() +{ + let key; + + for (key in TextureCache) + { + TextureCache[key].destroy(); + } + for (key in BaseTextureCache) + { + BaseTextureCache[key].destroy(); + } +} + +/** + * Removes all textures from cache, but does not destroy them + * + * @memberof PIXI.utils + * @function clearTextureCache + */ +export function clearTextureCache() +{ + let key; + + for (key in TextureCache) + { + delete TextureCache[key]; + } + for (key in BaseTextureCache) + { + delete BaseTextureCache[key]; + } +} diff --git a/src/core/utils/mixin.js b/src/core/utils/mixin.js new file mode 100644 index 0000000..114de2a --- /dev/null +++ b/src/core/utils/mixin.js @@ -0,0 +1,59 @@ +/** + * Mixes all enumerable properties and methods from a source object to a target object. + * + * @memberof PIXI.utils.mixins + * @function mixin + * @param {object} target The prototype or instance that properties and methods should be added to. + * @param {object} source The source of properties and methods to mix in. + */ +export function mixin(target, source) +{ + if (!target || !source) return; + // in ES8/ES2017, this would be really easy: + // Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); + + // get all the enumerable property keys + const keys = Object.keys(source); + + // loop through properties + for (let i = 0; i < keys.length; ++i) + { + const propertyName = keys[i]; + + // Set the property using the property descriptor - this works for accessors and normal value properties + Object.defineProperty(target, propertyName, Object.getOwnPropertyDescriptor(source, propertyName)); + } +} + +const mixins = []; + +/** + * Queues a mixin to be handled towards the end of the initialization of PIXI, so that deprecation + * can take effect. + * + * @memberof PIXI.utils.mixins + * @function delayMixin + * @private + * @param {object} target The prototype or instance that properties and methods should be added to. + * @param {object} source The source of properties and methods to mix in. + */ +export function delayMixin(target, source) +{ + mixins.push(target, source); +} + +/** + * Handles all mixins queued via delayMixin(). + * + * @memberof PIXI.utils.mixins + * @function performMixins + * @private + */ +export function performMixins() +{ + for (let i = 0; i < mixins.length; i += 2) + { + mixin(mixins[i], mixins[i + 1]); + } + mixins.length = 0; +} diff --git a/src/deprecation.js b/src/deprecation.js index 045a19f..b9c73b3 100644 --- a/src/deprecation.js +++ b/src/deprecation.js @@ -5,6 +5,7 @@ import * as filters from './filters'; import * as prepare from './prepare'; import * as loaders from './loaders'; +import * as interaction from './interaction'; // provide method to give a stack track for warnings // useful for tracking-down where deprecated methods/properties/classes @@ -534,7 +535,7 @@ { parent: 'GC_MODES', target: 'GC_MODE' }, { parent: 'WRAP_MODES', target: 'WRAP_MODE' }, { parent: 'SCALE_MODES', target: 'SCALE_MODE' }, - { parent: 'PRECISION', target: 'PRECISION' }, + { parent: 'PRECISION', target: 'PRECISION_FRAGMENT' }, ]; for (let i = 0; i < defaults.length; i++) @@ -558,6 +559,32 @@ }); } +Object.defineProperties(core.settings, { + + /** + * @static + * @name PRECISION + * @memberof PIXI.settings + * @see PIXI.PRECISION + * @deprecated since version 4.4.0 + */ + PRECISION: { + enumerable: true, + get() + { + warn('PIXI.settings.PRECISION has been deprecated, please use PIXI.settings.PRECISION_FRAGMENT'); + + return core.settings.PRECISION_FRAGMENT; + }, + set(value) + { + warn('PIXI.settings.PRECISION has been deprecated, please use PIXI.settings.PRECISION_FRAGMENT'); + + core.settings.PRECISION_FRAGMENT = value; + }, + }, +}); + Object.defineProperties(extras, { /** @@ -694,7 +721,7 @@ warn('determineFontProperties is now deprecated, please use the static calculateFontProperties method, ' + 'e.g : Text.calculateFontProperties(fontStyle);'); - return Text.calculateFontProperties(fontStyle); + return core.Text.calculateFontProperties(fontStyle); }; Object.defineProperties(core.TextStyle.prototype, { @@ -1003,3 +1030,69 @@ }, }, }); + +/** + * @name PIXI.interaction.interactiveTarget#defaultCursor + * @static + * @type {number} + * @see PIXI.interaction.interactiveTarget#cursor + * @deprecated since 4.3.0 + */ +Object.defineProperty(interaction.interactiveTarget, 'defaultCursor', { + set(value) + { + warn('Property defaultCursor has been replaced with \'cursor\'. '); + this.cursor = value; + }, + get() + { + warn('Property defaultCursor has been replaced with \'cursor\'. '); + + return this.cursor; + }, + enumerable: true, +}); + +/** + * @name PIXI.interaction.InteractionManager#defaultCursorStyle + * @static + * @type {string} + * @see PIXI.interaction.InteractionManager#cursorStyles + * @deprecated since 4.3.0 + */ +Object.defineProperty(interaction.InteractionManager, 'defaultCursorStyle', { + set(value) + { + warn('Property defaultCursorStyle has been replaced with \'cursorStyles.default\'. '); + this.cursorStyles.default = value; + }, + get() + { + warn('Property defaultCursorStyle has been replaced with \'cursorStyles.default\'. '); + + return this.cursorStyles.default; + }, +}); + +/** + * @name PIXI.interaction.InteractionManager#currentCursorStyle + * @static + * @type {string} + * @see PIXI.interaction.InteractionManager#cursorStyles + * @deprecated since 4.3.0 + */ +Object.defineProperty(interaction.InteractionManager, 'currentCursorStyle', { + set(value) + { + warn('Property currentCursorStyle has been removed.' + + 'See the currentCursorMode property, which works differently.'); + this.currentCursorMode = value; + }, + get() + { + warn('Property currentCursorStyle has been removed.' + + 'See the currentCursorMode property, which works differently.'); + + return this.currentCursorMode; + }, +}); diff --git a/src/extras/BitmapText.js b/src/extras/BitmapText.js index fb6c343..0a0e150 100644 --- a/src/extras/BitmapText.js +++ b/src/extras/BitmapText.js @@ -455,6 +455,73 @@ return this._textHeight; } + + /** + * Register a bitmap font with data and a texture. + * + * @static + * @param {XMLDocument} xml - The XML document data. + * @param {PIXI.Texture} texture - Texture with all symbols. + * @return {Object} Result font object with font, size, lineHeight and char fields. + */ + static registerFont(xml, texture) + { + const data = {}; + const info = xml.getElementsByTagName('info')[0]; + const common = xml.getElementsByTagName('common')[0]; + + data.font = info.getAttribute('face'); + data.size = parseInt(info.getAttribute('size'), 10); + data.lineHeight = parseInt(common.getAttribute('lineHeight'), 10); + data.chars = {}; + + // parse letters + const letters = xml.getElementsByTagName('char'); + + for (let i = 0; i < letters.length; i++) + { + const letter = letters[i]; + const charCode = parseInt(letter.getAttribute('id'), 10); + + const textureRect = new core.Rectangle( + parseInt(letter.getAttribute('x'), 10) + texture.frame.x, + parseInt(letter.getAttribute('y'), 10) + texture.frame.y, + parseInt(letter.getAttribute('width'), 10), + parseInt(letter.getAttribute('height'), 10) + ); + + data.chars[charCode] = { + xOffset: parseInt(letter.getAttribute('xoffset'), 10), + yOffset: parseInt(letter.getAttribute('yoffset'), 10), + xAdvance: parseInt(letter.getAttribute('xadvance'), 10), + kerning: {}, + texture: new core.Texture(texture.baseTexture, textureRect), + + }; + } + + // parse kernings + const kernings = xml.getElementsByTagName('kerning'); + + for (let i = 0; i < kernings.length; i++) + { + const kerning = kernings[i]; + const first = parseInt(kerning.getAttribute('first'), 10); + const second = parseInt(kerning.getAttribute('second'), 10); + const amount = parseInt(kerning.getAttribute('amount'), 10); + + if (data.chars[second]) + { + data.chars[second].kerning[first] = amount; + } + } + + // I'm leaving this as a temporary fix so we can test the bitmap fonts in v3 + // but it's very likely to change + BitmapText.fonts[data.font] = data; + + return data; + } } BitmapText.fonts = {}; diff --git a/src/index.js b/src/index.js index 59356cf..c553427 100644 --- a/src/index.js +++ b/src/index.js @@ -16,6 +16,10 @@ import * as particles from './particles'; import * as prepare from './prepare'; +// handle mixins now, after all code has been added, including deprecation +import { utils } from './core'; +utils.mixins.performMixins(); + export { accessibility, extract, diff --git a/src/interaction/InteractionManager.js b/src/interaction/InteractionManager.js index 7d51134..8baf13a 100644 --- a/src/interaction/InteractionManager.js +++ b/src/interaction/InteractionManager.js @@ -1,18 +1,20 @@ 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'; -// Mix interactiveTarget into core.DisplayObject.prototype -Object.assign( +// Mix interactiveTarget into core.DisplayObject.prototype, after deprecation has been handled +core.utils.mixins.delayMixin( core.DisplayObject.prototype, 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 +70,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 +101,6 @@ this.eventData = new InteractionEvent(); /** - * Tiny little interactiveData pool ! - * - * @member {PIXI.interaction.InteractionData[]} - */ - this.interactiveDataPool = []; - - /** * The DOM element to bind to. * * @private @@ -153,66 +155,12 @@ */ this.supportsPointerEvents = !!window.PointerEvent; - /** - * Are touch events being 'normalized' and converted into pointer events if pointer events are not supported - * For example, on a touch screen mobile device, a touchstart would also be emitted as a pointerdown - * - * @private - * @readonly - * @member {boolean} - */ - this.normalizeTouchEvents = !this.supportsPointerEvents && this.supportsTouchEvents; - - /** - * Are mouse events being 'normalized' and converted into pointer events if pointer events are not supported - * For example, on a desktop pc, a mousedown would also be emitted as a pointerdown - * - * @private - * @readonly - * @member {boolean} - */ - this.normalizeMouseEvents = !this.supportsPointerEvents && !MobileDevice.any; - // this will make it so that you don't have to call bind all the time /** * @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 +168,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,41 +199,32 @@ this.onPointerOver = this.onPointerOver.bind(this); /** - * @private - * @member {Function} + * Dictionary of how different cursor modes are handled. Strings are handled as CSS cursor + * values, objects are handled as dictionaries of CSS values for interactionDOMElement, + * and functions are called instead of changing the CSS. + * Default CSS cursor values are provided for 'default' and 'pointer' modes. + * @member {Object.)>} */ - this.onTouchStart = this.onTouchStart.bind(this); - this.processTouchStart = this.processTouchStart.bind(this); + this.cursorStyles = { + default: 'inherit', + pointer: 'pointer', + }; /** - * @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. - * - * @member {string} - * @default 'inherit' - */ - this.defaultCursorStyle = 'inherit'; - - /** - * The css style of the cursor that is being used. + * The mode of the cursor that is being used. + * The value of this is a key from the cursorStyles dictionary. * * @member {string} */ - this.currentCursorStyle = 'inherit'; + this.currentCursorMode = null; + + /** + * Internal cached let. + * + * @private + * @member {string} + */ + this.cursor = null; /** * Internal cached let. @@ -303,6 +249,7 @@ * object. * * @event mousedown + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -311,6 +258,7 @@ * on the display object. * * @event rightdown + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -319,6 +267,7 @@ * object. * * @event mouseup + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -327,6 +276,7 @@ * over the display object. * * @event rightup + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -335,6 +285,7 @@ * the display object. * * @event click + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -343,6 +294,7 @@ * and released on the display object. * * @event rightclick + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -352,6 +304,7 @@ * [mousedown]{@link PIXI.interaction.InteractionManager#event:mousedown}. * * @event mouseupoutside + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -361,6 +314,7 @@ * [rightdown]{@link PIXI.interaction.InteractionManager#event:rightdown}. * * @event rightupoutside + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -368,6 +322,7 @@ * Fired when a pointer device (usually a mouse) is moved while over the display object * * @event mousemove + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -375,6 +330,7 @@ * Fired when a pointer device (usually a mouse) is moved onto the display object * * @event mouseover + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -382,6 +338,7 @@ * Fired when a pointer device (usually a mouse) is moved off the display object * * @event mouseout + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -389,6 +346,7 @@ * Fired when a pointer device button is pressed on the display object. * * @event pointerdown + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -396,6 +354,14 @@ * Fired when a pointer device button is released over the display object. * * @event pointerup + * @type {PIXI.interaction.InteractionData} + * @memberof PIXI.interaction.InteractionManager# + */ + + /** + * Fired when the operating system cancels a pointer event + * + * @event pointercancel * @memberof PIXI.interaction.InteractionManager# */ @@ -403,6 +369,7 @@ * Fired when a pointer device button is pressed and released on the display object. * * @event pointertap + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -411,6 +378,7 @@ * registered a [pointerdown]{@link PIXI.interaction.InteractionManager#event:pointerdown}. * * @event pointerupoutside + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -418,6 +386,7 @@ * Fired when a pointer device is moved while over the display object * * @event pointermove + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -425,6 +394,7 @@ * Fired when a pointer device is moved onto the display object * * @event pointerover + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -432,6 +402,7 @@ * Fired when a pointer device is moved off the display object * * @event pointerout + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -439,6 +410,7 @@ * Fired when a touch point is placed on the display object. * * @event touchstart + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -446,6 +418,14 @@ * Fired when a touch point is removed from the display object. * * @event touchend + * @type {PIXI.interaction.InteractionData} + * @memberof PIXI.interaction.InteractionManager# + */ + + /** + * Fired when the operating system cancels a touch + * + * @event touchcancel * @memberof PIXI.interaction.InteractionManager# */ @@ -453,6 +433,7 @@ * Fired when a touch point is placed and removed from the display object. * * @event tap + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -461,6 +442,7 @@ * registered a [touchstart]{@link PIXI.interaction.InteractionManager#event:touchstart}. * * @event touchendoutside + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -468,6 +450,7 @@ * Fired when a touch point is moved along the display object. * * @event touchmove + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ } @@ -524,45 +507,30 @@ { window.document.addEventListener('pointermove', this.onPointerMove, true); this.interactionDOMElement.addEventListener('pointerdown', this.onPointerDown, true); - this.interactionDOMElement.addEventListener('pointerout', this.onPointerOut, true); + // pointerout is fired in addition to pointerup (for touch events) and pointercancel + // we already handle those, so for the purposes of what we do in onPointerOut, we only + // care about the pointerleave event + this.interactionDOMElement.addEventListener('pointerleave', 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; @@ -596,45 +564,26 @@ { window.document.removeEventListener('pointermove', this.onPointerMove, true); this.interactionDOMElement.removeEventListener('pointerdown', this.onPointerDown, true); - this.interactionDOMElement.removeEventListener('pointerout', this.onPointerOut, true); + this.interactionDOMElement.removeEventListener('pointerleave', 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) + 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.onPointerDown, true); + this.interactionDOMElement.removeEventListener('touchcancel', this.onPointerCancel, 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); - - 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 = null; @@ -664,7 +613,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; @@ -672,25 +621,80 @@ return; } - this.cursor = this.defaultCursorStyle; + this.cursor = null; // 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(); - - this.processInteractive(this.mouse.global, this.renderer._lastObjectRendered, this.processMouseOverOut, true); - - if (this.currentCursorStyle !== this.cursor) + for (const k in this.activeInteractionData) { - this.currentCursorStyle = this.cursor; - this.interactionDOMElement.style.cursor = this.cursor; + // eslint-disable-next-line no-prototype-builtins + if (this.activeInteractionData.hasOwnProperty(k)) + { + const interactionData = this.activeInteractionData[k]; + + if (interactionData.originalEvent && interactionData.pointerType !== 'touch') + { + const interactionEvent = this.configureInteractionEventForDOMEvent( + this.eventData, + interactionData.originalEvent, + interactionData + ); + + this.processInteractive( + interactionEvent, + this.renderer._lastObjectRendered, + this.processPointerOverOut, + true + ); + } + } } + this.setCursorMode(this.cursor); + // TODO } /** + * Sets the current cursor mode, handling any callbacks or CSS style changes. + * + * @param {string} mode - cursor mode, a key from the cursorStyles dictionary + */ + setCursorMode(mode) + { + mode = mode || 'default'; + // if the mode didn't actually change, bail early + if (this.currentCursorMode === mode) + { + return; + } + this.currentCursorMode = mode; + const style = this.cursorStyles[mode]; + + // only do things if there is a cursor style for it + if (style) + { + switch (typeof style) + { + case 'string': + // string styles are handled as cursor CSS + this.interactionDOMElement.style.cursor = style; + break; + case 'function': + // functions are just called, and passed the cursor mode + style(mode); + break; + case 'object': + // if it is an object, assume that it is a dictionary of CSS styles, + // apply it to the interactionDOMElement + Object.assign(this.interactionDOMElement.style, style); + break; + } + } + } + + /** * Dispatches an event on the display object that was interacted with * * @param {PIXI.Container|PIXI.Sprite|PIXI.extras.TilingSprite} displayObject - the display object in question @@ -748,22 +752,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 @@ -787,9 +795,8 @@ { interactiveParent = false; } - - // it has a mask! Then lets hit test that before continuing.. - if (hitTest && displayObject._mask) + // it has a mask! Then lets hit test that before continuing + else if (hitTest && displayObject._mask) { if (!displayObject._mask.containsPoint(point)) { @@ -797,14 +804,7 @@ } } - // it has a filterArea! Same as mask but easier, its a rectangle - if (hitTest && displayObject.filterArea) - { - if (!displayObject.filterArea.contains(point.x, point.y)) - { - hitTest = false; - } - } + let keepHitTestingAfterChildren = hitTest; // ** FREE TIP **! If an object is not interactive or has no buttons in it // (such as a game scene!) set interactiveChildren to false for that displayObject. @@ -818,7 +818,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 @@ -838,15 +838,20 @@ // This means we no longer need to hit test anything else. We still need to run // through all objects, but we don't need to perform any hit tests. - // { - hitTest = false; - // } + keepHitTestingAfterChildren = false; + + if (child.interactive) + { + hitTest = false; + } // we can break now as we have hit an object. } } } + hitTest = keepHitTestingAfterChildren; + // no point running this if the item is not interactive or does not have an interactive parent. if (interactive) { @@ -868,14 +873,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 +886,175 @@ } /** - * 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].isNormalized) { - 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); + if (!displayObject.trackedPointers[id]) + { + displayObject.trackedPointers[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.trackedPointers[id].rightDown = true; + } + else + { + displayObject.trackedPointers[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.trackedPointers[id] !== undefined) + { + delete displayObject.trackedPointers[id]; + this.dispatchEvent(displayObject, 'pointercancel', interactionEvent); + + if (e.type === 'touchcancel' || e.pointerType === 'touch') + { + this.dispatchEvent(displayObject, 'touchcancel', interactionEvent); + } } } @@ -1159,42 +1066,94 @@ */ 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) { - if (hit) - { - this.dispatchEvent(displayObject, 'pointerup', this.eventData); + const e = interactionEvent.data.originalEvent; - if (displayObject._pointerDown) + const id = interactionEvent.data.identifier; + + const trackingData = displayObject.trackedPointers[id]; + + const isTouch = (e.type === 'touchend' || e.pointerType === 'touch'); + + const isMouse = (e.type.indexOf('mouse') === 0 || e.pointerType === 'mouse'); + + // 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) { - displayObject._pointerDown = false; - this.dispatchEvent(displayObject, 'pointertap', this.eventData); + 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); + } + // update the down state of the tracking data + if (trackingData) + { + if (isRightButton) + { + trackingData.rightDown = hit; + } + else + { + trackingData.leftDown = hit; + } } } - else if (displayObject._pointerDown) + + // Pointers and Touches, and Mouse + if (hit) { - displayObject._pointerDown = false; - this.dispatchEvent(displayObject, 'pointerupoutside', this.eventData); + this.dispatchEvent(displayObject, 'pointerup', interactionEvent); + if (isTouch) this.dispatchEvent(displayObject, 'touchend', interactionEvent); + + if (trackingData) + { + this.dispatchEvent(displayObject, 'pointertap', interactionEvent); + if (isTouch) + { + this.dispatchEvent(displayObject, 'tap', interactionEvent); + // touches are no longer over (if they ever were) when we get the touchend + // so we should ensure that we don't keep pretending that they are + trackingData.over = false; + } + } + } + else if (trackingData) + { + this.dispatchEvent(displayObject, 'pointerupoutside', interactionEvent); + if (isTouch) this.dispatchEvent(displayObject, 'touchendoutside', interactionEvent); + } + // Only remove the tracking data if there is no over/down state still associated with it + if (trackingData && trackingData.none) + { + delete displayObject.trackedPointers[id]; } } @@ -1202,39 +1161,78 @@ * 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 = null; + } - 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') + { + this.setCursorMode(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 (isMouse) { - 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 +1240,94 @@ * 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.setCursorMode(null); + } - 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'); + + let trackingData = displayObject.trackedPointers[id]; + + // if we just moused over the display object, then we need to track that state + if (hit && !trackingData) + { + trackingData = displayObject.trackedPointers[id] = new InteractionTrackingData(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); + } + } + + // only change the cursor if it has not already been changed (by something deeper in the + // display tree) + if (isMouse && this.cursor === null) + { + this.cursor = displayObject.cursor; } } - 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); + } + // if there is no mouse down information for the pointer, then it is safe to delete + if (trackingData.none) + { + delete displayObject.trackedPointers[id]; + } } } @@ -1287,253 +1335,154 @@ * 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 && pointerEvent.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; + + // mark the touch as normalized, just so that we know we did it + touch.isNormalized = true; + + normalizedEvents.push(touch); + } } - else if (this.normalizeMouseEvents) + // apparently PointerEvent subclasses MouseEvent, so yay + else if (event instanceof MouseEvent && (!this.supportsPointerEvents || !(event instanceof window.PointerEvent))) { if (typeof event.isPrimary === 'undefined') event.isPrimary = true; if (typeof event.width === 'undefined') event.width = 1; @@ -1541,10 +1490,21 @@ 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; + + // mark the mouse event as normalized, just so that we know we did it + event.isNormalized = true; + + normalizedEvents.push(event); } + else + { + normalizedEvents.push(event); + } + + return normalizedEvents; } /** @@ -1563,30 +1523,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 +1542,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..6a1f69e --- /dev/null +++ b/src/interaction/InteractionTrackingData.js @@ -0,0 +1,147 @@ +/** + * 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 inactive (not over or down)? + * + * @member {number} + * @memberof PIXI.interaction.InteractionTrackingData# + */ + get none() + { + return this._flags === this.constructor.FLAGS.NONE; + } + + /** + * 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..d252fba 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 * ); */ @@ -38,68 +38,56 @@ hitArea: null, /** - * If enabled, the mouse cursor will change when hovered over the displayObject if it is interactive + * If enabled, the mouse cursor use the pointer behavior when hovered over the displayObject if it is interactive + * Setting this changes the 'cursor' property to `'pointer'`. * - * @inner {boolean} + * @member {boolean} + * @memberof PIXI.interaction.interactiveTarget# */ - buttonMode: false, + get buttonMode() + { + return this.cursor === 'pointer'; + }, + set buttonMode(value) + { + if (value) + { + this.cursor = 'pointer'; + } + else if (this.cursor === 'pointer') + { + this.cursor = null; + } + }, /** - * If buttonMode is enabled, this defines what CSS cursor property is used when the mouse cursor - * is hovered over the displayObject + * This defines what cursor mode is used when the mouse cursor + * is hovered over the displayObject. * * @see https://developer.mozilla.org/en/docs/Web/CSS/cursor * * @inner {string} */ - defaultCursor: 'pointer', - - // some internal checks.. - /** - * Internal check to detect if the mouse cursor is hovered over the displayObject - * - * @inner {boolean} - * @private - */ - _over: false, + cursor: null, /** - * Internal check to detect if the left mouse button is pressed on the displayObject + * Internal set of all active pointers, by identifier * - * @inner {boolean} + * @member {Map} + * @memberof PIXI.interaction.interactiveTarget# * @private */ - _isLeftDown: false, + get trackedPointers() + { + if (this._trackedPointers === undefined) this._trackedPointers = {}; + + return this._trackedPointers; + }, /** - * Internal check to detect if the right mouse button is pressed on the displayObject + * Map of all tracked pointers, by identifier. Use trackedPointers to access. * - * @inner {boolean} - * @private + * @private {Map} */ - _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, + _trackedPointers: undefined, }; diff --git a/src/loaders/bitmapFontParser.js b/src/loaders/bitmapFontParser.js index cc2618c..f4cf996 100644 --- a/src/loaders/bitmapFontParser.js +++ b/src/loaders/bitmapFontParser.js @@ -1,63 +1,19 @@ import * as path from 'path'; -import { Rectangle, Texture, utils } from '../core'; +import { utils } from '../core'; import { Resource } from 'resource-loader'; import { BitmapText } from '../extras'; +/** + * Register a BitmapText font from loader resource. + * + * @function parseBitmapFontData + * @memberof PIXI.loaders + * @param {PIXI.loaders.Resource} resource - Loader resource. + * @param {PIXI.Texture} texture - Reference to texture. + */ export function parse(resource, texture) { - const data = {}; - const info = resource.data.getElementsByTagName('info')[0]; - const common = resource.data.getElementsByTagName('common')[0]; - - data.font = info.getAttribute('face'); - data.size = parseInt(info.getAttribute('size'), 10); - data.lineHeight = parseInt(common.getAttribute('lineHeight'), 10); - data.chars = {}; - - // parse letters - const letters = resource.data.getElementsByTagName('char'); - - for (let i = 0; i < letters.length; i++) - { - const charCode = parseInt(letters[i].getAttribute('id'), 10); - - const textureRect = new Rectangle( - parseInt(letters[i].getAttribute('x'), 10) + texture.frame.x, - parseInt(letters[i].getAttribute('y'), 10) + texture.frame.y, - parseInt(letters[i].getAttribute('width'), 10), - parseInt(letters[i].getAttribute('height'), 10) - ); - - data.chars[charCode] = { - xOffset: parseInt(letters[i].getAttribute('xoffset'), 10), - yOffset: parseInt(letters[i].getAttribute('yoffset'), 10), - xAdvance: parseInt(letters[i].getAttribute('xadvance'), 10), - kerning: {}, - texture: new Texture(texture.baseTexture, textureRect), - - }; - } - - // parse kernings - const kernings = resource.data.getElementsByTagName('kerning'); - - for (let i = 0; i < kernings.length; i++) - { - const first = parseInt(kernings[i].getAttribute('first'), 10); - const second = parseInt(kernings[i].getAttribute('second'), 10); - const amount = parseInt(kernings[i].getAttribute('amount'), 10); - - if (data.chars[second]) - { - data.chars[second].kerning[first] = amount; - } - } - - resource.bitmapFont = data; - - // I'm leaving this as a temporary fix so we can test the bitmap fonts in v3 - // but it's very likely to change - BitmapText.fonts[data.font] = data; + resource.bitmapFont = BitmapText.registerFont(resource.data, texture); } export default function () diff --git a/src/loaders/index.js b/src/loaders/index.js index 8942436..5189793 100644 --- a/src/loaders/index.js +++ b/src/loaders/index.js @@ -5,4 +5,11 @@ export { default as bitmapFontParser, parse as parseBitmapFontData } from './bitmapFontParser'; export { default as spritesheetParser } from './spritesheetParser'; export { default as textureParser } from './textureParser'; + +/** + * Reference to **resource-loader**'s Resource class. + * See https://github.com/englercj/resource-loader + * @class Resource + * @memberof PIXI.loaders + */ export { Resource } from 'resource-loader'; diff --git a/src/loaders/loader.js b/src/loaders/loader.js index 0a670cb..767a411 100644 --- a/src/loaders/loader.js +++ b/src/loaders/loader.js @@ -7,20 +7,47 @@ /** * - * The new loader, extends Resource Loader by Chad Engler : https://github.com/englercj/resource-loader + * The new loader, extends Resource Loader by Chad Engler: https://github.com/englercj/resource-loader * * ```js - * let loader = PIXI.loader; // pixi exposes a premade instance for you to use. + * const loader = PIXI.loader; // pixi exposes a premade instance for you to use. * //or - * let loader = new PIXI.loaders.Loader(); // you can also create your own if you want + * const loader = new PIXI.loaders.Loader(); // you can also create your own if you want * - * loader.add('bunny', 'data/bunny.png'); - * loader.add('spaceship', 'assets/spritesheet.json'); + * const sprites = {}; + * + * // Chainable `add` to enqueue a resource + * loader.add('bunny', 'data/bunny.png') + * .add('spaceship', 'assets/spritesheet.json'); * loader.add('scoreFont', 'assets/score.fnt'); * - * loader.once('complete',onAssetsLoaded); + * // Chainable `pre` to add a middleware that runs for each resource, *before* loading that resource. + * // This is useful to implement custom caching modules (using filesystem, indexeddb, memory, etc). + * loader.pre(cachingMiddleware); * - * loader.load(); + * // Chainable `use` to add a middleware that runs for each resource, *after* loading that resource. + * // This is useful to implement custom parsing modules (like spritesheet parsers, spine parser, etc). + * loader.use(parsingMiddleware); + * + * // The `load` method loads the queue of resources, and calls the passed in callback called once all + * // resources have loaded. + * loader.load((loader, resources) => { + * // resources is an object where the key is the name of the resource loaded and the value is the resource object. + * // They have a couple default properties: + * // - `url`: The URL that the resource was loaded from + * // - `error`: The error that happened when trying to load (if any) + * // - `data`: The raw data that was loaded + * // also may contain other properties based on the middleware that runs. + * sprites.bunny = new PIXI.TilingSprite(resources.bunny.texture); + * sprites.spaceship = new PIXI.TilingSprite(resources.spaceship.texture); + * sprites.scoreFont = new PIXI.TilingSprite(resources.scoreFont.texture); + * }); + * + * // throughout the process multiple signals can be dispatched. + * loader.onProgress.add(() => {}); // called once per loaded/errored file + * loader.onError.add(() => {}); // called once per errored file + * loader.onLoad.add(() => {}); // called once per loaded file + * loader.onComplete.add(() => {}); // called once when the queued resources all load. * ``` * * @see https://github.com/englercj/resource-loader diff --git a/src/loaders/spritesheetParser.js b/src/loaders/spritesheetParser.js index 7570332..e57dfb2 100644 --- a/src/loaders/spritesheetParser.js +++ b/src/loaders/spritesheetParser.js @@ -1,8 +1,6 @@ import { Resource } from 'resource-loader'; import path from 'path'; -import * as core from '../core'; - -const BATCH_SIZE = 1000; +import { Spritesheet } from '../core'; export default function () { @@ -43,134 +41,18 @@ // load the image for this sheet this.add(imageResourceName, resourcePath, loadOptions, function onImageLoad(res) { - resource.textures = {}; + const spritesheet = new Spritesheet( + res.texture.baseTexture, + resource.data, + resource.url + ); - const frames = resource.data.frames; - const frameKeys = Object.keys(frames); - const baseTexture = res.texture.baseTexture; - const scale = resource.data.meta.scale; - - // Use a defaultValue of `null` to check if a url-based resolution is set - let resolution = core.utils.getResolutionOfUrl(resource.url, null); - - // No resolution found via URL - if (resolution === null) + spritesheet.parse(() => { - // Use the scale value or default to 1 - resolution = scale !== undefined ? scale : 1; - } - - // For non-1 resolutions, update baseTexture - if (resolution !== 1) - { - baseTexture.resolution = resolution; - baseTexture.update(); - } - - let batchIndex = 0; - - function processFrames(initialFrameIndex, maxFrames) - { - let frameIndex = initialFrameIndex; - - while (frameIndex - initialFrameIndex < maxFrames && frameIndex < frameKeys.length) - { - const i = frameKeys[frameIndex]; - const rect = frames[i].frame; - - if (rect) - { - let frame = null; - let trim = null; - const orig = new core.Rectangle( - 0, - 0, - frames[i].sourceSize.w / resolution, - frames[i].sourceSize.h / resolution - ); - - if (frames[i].rotated) - { - frame = new core.Rectangle( - rect.x / resolution, - rect.y / resolution, - rect.h / resolution, - rect.w / resolution - ); - } - else - { - frame = new core.Rectangle( - rect.x / resolution, - rect.y / resolution, - rect.w / resolution, - rect.h / resolution - ); - } - - // Check to see if the sprite is trimmed - if (frames[i].trimmed) - { - trim = new core.Rectangle( - frames[i].spriteSourceSize.x / resolution, - frames[i].spriteSourceSize.y / resolution, - rect.w / resolution, - rect.h / resolution - ); - } - - resource.textures[i] = new core.Texture( - baseTexture, - frame, - orig, - trim, - frames[i].rotated ? 2 : 0 - ); - - // lets also add the frame to pixi's global cache for fromFrame and fromImage functions - core.utils.TextureCache[i] = resource.textures[i]; - } - - frameIndex++; - } - } - - function shouldProcessNextBatch() - { - return batchIndex * BATCH_SIZE < frameKeys.length; - } - - function processNextBatch(done) - { - processFrames(batchIndex * BATCH_SIZE, BATCH_SIZE); - batchIndex++; - setTimeout(done, 0); - } - - function iteration() - { - processNextBatch(() => - { - if (shouldProcessNextBatch()) - { - iteration(); - } - else - { - next(); - } - }); - } - - if (frameKeys.length <= BATCH_SIZE) - { - processFrames(0, BATCH_SIZE); + resource.spritesheet = spritesheet; + resource.textures = spritesheet.textures; next(); - } - else - { - iteration(); - } + }); }); }; } diff --git a/src/loaders/textureParser.js b/src/loaders/textureParser.js index 5398a7f..480edc1 100644 --- a/src/loaders/textureParser.js +++ b/src/loaders/textureParser.js @@ -1,5 +1,5 @@ -import * as core from '../core'; import { Resource } from 'resource-loader'; +import Texture from '../core/textures/Texture'; export default function () { @@ -8,23 +8,12 @@ // create a new texture if the data is an Image object if (resource.data && resource.type === Resource.TYPE.IMAGE) { - const baseTexture = new core.BaseTexture(resource.data, null, core.utils.getResolutionOfUrl(resource.url)); - - baseTexture.imageUrl = resource.url; - resource.texture = new core.Texture(baseTexture); - - // lets also add the frame to pixi's global cache for fromFrame and fromImage fucntions - core.utils.BaseTextureCache[resource.name] = baseTexture; - core.utils.TextureCache[resource.name] = resource.texture; - - // also add references by url if they are different. - if (resource.name !== resource.url) - { - core.utils.BaseTextureCache[resource.url] = baseTexture; - core.utils.TextureCache[resource.url] = resource.texture; - } + resource.texture = Texture.fromLoader( + resource.data, + resource.url, + resource.name + ); } - next(); }; } diff --git a/test/core/Container.js b/test/core/Container.js index 8a10b40..d11be33 100644 --- a/test/core/Container.js +++ b/test/core/Container.js @@ -1,5 +1,33 @@ 'use strict'; +function testAddChild(fn) +{ + fn(function (container, obj) + { + container.addChild(obj); + }); + fn(function (container, obj) + { + container.addChildAt(obj); + }); +} + +function testRemoveChild(fn) +{ + fn(function (container, obj) + { + container.removeChild(obj); + }); + fn(function (container, obj) + { + container.removeChildAt(container.children.indexOf(obj)); + }); + fn(function (container, obj) + { + container.removeChildren(container.children.indexOf(obj), container.children.indexOf(obj) + 1); + }); +} + describe('PIXI.Container', function () { describe('parent', function () @@ -70,6 +98,49 @@ expect(spy).to.have.been.called; expect(spy).to.have.been.calledWith(0); }); + + it('should flag child transform and container bounds for recalculation', testAddChild(function (mockAddChild) + { + const container = new PIXI.Container(); + const child = new PIXI.Container(); + + container.getBounds(); + child.getBounds(); + + const boundsID = container._boundsID; + const childParentID = child.transform._parentID; + + mockAddChild(container, child); + + expect(boundsID).to.not.be.equals(container._boundsID); + expect(childParentID).to.not.be.equals(child.transform._parentID); + })); + + it('should recalculate added child correctly', testAddChild(function (mockAddChild) + { + const parent = new PIXI.Container(); + const container = new PIXI.Container(); + const graphics = new PIXI.Graphics(); + + parent.addChild(container); + + graphics.drawRect(0, 0, 10, 10); + container.position.set(100, 200); + container.updateTransform(); + + graphics.getBounds(); + // Oops, that can happen sometimes! + graphics.transform._parentID = container.transform._worldID + 1; + + mockAddChild(container, graphics); + + const bounds = graphics.getBounds(); + + expect(bounds.x).to.be.equal(100); + expect(bounds.y).to.be.equal(200); + expect(bounds.width).to.be.equal(10); + expect(bounds.height).to.be.equal(10); + })); }); describe('removeChildAt', function () @@ -202,6 +273,44 @@ expect(spy).to.have.been.called; expect(spy).to.have.been.calledWith(0); }); + + it('should flag transform for recalculation', testRemoveChild(function (mockRemoveChild) + { + const container = new PIXI.Container(); + const child = new PIXI.Container(); + + container.addChild(child); + container.getBounds(); + + const childParentID = child.transform._parentID; + const boundsID = container._boundsID; + + mockRemoveChild(container, child); + + expect(childParentID).to.not.be.equals(child.transform._parentID); + expect(boundsID).to.not.be.equals(container._boundsID); + })); + + it('should recalculate removed child correctly', testRemoveChild(function (mockRemoveChild) + { + const parent = new PIXI.Container(); + const container = new PIXI.Container(); + const graphics = new PIXI.Graphics(); + + parent.addChild(container); + + graphics.drawRect(0, 0, 10, 10); + container.position.set(100, 200); + container.addChild(graphics); + graphics.getBounds(); + + mockRemoveChild(container, graphics); + + const bounds = graphics.getBounds(); + + expect(bounds.x).to.be.equal(0); + expect(bounds.y).to.be.equal(0); + })); }); describe('getChildIndex', function () diff --git a/test/core/Graphics.js b/test/core/Graphics.js index 98c7584..11e47a0 100644 --- a/test/core/Graphics.js +++ b/test/core/Graphics.js @@ -1,5 +1,8 @@ 'use strict'; +const MockPointer = require('../interaction/MockPointer'); +const withGL = require('../withGL'); + describe('PIXI.Graphics', function () { describe('constructor', function () @@ -218,4 +221,153 @@ expect(spy).to.have.been.calledOnce; }); }); + + describe('mask', function () + { + it('should trigger interaction callback when no mask present', function () + { + const stage = new PIXI.Container(); + const pointer = new MockPointer(stage); + const graphics = new PIXI.Graphics(); + const mask = new PIXI.Graphics(); + const spy = sinon.spy(); + + graphics.interactive = true; + graphics.beginFill(0xFF0000); + graphics.drawRect(0, 0, 50, 50); + graphics.on('click', spy); + stage.addChild(graphics); + mask.beginFill(); + mask.drawRect(0, 0, 50, 50); + graphics.mask = mask; + + pointer.click(10, 10); + + expect(spy).to.have.been.calledOnce; + }); + it('should trigger interaction callback when mask uses beginFill', function () + { + const stage = new PIXI.Container(); + const pointer = new MockPointer(stage); + const graphics = new PIXI.Graphics(); + const mask = new PIXI.Graphics(); + const spy = sinon.spy(); + + graphics.interactive = true; + graphics.beginFill(0xFF0000); + graphics.drawRect(0, 0, 50, 50); + graphics.on('click', spy); + stage.addChild(graphics); + mask.beginFill(); + mask.drawRect(0, 0, 50, 50); + graphics.mask = mask; + + pointer.click(10, 10); + + expect(spy).to.have.been.calledOnce; + }); + + it('should not trigger interaction callback when mask doesn\'t use beginFill', function () + { + const stage = new PIXI.Container(); + const pointer = new MockPointer(stage); + const graphics = new PIXI.Graphics(); + const mask = new PIXI.Graphics(); + const spy = sinon.spy(); + + graphics.interactive = true; + graphics.beginFill(0xFF0000); + graphics.drawRect(0, 0, 50, 50); + graphics.on('click', spy); + stage.addChild(graphics); + mask.drawRect(0, 0, 50, 50); + graphics.mask = mask; + + pointer.click(10, 10); + + expect(spy).to.have.not.been.called; + }); + + it('should trigger interaction callback when mask doesn\'t use beginFill but hitArea is defined', function () + { + const stage = new PIXI.Container(); + const pointer = new MockPointer(stage); + const graphics = new PIXI.Graphics(); + const mask = new PIXI.Graphics(); + const spy = sinon.spy(); + + graphics.interactive = true; + graphics.beginFill(0xFF0000); + graphics.hitArea = new PIXI.Rectangle(0, 0, 50, 50); + graphics.drawRect(0, 0, 50, 50); + graphics.on('click', spy); + stage.addChild(graphics); + mask.drawRect(0, 0, 50, 50); + graphics.mask = mask; + + pointer.click(10, 10); + + expect(spy).to.have.been.calledOnce; + }); + + it('should trigger interaction callback when mask is a sprite', function () + { + const stage = new PIXI.Container(); + const pointer = new MockPointer(stage); + const graphics = new PIXI.Graphics(); + const mask = new PIXI.Graphics(); + const spy = sinon.spy(); + + graphics.interactive = true; + graphics.beginFill(0xFF0000); + graphics.drawRect(0, 0, 50, 50); + graphics.on('click', spy); + stage.addChild(graphics); + mask.drawRect(0, 0, 50, 50); + graphics.mask = new PIXI.Sprite(mask.generateCanvasTexture()); + + pointer.click(10, 10); + + expect(spy).to.have.been.calledOnce; + }); + + it('should calculate tint, alpha and blendMode of fastRect correctly', withGL(function () + { + const renderer = new PIXI.WebGLRenderer(200, 200, {}); + + try + { + const graphics = new PIXI.Graphics(); + + graphics.beginFill(0x102030, 0.6); + graphics.drawRect(2, 3, 100, 100); + graphics.endFill(); + graphics.tint = 0x101010; + graphics.blendMode = 2; + graphics.alpha = 0.3; + + renderer.render(graphics); + + expect(graphics.isFastRect()).to.be.true; + + const sprite = graphics._spriteRect; + + expect(sprite).to.not.be.equals(null); + expect(sprite.worldAlpha).to.equals(0.18); + expect(sprite.blendMode).to.equals(2); + expect(sprite.tint).to.equals(0x010203); + + const bounds = sprite.getBounds(); + + expect(bounds.x).to.equals(2); + expect(bounds.y).to.equals(3); + expect(bounds.width).to.equals(100); + expect(bounds.height).to.equals(100); + } + finally + { + renderer.destroy(); + } + })); + }); }); diff --git a/test/core/Spritesheet.js b/test/core/Spritesheet.js new file mode 100644 index 0000000..283adf5 --- /dev/null +++ b/test/core/Spritesheet.js @@ -0,0 +1,89 @@ +'use strict'; + +const path = require('path'); + +describe('PIXI.Spritesheet', function () +{ + before(function () + { + this.resources = path.join(__dirname, 'resources'); + this.validate = function (spritesheet, done) + { + spritesheet.parse(function (textures) + { + const id = 'goldmine_10_5.png'; + + expect(Object.keys(textures).length).to.equal(1); + expect(Object.keys(spritesheet.textures).length).to.equal(1); + expect(textures[id]).to.be.an.instanceof(PIXI.Texture); + expect(textures[id].width).to.equal(spritesheet.data.frames[id].frame.w / spritesheet.resolution); + expect(textures[id].height).to.equal(spritesheet.data.frames[id].frame.h / spritesheet.resolution); + spritesheet.destroy(true); + expect(spritesheet.textures).to.be.null; + expect(spritesheet.baseTexture).to.be.null; + done(); + }); + }; + }); + + it('should exist on PIXI', function () + { + expect(PIXI.Spritesheet).to.be.a.function; + expect(PIXI.Spritesheet.BATCH_SIZE).to.be.a.number; + }); + + it('should create an instance', function () + { + const baseTexture = new PIXI.BaseTexture(); + const data = { + frames: {}, + meta: {}, + }; + const spritesheet = new PIXI.Spritesheet(baseTexture, data); + + expect(spritesheet.data).to.equal(data); + expect(spritesheet.baseTexture).to.equal(baseTexture); + expect(spritesheet.resolution).to.equal(1); + + spritesheet.destroy(true); + }); + + it('should create instance with scale resolution', function (done) + { + const data = require(path.resolve(this.resources, 'building1.json')); // eslint-disable-line global-require + const image = new Image(); + + image.src = path.join(this.resources, data.meta.image); + image.onload = () => + { + const baseTexture = new PIXI.BaseTexture(image, null, 1); + const spritesheet = new PIXI.Spritesheet(baseTexture, data); + + expect(data).to.be.an.object; + expect(data.meta.image).to.equal('building1.png'); + expect(spritesheet.resolution).to.equal(0.5); + + this.validate(spritesheet, done); + }; + }); + + it('should create instance with filename resolution', function (done) + { + const uri = path.resolve(this.resources, 'building1@2x.json'); + const data = require(uri); // eslint-disable-line global-require + const image = new Image(); + + image.src = path.join(this.resources, data.meta.image); + image.onload = () => + { + const baseTexture = new PIXI.BaseTexture(image, null, 1); + const spritesheet = new PIXI.Spritesheet(baseTexture, data, uri); + + expect(data).to.be.an.object; + expect(data.meta.image).to.equal('building1@2x.png'); + expect(spritesheet.resolution).to.equal(2); + + this.validate(spritesheet, done); + }; + }); +}); diff --git a/test/core/Texture.js b/test/core/Texture.js new file mode 100644 index 0000000..8c4ef98 --- /dev/null +++ b/test/core/Texture.js @@ -0,0 +1,19 @@ +'use strict'; + +describe('PIXI.Texture', function () +{ + it('should register Texture from Loader', function () + { + const URL = 'foo.png'; + const NAME = 'bar'; + const image = new Image(); + + const texture = PIXI.Texture.fromLoader(image, URL, NAME); + + expect(texture.baseTexture.imageUrl).to.equal('foo.png'); + expect(PIXI.utils.TextureCache[NAME]).to.equal(texture); + expect(PIXI.utils.BaseTextureCache[NAME]).to.equal(texture.baseTexture); + expect(PIXI.utils.TextureCache[URL]).to.equal(texture); + expect(PIXI.utils.BaseTextureCache[URL]).to.equal(texture.baseTexture); + }); +}); diff --git a/package.json b/package.json index 6a366ba..71ade15 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pixi.js", - "version": "4.3.4", + "version": "4.3.5", "description": "Pixi.js is a fast lightweight 2D library that works across all devices.", "author": "Mat Groves", "contributors": [ @@ -61,7 +61,7 @@ "ismobilejs": "^0.4.0", "object-assign": "^4.0.1", "pixi-gl-core": "^1.0.3", - "resource-loader": "^2.0.4" + "resource-loader": "^2.0.6" }, "devDependencies": { "babel-cli": "^6.18.0", @@ -70,9 +70,9 @@ "babel-preset-es2015": "^6.14.0", "babelify": "^7.3.0", "del": "^2.2.0", - "electron-prebuilt": "^1.3.2", + "electron": "^1.4.15", "eslint": "^3.5.0", - "floss": "^1.2.0", + "floss": "^2.0.1", "gh-pages": "^0.11.0", "jaguarjs-jsdoc": "^1.0.1", "js-md5": "^0.4.1", diff --git a/src/accessibility/AccessibilityManager.js b/src/accessibility/AccessibilityManager.js index df2716a..f16bcb1 100644 --- a/src/accessibility/AccessibilityManager.js +++ b/src/accessibility/AccessibilityManager.js @@ -3,7 +3,7 @@ import accessibleTarget from './accessibleTarget'; // add some extra variables to the container.. -Object.assign( +core.utils.mixins.delayMixin( core.DisplayObject.prototype, accessibleTarget ); diff --git a/src/core/Application.js b/src/core/Application.js index ea0f058..5340578 100644 --- a/src/core/Application.js +++ b/src/core/Application.js @@ -1,6 +1,6 @@ import { autoDetectRenderer } from './autoDetectRenderer'; import Container from './display/Container'; -import Ticker from './ticker/Ticker'; +import { shared, Ticker } from './ticker'; /** * Convenience class to create a new PIXI application. @@ -35,9 +35,9 @@ * @param {boolean} [noWebGL=false] - prevents selection of WebGL renderer, even if such is present * @param {boolean} [options.legacy=false] - If true Pixi will aim to ensure compatibility * with older / less advanced devices. If you experience unexplained flickering try setting this to true. - + * @param {boolean} [useSharedTicker=false] - `true` to use PIXI.ticker.shared, `false` to create new ticker. */ - constructor(width, height, options, noWebGL) + constructor(width, height, options, noWebGL, useSharedTicker = false) { /** * WebGL renderer if available, otherwise CanvasRenderer @@ -46,23 +46,46 @@ this.renderer = autoDetectRenderer(width, height, options, noWebGL); /** - * The root display container that's renderered. + * The root display container that's rendered. * @member {PIXI.Container} */ this.stage = new Container(); /** + * Internal reference to the ticker + * @member {PIXI.ticker.Ticker} + * @private + */ + this._ticker = null; + + /** * Ticker for doing render updates. * @member {PIXI.ticker.Ticker} + * @default PIXI.ticker.shared */ - this.ticker = new Ticker(); - - this.ticker.add(this.render, this); + this.ticker = useSharedTicker ? shared : new Ticker(); // Start the rendering this.start(); } + set ticker(ticker) // eslint-disable-line require-jsdoc + { + if (this._ticker) + { + this._ticker.remove(this.render, this); + } + this._ticker = ticker; + if (ticker) + { + ticker.add(this.render, this); + } + } + get ticker() // eslint-disable-line require-jsdoc + { + return this._ticker; + } + /** * Render the current stage. */ @@ -76,7 +99,7 @@ */ stop() { - this.ticker.stop(); + this._ticker.stop(); } /** @@ -84,7 +107,7 @@ */ start() { - this.ticker.start(); + this._ticker.start(); } /** @@ -98,13 +121,22 @@ } /** + * Reference to the renderer's screen rectangle. Its safe to use as filterArea or hitArea for whole screen + * @member {PIXI.Rectangle} + * @readonly + */ + get screen() + { + return this.renderer.screen; + } + + /** * Destroy and don't use after this. * @param {Boolean} [removeView=false] Automatically remove canvas from DOM. */ destroy(removeView) { this.stop(); - this.ticker.remove(this.render, this); this.ticker = null; this.stage.destroy(); diff --git a/src/core/Shader.js b/src/core/Shader.js new file mode 100644 index 0000000..78b17e6 --- /dev/null +++ b/src/core/Shader.js @@ -0,0 +1,46 @@ +import { GLShader } from 'pixi-gl-core'; +import settings from './settings'; + +function checkPrecision(src, def) +{ + if (src instanceof Array) + { + if (src[0].substring(0, 9) !== 'precision') + { + const copy = src.slice(0); + + copy.unshift(`precision ${def} float;`); + + return copy; + } + } + else if (src.substring(0, 9) !== 'precision') + { + return `precision ${def} float;\n${src}`; + } + + return src; +} + +/** + * Wrapper class, webGL Shader for Pixi. + * Adds precision string if vertexSrc or fragmentSrc have no mention of it. + * + * @class + * @extends GLShader + * @memberof PIXI + */ +export default class Shader extends GLShader +{ + /** + * + * @param {WebGLRenderingContext} gl - The current WebGL rendering context + * @param {string|string[]} vertexSrc - The vertex shader source as an array of strings. + * @param {string|string[]} fragmentSrc - The fragment shader source as an array of strings. + */ + constructor(gl, vertexSrc, fragmentSrc) + { + super(gl, checkPrecision(vertexSrc, settings.PRECISION_VERTEX), + checkPrecision(fragmentSrc, settings.PRECISION_FRAGMENT)); + } +} diff --git a/src/core/const.js b/src/core/const.js index ef6f662..3de0efe 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -188,7 +188,7 @@ * The gc modes that are supported by pixi. * * The {@link PIXI.settings.GC_MODE} Garbage Collection mode for pixi textures is AUTO - * If set to GC_MODE, the renderer will occasianally check textures usage. If they are not + * If set to GC_MODE, the renderer will occasionally check textures usage. If they are not * used for a specified period of time they will be removed from the GPU. They will of course * be uploaded again when they are required. This is a silent behind the scenes process that * should ensure that the GPU does not get filled up. diff --git a/src/core/display/Container.js b/src/core/display/Container.js index ed8cd5f..17fde8d 100644 --- a/src/core/display/Container.js +++ b/src/core/display/Container.js @@ -73,11 +73,12 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.push(child); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -108,11 +109,12 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.splice(index, 0, child); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -225,10 +227,11 @@ if (index === -1) return null; child.parent = null; + // ensure child transform will be recalculated + child.transform._parentID = -1; removeItems(this.children, index, 1); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -249,11 +252,12 @@ { const child = this.getChildAt(index); + // ensure child transform will be recalculated.. child.parent = null; + child.transform._parentID = -1; removeItems(this.children, index, 1); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -284,13 +288,12 @@ for (let i = 0; i < removed.length; ++i) { removed[i].parent = null; + if (removed[i].transform) + { + removed[i].transform._parentID = -1; + } } - // ensure a transform will be recalculated.. - if (this.transform) - { - this.transform._parentID = -1; - } this._boundsID++; this.onChildrenChange(beginIndex); diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index f37f2f5..be4c4fb 100644 --- a/src/core/graphics/Graphics.js +++ b/src/core/graphics/Graphics.js @@ -28,8 +28,9 @@ { /** * + * @param {boolean} [nativeLines=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP */ - constructor() + constructor(nativeLines = false) { super(); @@ -50,6 +51,13 @@ this.lineWidth = 0; /** + * If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * + * @member {boolean} + */ + this.nativeLines = nativeLines; + + /** * The color of any lines drawn. * * @member {string} @@ -757,28 +765,14 @@ if (!this._spriteRect) { - if (!Graphics._SPRITE_TEXTURE) - { - Graphics._SPRITE_TEXTURE = RenderTexture.create(10, 10); - - const canvas = document.createElement('canvas'); - - canvas.width = 10; - canvas.height = 10; - - const context = canvas.getContext('2d'); - - context.fillStyle = 'white'; - context.fillRect(0, 0, 10, 10); - - Graphics._SPRITE_TEXTURE = Texture.fromCanvas(canvas); - } - - this._spriteRect = new Sprite(Graphics._SPRITE_TEXTURE); + this._spriteRect = new Sprite(new Texture(Texture.WHITE)); } + + const sprite = this._spriteRect; + if (this.tint === 0xffffff) { - this._spriteRect.tint = this.graphicsData[0].fillColor; + sprite.tint = this.graphicsData[0].fillColor; } else { @@ -792,20 +786,21 @@ t1[1] *= t2[1]; t1[2] *= t2[2]; - this._spriteRect.tint = rgb2hex(t1); + sprite.tint = rgb2hex(t1); } - this._spriteRect.alpha = this.graphicsData[0].fillAlpha; - this._spriteRect.worldAlpha = this.worldAlpha * this._spriteRect.alpha; + sprite.alpha = this.graphicsData[0].fillAlpha; + sprite.worldAlpha = this.worldAlpha * sprite.alpha; + sprite.blendMode = this.blendMode; - Graphics._SPRITE_TEXTURE._frame.width = rect.width; - Graphics._SPRITE_TEXTURE._frame.height = rect.height; + sprite.texture._frame.width = rect.width; + sprite.texture._frame.height = rect.height; - this._spriteRect.transform.worldTransform = this.transform.worldTransform; + sprite.transform.worldTransform = this.transform.worldTransform; - this._spriteRect.anchor.set(-rect.x / rect.width, -rect.y / rect.height); - this._spriteRect._onAnchorUpdate(); + sprite.anchor.set(-rect.x / rect.width, -rect.y / rect.height); + sprite._onAnchorUpdate(); - this._spriteRect._renderWebGL(renderer); + sprite._renderWebGL(renderer); } /** @@ -1031,6 +1026,7 @@ this.fillColor, this.fillAlpha, this.filling, + this.nativeLines, shape ); diff --git a/src/core/graphics/GraphicsData.js b/src/core/graphics/GraphicsData.js index f5a87ae..b6f03c0 100644 --- a/src/core/graphics/GraphicsData.js +++ b/src/core/graphics/GraphicsData.js @@ -14,14 +14,19 @@ * @param {number} fillColor - the color of the fill * @param {number} fillAlpha - the alpha of the fill * @param {boolean} fill - whether or not the shape is filled with a colour + * @param {boolean} nativeLines - the method for drawing lines * @param {PIXI.Circle|PIXI.Rectangle|PIXI.Ellipse|PIXI.Polygon} shape - The shape object to draw. */ - constructor(lineWidth, lineColor, lineAlpha, fillColor, fillAlpha, fill, shape) + constructor(lineWidth, lineColor, lineAlpha, fillColor, fillAlpha, fill, nativeLines, shape) { /** * @member {number} the width of the line to draw */ this.lineWidth = lineWidth; + /** + * @member {boolean} if true the liens will be draw using LINES instead of TRIANGLE_STRIP + */ + this.nativeLines = nativeLines; /** * @member {number} the color of the line to draw @@ -85,6 +90,7 @@ this.fillColor, this.fillAlpha, this.fill, + this.nativeLines, this.shape ); } diff --git a/src/core/graphics/webgl/GraphicsRenderer.js b/src/core/graphics/webgl/GraphicsRenderer.js index 14083fe..67f5e92 100644 --- a/src/core/graphics/webgl/GraphicsRenderer.js +++ b/src/core/graphics/webgl/GraphicsRenderer.js @@ -102,7 +102,15 @@ shaderTemp.uniforms.alpha = graphics.worldAlpha; renderer.bindVao(webGLData.vao); - webGLData.vao.draw(gl.TRIANGLE_STRIP, webGLData.indices.length); + + if (graphics.nativeLines) + { + gl.drawArrays(gl.LINES, 0, webGLData.points.length / 6); + } + else + { + webGLData.vao.draw(gl.TRIANGLE_STRIP, webGLData.indices.length); + } } } diff --git a/src/core/graphics/webgl/utils/buildLine.js b/src/core/graphics/webgl/utils/buildLine.js index ecf43f7..4d6862e 100644 --- a/src/core/graphics/webgl/utils/buildLine.js +++ b/src/core/graphics/webgl/utils/buildLine.js @@ -11,7 +11,29 @@ * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties * @param {object} webGLData - an object containing all the webGL-specific information to create this shape */ -export default function buildLine(graphicsData, webGLData) +export default function (graphicsData, webGLData) +{ + if (graphicsData.nativeLines) + { + buildNativeLine(graphicsData, webGLData); + } + else + { + buildLine(graphicsData, webGLData); + } +} + +/** + * Builds a line to draw using the poligon method. + * + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @private + * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties + * @param {object} webGLData - an object containing all the webGL-specific information to create this shape + */ +function buildLine(graphicsData, webGLData) { // TODO OPTIMISE! let points = graphicsData.points; @@ -224,3 +246,46 @@ indices.push(indexStart - 1); } + +/** + * Builds a line to draw using the gl.drawArrays(gl.LINES) method + * + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @private + * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties + * @param {object} webGLData - an object containing all the webGL-specific information to create this shape + */ +function buildNativeLine(graphicsData, webGLData) +{ + let i = 0; + const points = graphicsData.points; + + if (points.length === 0) return; + + const verts = webGLData.points; + const length = points.length / 2; + + // sort color + const color = hex2rgb(graphicsData.lineColor); + const alpha = graphicsData.lineAlpha; + const r = color[0] * alpha; + const g = color[1] * alpha; + const b = color[2] * alpha; + + for (i = 1; i < length; i++) + { + const p1x = points[(i - 1) * 2]; + const p1y = points[((i - 1) * 2) + 1]; + + const p2x = points[i * 2]; + const p2y = points[(i * 2) + 1]; + + verts.push(p1x, p1y); + verts.push(r, g, b, alpha); + + verts.push(p2x, p2y); + verts.push(r, g, b, alpha); + } +} diff --git a/src/core/graphics/webgl/utils/buildRoundedRectangle.js b/src/core/graphics/webgl/utils/buildRoundedRectangle.js index 9abc807..a6c8461 100644 --- a/src/core/graphics/webgl/utils/buildRoundedRectangle.js +++ b/src/core/graphics/webgl/utils/buildRoundedRectangle.js @@ -77,6 +77,26 @@ } /** + * Calculate a single point for a quadratic bezier curve. + * Utility function used by quadraticBezierCurve. + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @private + * @param {number} n1 - first number + * @param {number} n2 - second number + * @param {number} perc - percentage + * @return {number} the result + * + */ +function getPt(n1, n2, perc) +{ + const diff = n2 - n1; + + return n1 + (diff * perc); +} + +/** * Calculate the points for a quadratic bezier curve. (helper function..) * Based on: https://stackoverflow.com/questions/785097/how-do-i-implement-a-bezier-curve-in-c * @@ -105,13 +125,6 @@ let x = 0; let y = 0; - function getPt(n1, n2, perc) - { - const diff = n2 - n1; - - return n1 + (diff * perc); - } - for (let i = 0, j = 0; i <= n; ++i) { j = i / n; diff --git a/src/core/index.js b/src/core/index.js index f7a817c..4f414e8 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -30,6 +30,7 @@ export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; export { default as CanvasGraphicsRenderer } from './graphics/canvas/CanvasGraphicsRenderer'; +export { default as Spritesheet } from './textures/Spritesheet'; export { default as Texture } from './textures/Texture'; export { default as BaseTexture } from './textures/BaseTexture'; export { default as RenderTexture } from './textures/RenderTexture'; diff --git a/src/core/math/Matrix.js b/src/core/math/Matrix.js index 0b77477..3679860 100644 --- a/src/core/math/Matrix.js +++ b/src/core/math/Matrix.js @@ -13,45 +13,50 @@ export default class Matrix { /** - * + * @param {number} [a=1] - x scale + * @param {number} [b=0] - y skew + * @param {number} [c=0] - x skew + * @param {number} [d=1] - y scale + * @param {number} [tx=0] - x translation + * @param {number} [ty=0] - y translation */ - constructor() + constructor(a = 1, b = 0, c = 0, d = 1, tx = 0, ty = 0) { /** * @member {number} * @default 1 */ - this.a = 1; + this.a = a; /** * @member {number} * @default 0 */ - this.b = 0; + this.b = b; /** * @member {number} * @default 0 */ - this.c = 0; + this.c = c; /** * @member {number} * @default 1 */ - this.d = 1; + this.d = d; /** * @member {number} * @default 0 */ - this.tx = 0; + this.tx = tx; /** * @member {number} * @default 0 */ - this.ty = 0; + this.ty = ty; this.array = null; } diff --git a/src/core/renderers/SystemRenderer.js b/src/core/renderers/SystemRenderer.js index fa116a6..a24c775 100644 --- a/src/core/renderers/SystemRenderer.js +++ b/src/core/renderers/SystemRenderer.js @@ -1,5 +1,5 @@ import { sayHello, hex2string, hex2rgb } from '../utils'; -import { Matrix } from '../math'; +import { Matrix, Rectangle } from '../math'; import { RENDERER_TYPE } from '../const'; import settings from '../settings'; import Container from '../display/Container'; @@ -21,8 +21,8 @@ { /** * @param {string} system - The name of the system this renderer is for. - * @param {number} [width=800] - the width of the canvas view - * @param {number} [height=600] - the height of the canvas view + * @param {number} [screenWidth=800] - the width of the screen + * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false @@ -37,7 +37,7 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(system, width, height, options) + constructor(system, screenWidth, screenHeight, options) { super(); @@ -69,20 +69,13 @@ this.type = RENDERER_TYPE.UNKNOWN; /** - * The width of the canvas view + * Measurements of the screen. (0, 0, screenWidth, screenHeight) * - * @member {number} - * @default 800 - */ - this.width = width || 800; - - /** - * The height of the canvas view + * Its safe to use as filterArea or hitArea for whole stage * - * @member {number} - * @default 600 + * @member {PIXI.Rectangle} */ - this.height = height || 600; + this.screen = new Rectangle(0, 0, screenWidth || 800, screenHeight || 600); /** * The canvas element that everything is drawn to @@ -107,7 +100,7 @@ this.transparent = options.transparent; /** - * Whether the render view should be resized automatically + * Whether css dimensions of canvas view should be resized to screen dimensions automatically * * @member {boolean} */ @@ -192,23 +185,48 @@ } /** - * Resizes the canvas view to the specified width and height + * Same as view.width, actual number of pixels in the canvas by horizontal * - * @param {number} width - the new width of the canvas view - * @param {number} height - the new height of the canvas view + * @member {number} + * @readonly + * @default 800 */ - resize(width, height) + get width() { - this.width = width * this.resolution; - this.height = height * this.resolution; + return this.view.width; + } - this.view.width = this.width; - this.view.height = this.height; + /** + * Same as view.height, actual number of pixels in the canvas by vertical + * + * @member {number} + * @readonly + * @default 600 + */ + get height() + { + return this.view.height; + } + + /** + * Resizes the screen and canvas to the specified width and height + * Canvas dimensions are multiplied by resolution + * + * @param {number} screenWidth - the new width of the screen + * @param {number} screenHeight - the new height of the screen + */ + resize(screenWidth, screenHeight) + { + this.screen.width = screenWidth; + this.screen.height = screenHeight; + + this.view.width = screenWidth * this.resolution; + this.view.height = screenHeight * this.resolution; if (this.autoResize) { - this.view.style.width = `${this.width / this.resolution}px`; - this.view.style.height = `${this.height / this.resolution}px`; + this.view.style.width = `${screenWidth}px`; + this.view.style.height = `${screenHeight}px`; } } @@ -249,11 +267,10 @@ this.type = RENDERER_TYPE.UNKNOWN; - this.width = 0; - this.height = 0; - this.view = null; + this.screen = null; + this.resolution = 0; this.transparent = false; diff --git a/src/core/renderers/canvas/CanvasRenderer.js b/src/core/renderers/canvas/CanvasRenderer.js index 1bd2d5e..e84e49c 100644 --- a/src/core/renderers/canvas/CanvasRenderer.js +++ b/src/core/renderers/canvas/CanvasRenderer.js @@ -18,8 +18,8 @@ export default class CanvasRenderer extends SystemRenderer { /** - * @param {number} [width=800] - the width of the canvas view - * @param {number} [height=600] - the height of the canvas view + * @param {number} [screenWidth=800] - the width of the screen + * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false @@ -34,9 +34,9 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(width, height, options = {}) + constructor(screenWidth, screenHeight, options = {}) { - super('Canvas', width, height, options); + super('Canvas', screenWidth, screenHeight, options); this.type = RENDERER_TYPE.CANVAS; @@ -96,7 +96,7 @@ this.context = null; this.renderingToScreen = false; - this.resize(width, height); + this.resize(screenWidth, screenHeight); } /** @@ -283,12 +283,12 @@ * * @extends PIXI.SystemRenderer#resize * - * @param {number} width - The new width of the canvas view - * @param {number} height - The new height of the canvas view + * @param {number} screenWidth - the new width of the screen + * @param {number} screenHeight - the new height of the screen */ - resize(width, height) + resize(screenWidth, screenHeight) { - super.resize(width, height); + super.resize(screenWidth, screenHeight); // reset the scale mode.. oddly this seems to be reset when the canvas is resized. // surely a browser bug?? Let pixi fix that for you.. diff --git a/src/core/renderers/webgl/TextureManager.js b/src/core/renderers/webgl/TextureManager.js index b685c1a..8227289 100644 --- a/src/core/renderers/webgl/TextureManager.js +++ b/src/core/renderers/webgl/TextureManager.js @@ -61,7 +61,7 @@ * Updates and/or Creates a WebGL texture for the renderer's context. * * @param {PIXI.BaseTexture|PIXI.Texture} texture - the texture to update - * @param {Number} location - the location the texture will be bound to. + * @param {number} location - the location the texture will be bound to. * @return {GLTexture} The gl texture. */ updateTexture(texture, location) diff --git a/src/core/renderers/webgl/WebGLRenderer.js b/src/core/renderers/webgl/WebGLRenderer.js index e4cd8a0..830b382 100644 --- a/src/core/renderers/webgl/WebGLRenderer.js +++ b/src/core/renderers/webgl/WebGLRenderer.js @@ -31,8 +31,8 @@ { /** * - * @param {number} [width=0] - the width of the canvas view - * @param {number} [height=0] - the height of the canvas view + * @param {number} [screenWidth=800] - the width of the screen + * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false @@ -53,9 +53,9 @@ * @param {boolean} [options.legacy=false] - If true Pixi will aim to ensure compatibility * with older / less advanced devices. If you experiance unexplained flickering try setting this to true. */ - constructor(width, height, options = {}) + constructor(screenWidth, screenHeight, options = {}) { - super('WebGL', width, height, options); + super('WebGL', screenWidth, screenHeight, options); this.legacy = !!options.legacy; @@ -245,7 +245,7 @@ glCore._testingContext = gl; // setup the width/height properties and gl viewport - this.resize(this.width, this.height); + this.resize(this.screen.width, this.screen.height); } /** @@ -343,16 +343,16 @@ /** * Resizes the webGL view to the specified width and height. * - * @param {number} width - the new width of the webGL view - * @param {number} height - the new height of the webGL view + * @param {number} screenWidth - the new width of the screen + * @param {number} screenHeight - the new height of the screen */ - resize(width, height) + resize(screenWidth, screenHeight) { // if(width * this.resolution === this.width && height * this.resolution === this.height)return; - SystemRenderer.prototype.resize.call(this, width, height); + SystemRenderer.prototype.resize.call(this, screenWidth, screenHeight); - this.rootRenderTarget.resize(width, height); + this.rootRenderTarget.resize(screenWidth, screenHeight); if (this._activeRenderTarget === this.rootRenderTarget) { @@ -396,6 +396,26 @@ } /** + * Erases the render texture and fills the drawing area with a colour + * + * @param {PIXI.RenderTexture} renderTexture - The render texture to clear + * @param {number} [clearColor] - The colour + * @return {PIXI.WebGLRenderer} Returns itself. + */ + clearRenderTexture(renderTexture, clearColor) + { + const baseTexture = renderTexture.baseTexture; + const renderTarget = baseTexture._glRenderTargets[this.CONTEXT_UID]; + + if (renderTarget) + { + renderTarget.clear(clearColor); + } + + return this; + } + + /** * Binds a render texture for rendering * * @param {PIXI.RenderTexture} renderTexture - The render texture to render @@ -474,9 +494,10 @@ * Changes the current GLShader to the one given in parameter * * @param {PIXI.glCore.Shader} shader - the new glShader + * @param {boolean} [autoProject=true] - Whether automatically set the projection matrix * @return {PIXI.WebGLRenderer} Returns itself. */ - _bindGLShader(shader) + _bindGLShader(shader, autoProject) { // TODO cache if (this._activeShader !== shader) @@ -484,8 +505,14 @@ this._activeShader = shader; shader.bind(); - // automatically set the projection matrix - shader.uniforms.projectionMatrix = this._activeRenderTarget.projectionMatrix.toArray(true); + // `autoProject` normally would be a default parameter set to true + // but because of how Babel transpiles default parameters + // it hinders the performance of this method. + if (autoProject !== false) + { + // automatically set the projection matrix + shader.uniforms.projectionMatrix = this._activeRenderTarget.projectionMatrix.toArray(true); + } } return this; diff --git a/src/core/renderers/webgl/WebGLState.js b/src/core/renderers/webgl/WebGLState.js index ba6ecc5..6c846d5 100755 --- a/src/core/renderers/webgl/WebGLState.js +++ b/src/core/renderers/webgl/WebGLState.js @@ -82,18 +82,20 @@ push() { // next state.. - let state = this.stack[++this.stackIndex]; + let state = this.stack[this.stackIndex]; if (!state) { state = this.stack[this.stackIndex] = new Uint8Array(16); } + ++this.stackIndex; + // copy state.. // set active state so we can force overrides of gl state for (let i = 0; i < this.activeState.length; i++) { - this.activeState[i] = state[i]; + state[i] = this.activeState[i]; } } diff --git a/src/core/renderers/webgl/filters/Filter.js b/src/core/renderers/webgl/filters/Filter.js index 2c3eaad..4eb6128 100644 --- a/src/core/renderers/webgl/filters/Filter.js +++ b/src/core/renderers/webgl/filters/Filter.js @@ -9,7 +9,7 @@ * @memberof PIXI * @extends PIXI.Shader */ -class Filter extends Shader +export default class Filter extends Shader { /** * @param {string} [vertexSrc] - The source of the vertex shader. @@ -56,15 +56,16 @@ * @param {PIXI.RenderTarget} input - The input render target. * @param {PIXI.RenderTarget} output - The target to output to. * @param {boolean} clear - Should the output be cleared before rendering to it + * @param {object} [currentState] - It's current state of filter. + * There are some useful properties in the currentState : + * target, filters, sourceFrame, destinationFrame, renderTarget, resolution */ - apply(filterManager, input, output, clear) + apply(filterManager, input, output, clear, currentState) // eslint-disable-line no-unused-vars { // do as you please! - filterManager.applyFilter(this, input, output, clear); + filterManager.applyFilter(this, input, output, clear, currentState); // or just do a regular render.. } } - -export default Filter; diff --git a/src/core/renderers/webgl/filters/extractUniformsFromSrc.js b/src/core/renderers/webgl/filters/extractUniformsFromSrc.js index d78a0b1..4893214 100644 --- a/src/core/renderers/webgl/filters/extractUniformsFromSrc.js +++ b/src/core/renderers/webgl/filters/extractUniformsFromSrc.js @@ -12,7 +12,7 @@ function extractUniformsFromString(string) { - const maskRegex = new RegExp('^(projectionMatrix|uSampler|filterArea)$'); + const maskRegex = new RegExp('^(projectionMatrix|uSampler|filterArea|filterClamp)$'); const uniforms = {}; let nameSplit; diff --git a/src/core/renderers/webgl/managers/FilterManager.js b/src/core/renderers/webgl/managers/FilterManager.js index 43fc869..6c7422d 100644 --- a/src/core/renderers/webgl/managers/FilterManager.js +++ b/src/core/renderers/webgl/managers/FilterManager.js @@ -2,6 +2,7 @@ import RenderTarget from '../utils/RenderTarget'; import Quad from '../utils/Quad'; import { Rectangle } from '../../../math'; +import Shader from '../../../Shader'; import * as filterTransforms from '../filters/filterTransforms'; import bitTwiddle from 'bit-twiddle'; @@ -22,8 +23,6 @@ this.filters = []; this.target = null; this.resolution = 1; - - this.firstRun = true; } } @@ -155,7 +154,7 @@ if (filters.length === 1) { - filters[0].apply(this, currentState.renderTarget, lastState.renderTarget, false); + filters[0].apply(this, currentState.renderTarget, lastState.renderTarget, false, currentState); this.freePotRenderTarget(currentState.renderTarget); } else @@ -177,7 +176,7 @@ for (i = 0; i < filters.length - 1; ++i) { - filters[i].apply(this, flip, flop, true); + filters[i].apply(this, flip, flop, true, currentState); const t = flip; @@ -185,7 +184,7 @@ flop = t; } - filters[i].apply(this, flip, lastState.renderTarget, false); + filters[i].apply(this, flip, lastState.renderTarget, false, currentState); this.freePotRenderTarget(flip); this.freePotRenderTarget(flop); @@ -212,16 +211,28 @@ const renderer = this.renderer; const gl = renderer.gl; - renderer.bindShader(filter, true); + let shader = filter.glShaders[renderer.CONTEXT_UID]; - // TODO theres a better way! - // quad can be a mesh and we then should be able to pull this off without accessing the shader directly - const shader = renderer.shaderManager.getGLShader(); - - // TODO change quad to mesh.. - if (!this.firstRun) + // cacheing.. + if (!shader) { - this.firstRun = true; + if (filter.glShaderKey) + { + shader = this.shaderCache[filter.glShaderKey]; + + if (!shader) + { + shader = new Shader(this.gl, filter.vertexSrc, filter.fragmentSrc); + + filter.glShaders[renderer.CONTEXT_UID] = this.shaderCache[filter.glShaderKey] = shader; + } + } + else + { + shader = filter.glShaders[renderer.CONTEXT_UID] = new Shader(this.gl, filter.vertexSrc, filter.fragmentSrc); + } + + // TODO - this only needs to be done once? renderer.bindVao(null); this.quad.initVao(shader); @@ -244,58 +255,146 @@ renderer.maskManager.pushScissorMask(null, renderer.maskManager.scissorData); } - // renderer._bindGLShader(shader); + renderer.bindShader(shader); - let currentState; - const uniforms = filter.uniforms; + // free unit 0 for us, doesn't matter what was there + // don't try to restore it, because syncUniforms can upload it to another slot + // and it'll be a problem + const tex = this.renderer.emptyTextures[0]; - if (uniforms.filterArea) - { - currentState = this.filterData.stack[this.filterData.index]; - - const filterArea = uniforms.filterArea; - - filterArea[0] = currentState.renderTarget.size.width; - filterArea[1] = currentState.renderTarget.size.height; - filterArea[2] = currentState.sourceFrame.x; - filterArea[3] = currentState.sourceFrame.y; - - filter.uniforms.filterArea = filterArea; - } - - // use this to clamp displaced texture coords so they belong to filterArea - // see displacementFilter fragment shader for an example - if (uniforms.filterClamp) - { - currentState = this.filterData.stack[this.filterData.index]; - - const filterClamp = uniforms.filterClamp; - - filterClamp[0] = 0; - filterClamp[1] = 0; - filterClamp[2] = (currentState.sourceFrame.width - 1) / currentState.renderTarget.size.width; - filterClamp[3] = (currentState.sourceFrame.height - 1) / currentState.renderTarget.size.height; - - filter.uniforms.filterClamp = filterClamp; - } - - renderer.shaderManager.setUniforms(filter.uniforms); + this.renderer.boundTextures[0] = tex; + // this syncs the pixi filters uniforms with glsl uniforms + this.syncUniforms(shader, filter); renderer.state.setBlendMode(filter.blendMode); - // temporary bypass cache.. - const tex = this.renderer.boundTextures[0]; - gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, input.texture.texture); this.quad.vao.draw(this.renderer.gl.TRIANGLES, 6, 0); - // restore cache. gl.bindTexture(gl.TEXTURE_2D, tex._glTextures[this.renderer.CONTEXT_UID].texture); } /** + * Uploads the uniforms of the filter. + * + * @param {GLShader} shader - The underlying gl shader. + * @param {PIXI.Filter} filter - The filter we are synchronizing. + */ + syncUniforms(shader, filter) + { + const uniformData = filter.uniformData; + const uniforms = filter.uniforms; + + // 0 is reserved for the pixi texture so we start at 1! + let textureCount = 1; + let currentState; + + // filterArea and filterClamp that are handled by FilterManager directly + // they must not appear in uniformData + + if (shader.uniforms.filterArea) + { + currentState = this.filterData.stack[this.filterData.index]; + + const filterArea = shader.uniforms.filterArea; + + filterArea[0] = currentState.renderTarget.size.width; + filterArea[1] = currentState.renderTarget.size.height; + filterArea[2] = currentState.sourceFrame.x; + filterArea[3] = currentState.sourceFrame.y; + + shader.uniforms.filterArea = filterArea; + } + + // use this to clamp displaced texture coords so they belong to filterArea + // see displacementFilter fragment shader for an example + if (shader.uniforms.filterClamp) + { + currentState = currentState || this.filterData.stack[this.filterData.index]; + + const filterClamp = shader.uniforms.filterClamp; + + filterClamp[0] = 0; + filterClamp[1] = 0; + filterClamp[2] = (currentState.sourceFrame.width - 1) / currentState.renderTarget.size.width; + filterClamp[3] = (currentState.sourceFrame.height - 1) / currentState.renderTarget.size.height; + + shader.uniforms.filterClamp = filterClamp; + } + + // TODO Cacheing layer.. + for (const i in uniformData) + { + if (uniformData[i].type === 'sampler2D' && uniforms[i] !== 0) + { + if (uniforms[i].baseTexture) + { + shader.uniforms[i] = this.renderer.bindTexture(uniforms[i].baseTexture, textureCount); + } + else + { + shader.uniforms[i] = textureCount; + + // TODO + // this is helpful as renderTargets can also be set. + // Although thinking about it, we could probably + // make the filter texture cache return a RenderTexture + // rather than a renderTarget + const gl = this.renderer.gl; + + this.renderer.boundTextures[textureCount] = this.renderer.emptyTextures[textureCount]; + gl.activeTexture(gl.TEXTURE0 + textureCount); + + uniforms[i].texture.bind(); + } + + textureCount++; + } + else if (uniformData[i].type === 'mat3') + { + // check if its pixi matrix.. + if (uniforms[i].a !== undefined) + { + shader.uniforms[i] = uniforms[i].toArray(true); + } + else + { + shader.uniforms[i] = uniforms[i]; + } + } + else if (uniformData[i].type === 'vec2') + { + // check if its a point.. + if (uniforms[i].x !== undefined) + { + const val = shader.uniforms[i] || new Float32Array(2); + + val[0] = uniforms[i].x; + val[1] = uniforms[i].y; + shader.uniforms[i] = val; + } + else + { + shader.uniforms[i] = uniforms[i]; + } + } + else if (uniformData[i].type === 'float') + { + if (shader.uniforms.data[i].value !== uniformData[i]) + { + shader.uniforms[i] = uniforms[i]; + } + } + else + { + shader.uniforms[i] = uniforms[i]; + } + } + } + + /** * Gets a render target from the pool, or creates a new one. * * @param {boolean} clear - Should we clear the render texture when we get it? @@ -442,7 +541,6 @@ renderTarget.resolution = resolution; renderTarget.defaultFrame.width = renderTarget.size.width = minWidth / resolution; renderTarget.defaultFrame.height = renderTarget.size.height = minHeight / resolution; - renderTarget.attachStencilBuffer(); return renderTarget; } diff --git a/src/core/settings.js b/src/core/settings.js index bb08cb0..f4dd087 100644 --- a/src/core/settings.js +++ b/src/core/settings.js @@ -77,11 +77,11 @@ * * @static * @memberof PIXI.settings - * @type {RegExp|string} + * @type {RegExp} * @example `@2x` - * @default /@(.+)x/ + * @default /@([0-9\.]+)x/ */ - RETINA_PREFIX: /@(.+)x/, + RETINA_PREFIX: /@([0-9\.]+)x/, /** * The default render options if none are supplied to {@link PIXI.WebGLRenderer} @@ -175,14 +175,24 @@ SCALE_MODE: 0, /** - * Default specify float precision in shaders. + * Default specify float precision in vertex shader. + * + * @static + * @memberof PIXI.settings + * @type {PIXI.PRECISION} + * @default PIXI.PRECISION.HIGH + */ + PRECISION_VERTEX: 'highp', + + /** + * Default specify float precision in fragment shader. * * @static * @memberof PIXI.settings * @type {PIXI.PRECISION} * @default PIXI.PRECISION.MEDIUM */ - PRECISION: 'mediump', + PRECISION_FRAGMENT: 'mediump', /** * Can we upload the same buffer in a single frame? diff --git a/src/core/sprites/Sprite.js b/src/core/sprites/Sprite.js index 28b0b18..121bf58 100644 --- a/src/core/sprites/Sprite.js +++ b/src/core/sprites/Sprite.js @@ -212,11 +212,11 @@ } else { - w0 = orig.width * (1 - anchor._x); - w1 = orig.width * -anchor._x; + w1 = -anchor._x * orig.width; + w0 = w1 + orig.width; - h0 = orig.height * (1 - anchor._y); - h1 = orig.height * -anchor._y; + h1 = -anchor._y * orig.height; + h0 = h1 + orig.height; } // xy @@ -269,11 +269,11 @@ const tx = wt.tx; const ty = wt.ty; - const w0 = (orig.width) * (1 - anchor._x); - const w1 = (orig.width) * -anchor._x; + const w1 = -anchor._x * orig.width; + const w0 = w1 + orig.width; - const h0 = orig.height * (1 - anchor._y); - const h1 = orig.height * -anchor._y; + const h1 = -anchor._y * orig.height; + const h0 = h1 + orig.height; // xy vertexData[0] = (a * w1) + (c * h1) + tx; @@ -440,7 +440,7 @@ * * @static * @param {number|string|PIXI.BaseTexture|HTMLCanvasElement|HTMLVideoElement} source Source to create texture from - * @return {PIXI.Sprite} The newly created texture + * @return {PIXI.Sprite} The newly created sprite */ static from(source) { @@ -538,8 +538,8 @@ } /** - * The tint applied to the sprite. This is a hex value. A value of - * 0xFFFFFF will remove any tint effect. + * The tint applied to the sprite. This is a hex value. + * A value of 0xFFFFFF will remove any tint effect. * * @member {number} * @default 0xFFFFFF diff --git a/src/core/sprites/canvas/CanvasTinter.js b/src/core/sprites/canvas/CanvasTinter.js index 1cf316b..27e5e0c 100644 --- a/src/core/sprites/canvas/CanvasTinter.js +++ b/src/core/sprites/canvas/CanvasTinter.js @@ -75,8 +75,8 @@ crop.width *= resolution; crop.height *= resolution; - canvas.width = crop.width; - canvas.height = crop.height; + canvas.width = Math.ceil(crop.width); + canvas.height = Math.ceil(crop.height); context.fillStyle = `#${(`00000${(color | 0).toString(16)}`).substr(-6)}`; @@ -130,8 +130,8 @@ crop.width *= resolution; crop.height *= resolution; - canvas.width = crop.width; - canvas.height = crop.height; + canvas.width = Math.ceil(crop.width); + canvas.height = Math.ceil(crop.height); context.globalCompositeOperation = 'copy'; context.fillStyle = `#${(`00000${(color | 0).toString(16)}`).substr(-6)}`; @@ -172,8 +172,8 @@ crop.width *= resolution; crop.height *= resolution; - canvas.width = crop.width; - canvas.height = crop.height; + canvas.width = Math.ceil(crop.width); + canvas.height = Math.ceil(crop.height); context.globalCompositeOperation = 'copy'; context.drawImage( diff --git a/src/core/sprites/webgl/SpriteRenderer.js b/src/core/sprites/webgl/SpriteRenderer.js index 2affcd5..7c352e2 100644 --- a/src/core/sprites/webgl/SpriteRenderer.js +++ b/src/core/sprites/webgl/SpriteRenderer.js @@ -30,7 +30,7 @@ /** * Number of values sent in the vertex buffer. - * positionX, positionY, colorR, colorG, colorB = 5 + * aVertexPosition(2), aTextureCoord(1), aColor(1), aTextureId(1) = 5 * * @member {number} */ @@ -75,7 +75,6 @@ this.shader = null; this.currentIndex = 0; - TICK = 0; this.groups = []; for (let k = 0; k < this.size; k++) diff --git a/src/core/textures/BaseTexture.js b/src/core/textures/BaseTexture.js index 853de7e..aeccf39 100644 --- a/src/core/textures/BaseTexture.js +++ b/src/core/textures/BaseTexture.js @@ -232,16 +232,24 @@ this.realWidth = this.source.naturalWidth || this.source.videoWidth || this.source.width; this.realHeight = this.source.naturalHeight || this.source.videoHeight || this.source.height; - this.width = this.realWidth / this.resolution; - this.height = this.realHeight / this.resolution; - - this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); + this._updateDimensions(); } this.emit('update', this); } /** + * Update dimensions from real values + */ + _updateDimensions() + { + this.width = this.realWidth / this.resolution; + this.height = this.realHeight / this.resolution; + + this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); + } + + /** * Load a source. * * If the source is not-immediately-available, such as an image that needs to be @@ -525,11 +533,7 @@ this.realWidth = Math.round(svgWidth * this.sourceScale); this.realHeight = Math.round(svgHeight * this.sourceScale); - this.width = this.realWidth / this.resolution; - this.height = this.realHeight / this.resolution; - - // Check pow2 after scale - this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); + this._updateDimensions(); // Create a canvas element const canvas = document.createElement('canvas'); @@ -690,4 +694,52 @@ return baseTexture; } + + /** + * Helper function that creates a base texture based on the source you provide. + * The source can be - image url, image element, canvas element. + * + * @static + * @param {string|HTMLImageElement|HTMLCanvasElement} source - The source to create base texture from. + * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values + * @param {number} [sourceScale=(auto)] - Scale for the original image, used with Svg images. + * @return {PIXI.BaseTexture} The new base texture. + */ + static from(source, scaleMode, sourceScale) + { + if (typeof source === 'string') + { + return BaseTexture.fromImage(source, undefined, scaleMode, sourceScale); + } + else if (source instanceof HTMLImageElement) + { + const imageUrl = source.src; + let baseTexture = BaseTextureCache[imageUrl]; + + if (!baseTexture) + { + baseTexture = new BaseTexture(source, scaleMode); + baseTexture.imageUrl = imageUrl; + + if (sourceScale) + { + baseTexture.sourceScale = sourceScale; + } + + // if there is an @2x at the end of the url we are going to assume its a highres image + baseTexture.resolution = getResolutionOfUrl(imageUrl); + + BaseTextureCache[imageUrl] = baseTexture; + } + + return baseTexture; + } + else if (source instanceof HTMLCanvasElement) + { + return BaseTexture.fromCanvas(source, scaleMode); + } + + // lets assume its a base texture! + return source; + } } diff --git a/src/core/textures/Spritesheet.js b/src/core/textures/Spritesheet.js new file mode 100644 index 0000000..d469c62 --- /dev/null +++ b/src/core/textures/Spritesheet.js @@ -0,0 +1,274 @@ +import { Rectangle, Texture } from '../'; +import { getResolutionOfUrl, TextureCache } from '../utils'; + +/** + * Utility class for maintaining reference to a collection + * of Textures on a single Spritesheet. + * + * @class + * @memberof PIXI + */ +export default class Spritesheet +{ + /** + * The maximum number of Textures to build per process. + * + * @type {number} + * @default 1000 + */ + static get BATCH_SIZE() + { + return 1000; + } + + /** + * @param {PIXI.BaseTexture} baseTexture Reference to the source BaseTexture object. + * @param {Object} data - Spritesheet image data. + * @param {string} [resolutionFilename] - The filename to consider when determining + * the resolution of the spritesheet. If not provided, the imageUrl will + * be used on the BaseTexture. + */ + constructor(baseTexture, data, resolutionFilename = null) + { + /** + * Reference to ths source texture + * @type {PIXI.BaseTexture} + */ + this.baseTexture = baseTexture; + + /** + * Map of spritesheet textures. + * @type {Object} + */ + this.textures = {}; + + /** + * Reference to the original JSON data. + * @type {Object} + */ + this.data = data; + + /** + * The resolution of the spritesheet. + * @type {number} + */ + this.resolution = this._updateResolution( + resolutionFilename || this.baseTexture.imageUrl + ); + + /** + * Map of spritesheet frames. + * @type {Object} + * @private + */ + this._frames = this.data.frames; + + /** + * Collection of frame names. + * @type {string[]} + * @private + */ + this._frameKeys = Object.keys(this._frames); + + /** + * Current batch index being processed. + * @type {number} + * @private + */ + this._batchIndex = 0; + + /** + * Callback when parse is completed. + * @type {Function} + * @private + */ + this._callback = null; + } + + /** + * Generate the resolution from the filename or fallback + * to the meta.scale field of the JSON data. + * + * @private + * @param {string} resolutionFilename - The filename to use for resolving + * the default resolution. + * @return {number} Resolution to use for spritesheet. + */ + _updateResolution(resolutionFilename) + { + const scale = this.data.meta.scale; + + // Use a defaultValue of `null` to check if a url-based resolution is set + let resolution = getResolutionOfUrl(resolutionFilename, null); + + // No resolution found via URL + if (resolution === null) + { + // Use the scale value or default to 1 + resolution = scale !== undefined ? parseFloat(scale) : 1; + } + + // For non-1 resolutions, update baseTexture + if (resolution !== 1) + { + this.baseTexture.resolution = resolution; + this.baseTexture.update(); + } + + return resolution; + } + + /** + * Parser spritesheet from loaded data. This is done asynchronously + * to prevent creating too many Texture within a single process. + * + * @param {Function} callback - Callback when complete returns + * a map of the Textures for this spritesheet. + */ + parse(callback) + { + this._batchIndex = 0; + this._callback = callback; + + if (this._frameKeys.length <= Spritesheet.BATCH_SIZE) + { + this._processFrames(0); + this._parseComplete(); + } + else + { + this._nextBatch(); + } + } + + /** + * Process a batch of frames + * + * @private + * @param {number} initialFrameIndex - The index of frame to start. + */ + _processFrames(initialFrameIndex) + { + let frameIndex = initialFrameIndex; + const maxFrames = Spritesheet.BATCH_SIZE; + + while (frameIndex - initialFrameIndex < maxFrames && frameIndex < this._frameKeys.length) + { + const i = this._frameKeys[frameIndex]; + const rect = this._frames[i].frame; + + if (rect) + { + let frame = null; + let trim = null; + const orig = new Rectangle( + 0, + 0, + this._frames[i].sourceSize.w / this.resolution, + this._frames[i].sourceSize.h / this.resolution + ); + + if (this._frames[i].rotated) + { + frame = new Rectangle( + rect.x / this.resolution, + rect.y / this.resolution, + rect.h / this.resolution, + rect.w / this.resolution + ); + } + else + { + frame = new Rectangle( + rect.x / this.resolution, + rect.y / this.resolution, + rect.w / this.resolution, + rect.h / this.resolution + ); + } + + // Check to see if the sprite is trimmed + if (this._frames[i].trimmed) + { + trim = new Rectangle( + this._frames[i].spriteSourceSize.x / this.resolution, + this._frames[i].spriteSourceSize.y / this.resolution, + rect.w / this.resolution, + rect.h / this.resolution + ); + } + + this.textures[i] = new Texture( + this.baseTexture, + frame, + orig, + trim, + this._frames[i].rotated ? 2 : 0 + ); + + // lets also add the frame to pixi's global cache for fromFrame and fromImage functions + TextureCache[i] = this.textures[i]; + } + + frameIndex++; + } + } + + /** + * The parse has completed. + * + * @private + */ + _parseComplete() + { + const callback = this._callback; + + this._callback = null; + this._batchIndex = 0; + callback.call(this, this.textures); + } + + /** + * Begin the next batch of textures. + * + * @private + */ + _nextBatch() + { + this._processFrames(this._batchIndex * Spritesheet.BATCH_SIZE); + this._batchIndex++; + setTimeout(() => + { + if (this._batchIndex * Spritesheet.BATCH_SIZE < this._frameKeys.length) + { + this._nextBatch(); + } + else + { + this._parseComplete(); + } + }, 0); + } + + /** + * Destroy Spritesheet and don't use after this. + * + * @param {boolean} [destroyBase=false] Whether to destroy the base texture as well + */ + destroy(destroyBase = false) + { + for (const i in this.textures) + { + this.textures[i].destroy(); + } + this._frames = null; + this._frameKeys = null; + this.data = null; + this.textures = null; + if (destroyBase) + { + this.baseTexture.destroy(); + } + this.baseTexture = null; + } +} diff --git a/src/core/textures/Texture.js b/src/core/textures/Texture.js index 6403d4f..afe4254 100644 --- a/src/core/textures/Texture.js +++ b/src/core/textures/Texture.js @@ -3,7 +3,7 @@ import TextureUvs from './TextureUvs'; import EventEmitter from 'eventemitter3'; import { Rectangle } from '../math'; -import { TextureCache, BaseTextureCache } from '../utils'; +import { TextureCache, BaseTextureCache, getResolutionOfUrl } from '../utils'; /** * A texture stores the information that represents an image or part of an image. It cannot be added @@ -365,7 +365,8 @@ * The source can be - frame id, image url, video url, canvas element, video element, base texture * * @static - * @param {number|string|PIXI.BaseTexture|HTMLCanvasElement|HTMLVideoElement} source - Source to create texture from + * @param {number|string|HTMLImageElement|HTMLCanvasElement|HTMLVideoElement|PIXI.BaseTexture} + * source - Source to create texture from * @return {PIXI.Texture} The newly created texture */ static from(source) @@ -393,7 +394,7 @@ } else if (source instanceof HTMLImageElement) { - return new Texture(new BaseTexture(source)); + return new Texture(BaseTexture.from(source)); } else if (source instanceof HTMLCanvasElement) { @@ -413,6 +414,43 @@ } /** + * Create a texture from a source and add to the cache. + * + * @static + * @param {HTMLImageElement|HTMLCanvasElement} source - The input source. + * @param {String} imageUrl - File name of texture, for cache and resolving resolution. + * @param {String} [name] - Human readible name for the texture cache. If no name is + * specified, only `imageUrl` will be used as the cache ID. + * @return {PIXI.Texture} Output texture + */ + static fromLoader(source, imageUrl, name) + { + const baseTexture = new BaseTexture(source, null, getResolutionOfUrl(imageUrl)); + const texture = new Texture(baseTexture); + + baseTexture.imageUrl = imageUrl; + + // No name, use imageUrl instead + if (!name) + { + name = imageUrl; + } + + // lets also add the frame to pixi's global cache for fromFrame and fromImage fucntions + BaseTextureCache[name] = baseTexture; + TextureCache[name] = texture; + + // also add references by url if they are different. + if (name !== imageUrl) + { + BaseTextureCache[imageUrl] = baseTexture; + TextureCache[imageUrl] = texture; + } + + return texture; + } + + /** * Adds a texture to the global TextureCache. This cache is shared across the whole PIXI object. * * @static @@ -522,6 +560,29 @@ } } +function createWhiteTexture() +{ + const canvas = document.createElement('canvas'); + + canvas.width = 10; + canvas.height = 10; + + const context = canvas.getContext('2d'); + + context.fillStyle = 'white'; + context.fillRect(0, 0, 10, 10); + + return new Texture(new BaseTexture(canvas)); +} + +function removeAllHandlers(tex) +{ + tex.destroy = function _emptyDestroy() { /* empty */ }; + tex.on = function _emptyOn() { /* empty */ }; + tex.once = function _emptyOnce() { /* empty */ }; + tex.emit = function _emptyEmit() { /* empty */ }; +} + /** * An empty texture, used often to not have to create multiple empty textures. * Can not be destroyed. @@ -530,7 +591,14 @@ * @constant */ Texture.EMPTY = new Texture(new BaseTexture()); -Texture.EMPTY.destroy = function _emptyDestroy() { /* empty */ }; -Texture.EMPTY.on = function _emptyOn() { /* empty */ }; -Texture.EMPTY.once = function _emptyOnce() { /* empty */ }; -Texture.EMPTY.emit = function _emptyEmit() { /* empty */ }; +removeAllHandlers(Texture.EMPTY); + +/** + * A white texture of 10x10 size, used for graphics and other things + * Can not be destroyed. + * + * @static + * @constant + */ +Texture.WHITE = createWhiteTexture(); +removeAllHandlers(Texture.WHITE); diff --git a/src/core/ticker/Ticker.js b/src/core/ticker/Ticker.js index d98a1f0..17a1517 100644 --- a/src/core/ticker/Ticker.js +++ b/src/core/ticker/Ticker.js @@ -67,11 +67,12 @@ * is based, this value is neither capped nor scaled. * If the platform supports DOMHighResTimeStamp, * this value will have a precision of 1 µs. + * Defaults to target frame time * * @member {number} - * @default 1 / TARGET_FPMS + * @default 16.66 */ - this.elapsedMS = 1 / settings.TARGET_FPMS; // default to target frame time + this.elapsedMS = 1 / settings.TARGET_FPMS; /** * The last time {@link PIXI.ticker.Ticker#update} was invoked. diff --git a/src/core/utils/index.js b/src/core/utils/index.js index 6ff609f..be44750 100644 --- a/src/core/utils/index.js +++ b/src/core/utils/index.js @@ -2,6 +2,7 @@ import settings from '../settings'; import EventEmitter from 'eventemitter3'; import pluginTarget from './pluginTarget'; +import * as mixins from './mixin'; import * as isMobile from 'ismobilejs'; let nextUid = 0; @@ -33,6 +34,7 @@ * @type {mixin} */ pluginTarget, + mixins, }; /** @@ -93,7 +95,7 @@ */ export function rgb2hex(rgb) { - return (((rgb[0] * 255) << 16) + ((rgb[1] * 255) << 8) + (rgb[2] * 255)); + return (((rgb[0] * 255) << 16) + ((rgb[1] * 255) << 8) + (rgb[2] * 255 | 0)); } /** @@ -368,3 +370,43 @@ * @private */ export const ProgramCache = {}; + +/** + * Destroys all texture in the cache + * + * @memberof PIXI.utils + * @function destroyTextureCache + */ +export function destroyTextureCache() +{ + let key; + + for (key in TextureCache) + { + TextureCache[key].destroy(); + } + for (key in BaseTextureCache) + { + BaseTextureCache[key].destroy(); + } +} + +/** + * Removes all textures from cache, but does not destroy them + * + * @memberof PIXI.utils + * @function clearTextureCache + */ +export function clearTextureCache() +{ + let key; + + for (key in TextureCache) + { + delete TextureCache[key]; + } + for (key in BaseTextureCache) + { + delete BaseTextureCache[key]; + } +} diff --git a/src/core/utils/mixin.js b/src/core/utils/mixin.js new file mode 100644 index 0000000..114de2a --- /dev/null +++ b/src/core/utils/mixin.js @@ -0,0 +1,59 @@ +/** + * Mixes all enumerable properties and methods from a source object to a target object. + * + * @memberof PIXI.utils.mixins + * @function mixin + * @param {object} target The prototype or instance that properties and methods should be added to. + * @param {object} source The source of properties and methods to mix in. + */ +export function mixin(target, source) +{ + if (!target || !source) return; + // in ES8/ES2017, this would be really easy: + // Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); + + // get all the enumerable property keys + const keys = Object.keys(source); + + // loop through properties + for (let i = 0; i < keys.length; ++i) + { + const propertyName = keys[i]; + + // Set the property using the property descriptor - this works for accessors and normal value properties + Object.defineProperty(target, propertyName, Object.getOwnPropertyDescriptor(source, propertyName)); + } +} + +const mixins = []; + +/** + * Queues a mixin to be handled towards the end of the initialization of PIXI, so that deprecation + * can take effect. + * + * @memberof PIXI.utils.mixins + * @function delayMixin + * @private + * @param {object} target The prototype or instance that properties and methods should be added to. + * @param {object} source The source of properties and methods to mix in. + */ +export function delayMixin(target, source) +{ + mixins.push(target, source); +} + +/** + * Handles all mixins queued via delayMixin(). + * + * @memberof PIXI.utils.mixins + * @function performMixins + * @private + */ +export function performMixins() +{ + for (let i = 0; i < mixins.length; i += 2) + { + mixin(mixins[i], mixins[i + 1]); + } + mixins.length = 0; +} diff --git a/src/deprecation.js b/src/deprecation.js index 045a19f..b9c73b3 100644 --- a/src/deprecation.js +++ b/src/deprecation.js @@ -5,6 +5,7 @@ import * as filters from './filters'; import * as prepare from './prepare'; import * as loaders from './loaders'; +import * as interaction from './interaction'; // provide method to give a stack track for warnings // useful for tracking-down where deprecated methods/properties/classes @@ -534,7 +535,7 @@ { parent: 'GC_MODES', target: 'GC_MODE' }, { parent: 'WRAP_MODES', target: 'WRAP_MODE' }, { parent: 'SCALE_MODES', target: 'SCALE_MODE' }, - { parent: 'PRECISION', target: 'PRECISION' }, + { parent: 'PRECISION', target: 'PRECISION_FRAGMENT' }, ]; for (let i = 0; i < defaults.length; i++) @@ -558,6 +559,32 @@ }); } +Object.defineProperties(core.settings, { + + /** + * @static + * @name PRECISION + * @memberof PIXI.settings + * @see PIXI.PRECISION + * @deprecated since version 4.4.0 + */ + PRECISION: { + enumerable: true, + get() + { + warn('PIXI.settings.PRECISION has been deprecated, please use PIXI.settings.PRECISION_FRAGMENT'); + + return core.settings.PRECISION_FRAGMENT; + }, + set(value) + { + warn('PIXI.settings.PRECISION has been deprecated, please use PIXI.settings.PRECISION_FRAGMENT'); + + core.settings.PRECISION_FRAGMENT = value; + }, + }, +}); + Object.defineProperties(extras, { /** @@ -694,7 +721,7 @@ warn('determineFontProperties is now deprecated, please use the static calculateFontProperties method, ' + 'e.g : Text.calculateFontProperties(fontStyle);'); - return Text.calculateFontProperties(fontStyle); + return core.Text.calculateFontProperties(fontStyle); }; Object.defineProperties(core.TextStyle.prototype, { @@ -1003,3 +1030,69 @@ }, }, }); + +/** + * @name PIXI.interaction.interactiveTarget#defaultCursor + * @static + * @type {number} + * @see PIXI.interaction.interactiveTarget#cursor + * @deprecated since 4.3.0 + */ +Object.defineProperty(interaction.interactiveTarget, 'defaultCursor', { + set(value) + { + warn('Property defaultCursor has been replaced with \'cursor\'. '); + this.cursor = value; + }, + get() + { + warn('Property defaultCursor has been replaced with \'cursor\'. '); + + return this.cursor; + }, + enumerable: true, +}); + +/** + * @name PIXI.interaction.InteractionManager#defaultCursorStyle + * @static + * @type {string} + * @see PIXI.interaction.InteractionManager#cursorStyles + * @deprecated since 4.3.0 + */ +Object.defineProperty(interaction.InteractionManager, 'defaultCursorStyle', { + set(value) + { + warn('Property defaultCursorStyle has been replaced with \'cursorStyles.default\'. '); + this.cursorStyles.default = value; + }, + get() + { + warn('Property defaultCursorStyle has been replaced with \'cursorStyles.default\'. '); + + return this.cursorStyles.default; + }, +}); + +/** + * @name PIXI.interaction.InteractionManager#currentCursorStyle + * @static + * @type {string} + * @see PIXI.interaction.InteractionManager#cursorStyles + * @deprecated since 4.3.0 + */ +Object.defineProperty(interaction.InteractionManager, 'currentCursorStyle', { + set(value) + { + warn('Property currentCursorStyle has been removed.' + + 'See the currentCursorMode property, which works differently.'); + this.currentCursorMode = value; + }, + get() + { + warn('Property currentCursorStyle has been removed.' + + 'See the currentCursorMode property, which works differently.'); + + return this.currentCursorMode; + }, +}); diff --git a/src/extras/BitmapText.js b/src/extras/BitmapText.js index fb6c343..0a0e150 100644 --- a/src/extras/BitmapText.js +++ b/src/extras/BitmapText.js @@ -455,6 +455,73 @@ return this._textHeight; } + + /** + * Register a bitmap font with data and a texture. + * + * @static + * @param {XMLDocument} xml - The XML document data. + * @param {PIXI.Texture} texture - Texture with all symbols. + * @return {Object} Result font object with font, size, lineHeight and char fields. + */ + static registerFont(xml, texture) + { + const data = {}; + const info = xml.getElementsByTagName('info')[0]; + const common = xml.getElementsByTagName('common')[0]; + + data.font = info.getAttribute('face'); + data.size = parseInt(info.getAttribute('size'), 10); + data.lineHeight = parseInt(common.getAttribute('lineHeight'), 10); + data.chars = {}; + + // parse letters + const letters = xml.getElementsByTagName('char'); + + for (let i = 0; i < letters.length; i++) + { + const letter = letters[i]; + const charCode = parseInt(letter.getAttribute('id'), 10); + + const textureRect = new core.Rectangle( + parseInt(letter.getAttribute('x'), 10) + texture.frame.x, + parseInt(letter.getAttribute('y'), 10) + texture.frame.y, + parseInt(letter.getAttribute('width'), 10), + parseInt(letter.getAttribute('height'), 10) + ); + + data.chars[charCode] = { + xOffset: parseInt(letter.getAttribute('xoffset'), 10), + yOffset: parseInt(letter.getAttribute('yoffset'), 10), + xAdvance: parseInt(letter.getAttribute('xadvance'), 10), + kerning: {}, + texture: new core.Texture(texture.baseTexture, textureRect), + + }; + } + + // parse kernings + const kernings = xml.getElementsByTagName('kerning'); + + for (let i = 0; i < kernings.length; i++) + { + const kerning = kernings[i]; + const first = parseInt(kerning.getAttribute('first'), 10); + const second = parseInt(kerning.getAttribute('second'), 10); + const amount = parseInt(kerning.getAttribute('amount'), 10); + + if (data.chars[second]) + { + data.chars[second].kerning[first] = amount; + } + } + + // I'm leaving this as a temporary fix so we can test the bitmap fonts in v3 + // but it's very likely to change + BitmapText.fonts[data.font] = data; + + return data; + } } BitmapText.fonts = {}; diff --git a/src/index.js b/src/index.js index 59356cf..c553427 100644 --- a/src/index.js +++ b/src/index.js @@ -16,6 +16,10 @@ import * as particles from './particles'; import * as prepare from './prepare'; +// handle mixins now, after all code has been added, including deprecation +import { utils } from './core'; +utils.mixins.performMixins(); + export { accessibility, extract, diff --git a/src/interaction/InteractionManager.js b/src/interaction/InteractionManager.js index 7d51134..8baf13a 100644 --- a/src/interaction/InteractionManager.js +++ b/src/interaction/InteractionManager.js @@ -1,18 +1,20 @@ 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'; -// Mix interactiveTarget into core.DisplayObject.prototype -Object.assign( +// Mix interactiveTarget into core.DisplayObject.prototype, after deprecation has been handled +core.utils.mixins.delayMixin( core.DisplayObject.prototype, 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 +70,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 +101,6 @@ this.eventData = new InteractionEvent(); /** - * Tiny little interactiveData pool ! - * - * @member {PIXI.interaction.InteractionData[]} - */ - this.interactiveDataPool = []; - - /** * The DOM element to bind to. * * @private @@ -153,66 +155,12 @@ */ this.supportsPointerEvents = !!window.PointerEvent; - /** - * Are touch events being 'normalized' and converted into pointer events if pointer events are not supported - * For example, on a touch screen mobile device, a touchstart would also be emitted as a pointerdown - * - * @private - * @readonly - * @member {boolean} - */ - this.normalizeTouchEvents = !this.supportsPointerEvents && this.supportsTouchEvents; - - /** - * Are mouse events being 'normalized' and converted into pointer events if pointer events are not supported - * For example, on a desktop pc, a mousedown would also be emitted as a pointerdown - * - * @private - * @readonly - * @member {boolean} - */ - this.normalizeMouseEvents = !this.supportsPointerEvents && !MobileDevice.any; - // this will make it so that you don't have to call bind all the time /** * @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 +168,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,41 +199,32 @@ this.onPointerOver = this.onPointerOver.bind(this); /** - * @private - * @member {Function} + * Dictionary of how different cursor modes are handled. Strings are handled as CSS cursor + * values, objects are handled as dictionaries of CSS values for interactionDOMElement, + * and functions are called instead of changing the CSS. + * Default CSS cursor values are provided for 'default' and 'pointer' modes. + * @member {Object.)>} */ - this.onTouchStart = this.onTouchStart.bind(this); - this.processTouchStart = this.processTouchStart.bind(this); + this.cursorStyles = { + default: 'inherit', + pointer: 'pointer', + }; /** - * @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. - * - * @member {string} - * @default 'inherit' - */ - this.defaultCursorStyle = 'inherit'; - - /** - * The css style of the cursor that is being used. + * The mode of the cursor that is being used. + * The value of this is a key from the cursorStyles dictionary. * * @member {string} */ - this.currentCursorStyle = 'inherit'; + this.currentCursorMode = null; + + /** + * Internal cached let. + * + * @private + * @member {string} + */ + this.cursor = null; /** * Internal cached let. @@ -303,6 +249,7 @@ * object. * * @event mousedown + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -311,6 +258,7 @@ * on the display object. * * @event rightdown + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -319,6 +267,7 @@ * object. * * @event mouseup + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -327,6 +276,7 @@ * over the display object. * * @event rightup + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -335,6 +285,7 @@ * the display object. * * @event click + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -343,6 +294,7 @@ * and released on the display object. * * @event rightclick + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -352,6 +304,7 @@ * [mousedown]{@link PIXI.interaction.InteractionManager#event:mousedown}. * * @event mouseupoutside + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -361,6 +314,7 @@ * [rightdown]{@link PIXI.interaction.InteractionManager#event:rightdown}. * * @event rightupoutside + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -368,6 +322,7 @@ * Fired when a pointer device (usually a mouse) is moved while over the display object * * @event mousemove + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -375,6 +330,7 @@ * Fired when a pointer device (usually a mouse) is moved onto the display object * * @event mouseover + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -382,6 +338,7 @@ * Fired when a pointer device (usually a mouse) is moved off the display object * * @event mouseout + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -389,6 +346,7 @@ * Fired when a pointer device button is pressed on the display object. * * @event pointerdown + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -396,6 +354,14 @@ * Fired when a pointer device button is released over the display object. * * @event pointerup + * @type {PIXI.interaction.InteractionData} + * @memberof PIXI.interaction.InteractionManager# + */ + + /** + * Fired when the operating system cancels a pointer event + * + * @event pointercancel * @memberof PIXI.interaction.InteractionManager# */ @@ -403,6 +369,7 @@ * Fired when a pointer device button is pressed and released on the display object. * * @event pointertap + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -411,6 +378,7 @@ * registered a [pointerdown]{@link PIXI.interaction.InteractionManager#event:pointerdown}. * * @event pointerupoutside + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -418,6 +386,7 @@ * Fired when a pointer device is moved while over the display object * * @event pointermove + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -425,6 +394,7 @@ * Fired when a pointer device is moved onto the display object * * @event pointerover + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -432,6 +402,7 @@ * Fired when a pointer device is moved off the display object * * @event pointerout + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -439,6 +410,7 @@ * Fired when a touch point is placed on the display object. * * @event touchstart + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -446,6 +418,14 @@ * Fired when a touch point is removed from the display object. * * @event touchend + * @type {PIXI.interaction.InteractionData} + * @memberof PIXI.interaction.InteractionManager# + */ + + /** + * Fired when the operating system cancels a touch + * + * @event touchcancel * @memberof PIXI.interaction.InteractionManager# */ @@ -453,6 +433,7 @@ * Fired when a touch point is placed and removed from the display object. * * @event tap + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -461,6 +442,7 @@ * registered a [touchstart]{@link PIXI.interaction.InteractionManager#event:touchstart}. * * @event touchendoutside + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -468,6 +450,7 @@ * Fired when a touch point is moved along the display object. * * @event touchmove + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ } @@ -524,45 +507,30 @@ { window.document.addEventListener('pointermove', this.onPointerMove, true); this.interactionDOMElement.addEventListener('pointerdown', this.onPointerDown, true); - this.interactionDOMElement.addEventListener('pointerout', this.onPointerOut, true); + // pointerout is fired in addition to pointerup (for touch events) and pointercancel + // we already handle those, so for the purposes of what we do in onPointerOut, we only + // care about the pointerleave event + this.interactionDOMElement.addEventListener('pointerleave', 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; @@ -596,45 +564,26 @@ { window.document.removeEventListener('pointermove', this.onPointerMove, true); this.interactionDOMElement.removeEventListener('pointerdown', this.onPointerDown, true); - this.interactionDOMElement.removeEventListener('pointerout', this.onPointerOut, true); + this.interactionDOMElement.removeEventListener('pointerleave', 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) + 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.onPointerDown, true); + this.interactionDOMElement.removeEventListener('touchcancel', this.onPointerCancel, 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); - - 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 = null; @@ -664,7 +613,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; @@ -672,25 +621,80 @@ return; } - this.cursor = this.defaultCursorStyle; + this.cursor = null; // 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(); - - this.processInteractive(this.mouse.global, this.renderer._lastObjectRendered, this.processMouseOverOut, true); - - if (this.currentCursorStyle !== this.cursor) + for (const k in this.activeInteractionData) { - this.currentCursorStyle = this.cursor; - this.interactionDOMElement.style.cursor = this.cursor; + // eslint-disable-next-line no-prototype-builtins + if (this.activeInteractionData.hasOwnProperty(k)) + { + const interactionData = this.activeInteractionData[k]; + + if (interactionData.originalEvent && interactionData.pointerType !== 'touch') + { + const interactionEvent = this.configureInteractionEventForDOMEvent( + this.eventData, + interactionData.originalEvent, + interactionData + ); + + this.processInteractive( + interactionEvent, + this.renderer._lastObjectRendered, + this.processPointerOverOut, + true + ); + } + } } + this.setCursorMode(this.cursor); + // TODO } /** + * Sets the current cursor mode, handling any callbacks or CSS style changes. + * + * @param {string} mode - cursor mode, a key from the cursorStyles dictionary + */ + setCursorMode(mode) + { + mode = mode || 'default'; + // if the mode didn't actually change, bail early + if (this.currentCursorMode === mode) + { + return; + } + this.currentCursorMode = mode; + const style = this.cursorStyles[mode]; + + // only do things if there is a cursor style for it + if (style) + { + switch (typeof style) + { + case 'string': + // string styles are handled as cursor CSS + this.interactionDOMElement.style.cursor = style; + break; + case 'function': + // functions are just called, and passed the cursor mode + style(mode); + break; + case 'object': + // if it is an object, assume that it is a dictionary of CSS styles, + // apply it to the interactionDOMElement + Object.assign(this.interactionDOMElement.style, style); + break; + } + } + } + + /** * Dispatches an event on the display object that was interacted with * * @param {PIXI.Container|PIXI.Sprite|PIXI.extras.TilingSprite} displayObject - the display object in question @@ -748,22 +752,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 @@ -787,9 +795,8 @@ { interactiveParent = false; } - - // it has a mask! Then lets hit test that before continuing.. - if (hitTest && displayObject._mask) + // it has a mask! Then lets hit test that before continuing + else if (hitTest && displayObject._mask) { if (!displayObject._mask.containsPoint(point)) { @@ -797,14 +804,7 @@ } } - // it has a filterArea! Same as mask but easier, its a rectangle - if (hitTest && displayObject.filterArea) - { - if (!displayObject.filterArea.contains(point.x, point.y)) - { - hitTest = false; - } - } + let keepHitTestingAfterChildren = hitTest; // ** FREE TIP **! If an object is not interactive or has no buttons in it // (such as a game scene!) set interactiveChildren to false for that displayObject. @@ -818,7 +818,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 @@ -838,15 +838,20 @@ // This means we no longer need to hit test anything else. We still need to run // through all objects, but we don't need to perform any hit tests. - // { - hitTest = false; - // } + keepHitTestingAfterChildren = false; + + if (child.interactive) + { + hitTest = false; + } // we can break now as we have hit an object. } } } + hitTest = keepHitTestingAfterChildren; + // no point running this if the item is not interactive or does not have an interactive parent. if (interactive) { @@ -868,14 +873,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 +886,175 @@ } /** - * 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].isNormalized) { - 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); + if (!displayObject.trackedPointers[id]) + { + displayObject.trackedPointers[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.trackedPointers[id].rightDown = true; + } + else + { + displayObject.trackedPointers[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.trackedPointers[id] !== undefined) + { + delete displayObject.trackedPointers[id]; + this.dispatchEvent(displayObject, 'pointercancel', interactionEvent); + + if (e.type === 'touchcancel' || e.pointerType === 'touch') + { + this.dispatchEvent(displayObject, 'touchcancel', interactionEvent); + } } } @@ -1159,42 +1066,94 @@ */ 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) { - if (hit) - { - this.dispatchEvent(displayObject, 'pointerup', this.eventData); + const e = interactionEvent.data.originalEvent; - if (displayObject._pointerDown) + const id = interactionEvent.data.identifier; + + const trackingData = displayObject.trackedPointers[id]; + + const isTouch = (e.type === 'touchend' || e.pointerType === 'touch'); + + const isMouse = (e.type.indexOf('mouse') === 0 || e.pointerType === 'mouse'); + + // 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) { - displayObject._pointerDown = false; - this.dispatchEvent(displayObject, 'pointertap', this.eventData); + 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); + } + // update the down state of the tracking data + if (trackingData) + { + if (isRightButton) + { + trackingData.rightDown = hit; + } + else + { + trackingData.leftDown = hit; + } } } - else if (displayObject._pointerDown) + + // Pointers and Touches, and Mouse + if (hit) { - displayObject._pointerDown = false; - this.dispatchEvent(displayObject, 'pointerupoutside', this.eventData); + this.dispatchEvent(displayObject, 'pointerup', interactionEvent); + if (isTouch) this.dispatchEvent(displayObject, 'touchend', interactionEvent); + + if (trackingData) + { + this.dispatchEvent(displayObject, 'pointertap', interactionEvent); + if (isTouch) + { + this.dispatchEvent(displayObject, 'tap', interactionEvent); + // touches are no longer over (if they ever were) when we get the touchend + // so we should ensure that we don't keep pretending that they are + trackingData.over = false; + } + } + } + else if (trackingData) + { + this.dispatchEvent(displayObject, 'pointerupoutside', interactionEvent); + if (isTouch) this.dispatchEvent(displayObject, 'touchendoutside', interactionEvent); + } + // Only remove the tracking data if there is no over/down state still associated with it + if (trackingData && trackingData.none) + { + delete displayObject.trackedPointers[id]; } } @@ -1202,39 +1161,78 @@ * 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 = null; + } - 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') + { + this.setCursorMode(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 (isMouse) { - 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 +1240,94 @@ * 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.setCursorMode(null); + } - 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'); + + let trackingData = displayObject.trackedPointers[id]; + + // if we just moused over the display object, then we need to track that state + if (hit && !trackingData) + { + trackingData = displayObject.trackedPointers[id] = new InteractionTrackingData(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); + } + } + + // only change the cursor if it has not already been changed (by something deeper in the + // display tree) + if (isMouse && this.cursor === null) + { + this.cursor = displayObject.cursor; } } - 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); + } + // if there is no mouse down information for the pointer, then it is safe to delete + if (trackingData.none) + { + delete displayObject.trackedPointers[id]; + } } } @@ -1287,253 +1335,154 @@ * 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 && pointerEvent.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; + + // mark the touch as normalized, just so that we know we did it + touch.isNormalized = true; + + normalizedEvents.push(touch); + } } - else if (this.normalizeMouseEvents) + // apparently PointerEvent subclasses MouseEvent, so yay + else if (event instanceof MouseEvent && (!this.supportsPointerEvents || !(event instanceof window.PointerEvent))) { if (typeof event.isPrimary === 'undefined') event.isPrimary = true; if (typeof event.width === 'undefined') event.width = 1; @@ -1541,10 +1490,21 @@ 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; + + // mark the mouse event as normalized, just so that we know we did it + event.isNormalized = true; + + normalizedEvents.push(event); } + else + { + normalizedEvents.push(event); + } + + return normalizedEvents; } /** @@ -1563,30 +1523,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 +1542,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..6a1f69e --- /dev/null +++ b/src/interaction/InteractionTrackingData.js @@ -0,0 +1,147 @@ +/** + * 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 inactive (not over or down)? + * + * @member {number} + * @memberof PIXI.interaction.InteractionTrackingData# + */ + get none() + { + return this._flags === this.constructor.FLAGS.NONE; + } + + /** + * 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..d252fba 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 * ); */ @@ -38,68 +38,56 @@ hitArea: null, /** - * If enabled, the mouse cursor will change when hovered over the displayObject if it is interactive + * If enabled, the mouse cursor use the pointer behavior when hovered over the displayObject if it is interactive + * Setting this changes the 'cursor' property to `'pointer'`. * - * @inner {boolean} + * @member {boolean} + * @memberof PIXI.interaction.interactiveTarget# */ - buttonMode: false, + get buttonMode() + { + return this.cursor === 'pointer'; + }, + set buttonMode(value) + { + if (value) + { + this.cursor = 'pointer'; + } + else if (this.cursor === 'pointer') + { + this.cursor = null; + } + }, /** - * If buttonMode is enabled, this defines what CSS cursor property is used when the mouse cursor - * is hovered over the displayObject + * This defines what cursor mode is used when the mouse cursor + * is hovered over the displayObject. * * @see https://developer.mozilla.org/en/docs/Web/CSS/cursor * * @inner {string} */ - defaultCursor: 'pointer', - - // some internal checks.. - /** - * Internal check to detect if the mouse cursor is hovered over the displayObject - * - * @inner {boolean} - * @private - */ - _over: false, + cursor: null, /** - * Internal check to detect if the left mouse button is pressed on the displayObject + * Internal set of all active pointers, by identifier * - * @inner {boolean} + * @member {Map} + * @memberof PIXI.interaction.interactiveTarget# * @private */ - _isLeftDown: false, + get trackedPointers() + { + if (this._trackedPointers === undefined) this._trackedPointers = {}; + + return this._trackedPointers; + }, /** - * Internal check to detect if the right mouse button is pressed on the displayObject + * Map of all tracked pointers, by identifier. Use trackedPointers to access. * - * @inner {boolean} - * @private + * @private {Map} */ - _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, + _trackedPointers: undefined, }; diff --git a/src/loaders/bitmapFontParser.js b/src/loaders/bitmapFontParser.js index cc2618c..f4cf996 100644 --- a/src/loaders/bitmapFontParser.js +++ b/src/loaders/bitmapFontParser.js @@ -1,63 +1,19 @@ import * as path from 'path'; -import { Rectangle, Texture, utils } from '../core'; +import { utils } from '../core'; import { Resource } from 'resource-loader'; import { BitmapText } from '../extras'; +/** + * Register a BitmapText font from loader resource. + * + * @function parseBitmapFontData + * @memberof PIXI.loaders + * @param {PIXI.loaders.Resource} resource - Loader resource. + * @param {PIXI.Texture} texture - Reference to texture. + */ export function parse(resource, texture) { - const data = {}; - const info = resource.data.getElementsByTagName('info')[0]; - const common = resource.data.getElementsByTagName('common')[0]; - - data.font = info.getAttribute('face'); - data.size = parseInt(info.getAttribute('size'), 10); - data.lineHeight = parseInt(common.getAttribute('lineHeight'), 10); - data.chars = {}; - - // parse letters - const letters = resource.data.getElementsByTagName('char'); - - for (let i = 0; i < letters.length; i++) - { - const charCode = parseInt(letters[i].getAttribute('id'), 10); - - const textureRect = new Rectangle( - parseInt(letters[i].getAttribute('x'), 10) + texture.frame.x, - parseInt(letters[i].getAttribute('y'), 10) + texture.frame.y, - parseInt(letters[i].getAttribute('width'), 10), - parseInt(letters[i].getAttribute('height'), 10) - ); - - data.chars[charCode] = { - xOffset: parseInt(letters[i].getAttribute('xoffset'), 10), - yOffset: parseInt(letters[i].getAttribute('yoffset'), 10), - xAdvance: parseInt(letters[i].getAttribute('xadvance'), 10), - kerning: {}, - texture: new Texture(texture.baseTexture, textureRect), - - }; - } - - // parse kernings - const kernings = resource.data.getElementsByTagName('kerning'); - - for (let i = 0; i < kernings.length; i++) - { - const first = parseInt(kernings[i].getAttribute('first'), 10); - const second = parseInt(kernings[i].getAttribute('second'), 10); - const amount = parseInt(kernings[i].getAttribute('amount'), 10); - - if (data.chars[second]) - { - data.chars[second].kerning[first] = amount; - } - } - - resource.bitmapFont = data; - - // I'm leaving this as a temporary fix so we can test the bitmap fonts in v3 - // but it's very likely to change - BitmapText.fonts[data.font] = data; + resource.bitmapFont = BitmapText.registerFont(resource.data, texture); } export default function () diff --git a/src/loaders/index.js b/src/loaders/index.js index 8942436..5189793 100644 --- a/src/loaders/index.js +++ b/src/loaders/index.js @@ -5,4 +5,11 @@ export { default as bitmapFontParser, parse as parseBitmapFontData } from './bitmapFontParser'; export { default as spritesheetParser } from './spritesheetParser'; export { default as textureParser } from './textureParser'; + +/** + * Reference to **resource-loader**'s Resource class. + * See https://github.com/englercj/resource-loader + * @class Resource + * @memberof PIXI.loaders + */ export { Resource } from 'resource-loader'; diff --git a/src/loaders/loader.js b/src/loaders/loader.js index 0a670cb..767a411 100644 --- a/src/loaders/loader.js +++ b/src/loaders/loader.js @@ -7,20 +7,47 @@ /** * - * The new loader, extends Resource Loader by Chad Engler : https://github.com/englercj/resource-loader + * The new loader, extends Resource Loader by Chad Engler: https://github.com/englercj/resource-loader * * ```js - * let loader = PIXI.loader; // pixi exposes a premade instance for you to use. + * const loader = PIXI.loader; // pixi exposes a premade instance for you to use. * //or - * let loader = new PIXI.loaders.Loader(); // you can also create your own if you want + * const loader = new PIXI.loaders.Loader(); // you can also create your own if you want * - * loader.add('bunny', 'data/bunny.png'); - * loader.add('spaceship', 'assets/spritesheet.json'); + * const sprites = {}; + * + * // Chainable `add` to enqueue a resource + * loader.add('bunny', 'data/bunny.png') + * .add('spaceship', 'assets/spritesheet.json'); * loader.add('scoreFont', 'assets/score.fnt'); * - * loader.once('complete',onAssetsLoaded); + * // Chainable `pre` to add a middleware that runs for each resource, *before* loading that resource. + * // This is useful to implement custom caching modules (using filesystem, indexeddb, memory, etc). + * loader.pre(cachingMiddleware); * - * loader.load(); + * // Chainable `use` to add a middleware that runs for each resource, *after* loading that resource. + * // This is useful to implement custom parsing modules (like spritesheet parsers, spine parser, etc). + * loader.use(parsingMiddleware); + * + * // The `load` method loads the queue of resources, and calls the passed in callback called once all + * // resources have loaded. + * loader.load((loader, resources) => { + * // resources is an object where the key is the name of the resource loaded and the value is the resource object. + * // They have a couple default properties: + * // - `url`: The URL that the resource was loaded from + * // - `error`: The error that happened when trying to load (if any) + * // - `data`: The raw data that was loaded + * // also may contain other properties based on the middleware that runs. + * sprites.bunny = new PIXI.TilingSprite(resources.bunny.texture); + * sprites.spaceship = new PIXI.TilingSprite(resources.spaceship.texture); + * sprites.scoreFont = new PIXI.TilingSprite(resources.scoreFont.texture); + * }); + * + * // throughout the process multiple signals can be dispatched. + * loader.onProgress.add(() => {}); // called once per loaded/errored file + * loader.onError.add(() => {}); // called once per errored file + * loader.onLoad.add(() => {}); // called once per loaded file + * loader.onComplete.add(() => {}); // called once when the queued resources all load. * ``` * * @see https://github.com/englercj/resource-loader diff --git a/src/loaders/spritesheetParser.js b/src/loaders/spritesheetParser.js index 7570332..e57dfb2 100644 --- a/src/loaders/spritesheetParser.js +++ b/src/loaders/spritesheetParser.js @@ -1,8 +1,6 @@ import { Resource } from 'resource-loader'; import path from 'path'; -import * as core from '../core'; - -const BATCH_SIZE = 1000; +import { Spritesheet } from '../core'; export default function () { @@ -43,134 +41,18 @@ // load the image for this sheet this.add(imageResourceName, resourcePath, loadOptions, function onImageLoad(res) { - resource.textures = {}; + const spritesheet = new Spritesheet( + res.texture.baseTexture, + resource.data, + resource.url + ); - const frames = resource.data.frames; - const frameKeys = Object.keys(frames); - const baseTexture = res.texture.baseTexture; - const scale = resource.data.meta.scale; - - // Use a defaultValue of `null` to check if a url-based resolution is set - let resolution = core.utils.getResolutionOfUrl(resource.url, null); - - // No resolution found via URL - if (resolution === null) + spritesheet.parse(() => { - // Use the scale value or default to 1 - resolution = scale !== undefined ? scale : 1; - } - - // For non-1 resolutions, update baseTexture - if (resolution !== 1) - { - baseTexture.resolution = resolution; - baseTexture.update(); - } - - let batchIndex = 0; - - function processFrames(initialFrameIndex, maxFrames) - { - let frameIndex = initialFrameIndex; - - while (frameIndex - initialFrameIndex < maxFrames && frameIndex < frameKeys.length) - { - const i = frameKeys[frameIndex]; - const rect = frames[i].frame; - - if (rect) - { - let frame = null; - let trim = null; - const orig = new core.Rectangle( - 0, - 0, - frames[i].sourceSize.w / resolution, - frames[i].sourceSize.h / resolution - ); - - if (frames[i].rotated) - { - frame = new core.Rectangle( - rect.x / resolution, - rect.y / resolution, - rect.h / resolution, - rect.w / resolution - ); - } - else - { - frame = new core.Rectangle( - rect.x / resolution, - rect.y / resolution, - rect.w / resolution, - rect.h / resolution - ); - } - - // Check to see if the sprite is trimmed - if (frames[i].trimmed) - { - trim = new core.Rectangle( - frames[i].spriteSourceSize.x / resolution, - frames[i].spriteSourceSize.y / resolution, - rect.w / resolution, - rect.h / resolution - ); - } - - resource.textures[i] = new core.Texture( - baseTexture, - frame, - orig, - trim, - frames[i].rotated ? 2 : 0 - ); - - // lets also add the frame to pixi's global cache for fromFrame and fromImage functions - core.utils.TextureCache[i] = resource.textures[i]; - } - - frameIndex++; - } - } - - function shouldProcessNextBatch() - { - return batchIndex * BATCH_SIZE < frameKeys.length; - } - - function processNextBatch(done) - { - processFrames(batchIndex * BATCH_SIZE, BATCH_SIZE); - batchIndex++; - setTimeout(done, 0); - } - - function iteration() - { - processNextBatch(() => - { - if (shouldProcessNextBatch()) - { - iteration(); - } - else - { - next(); - } - }); - } - - if (frameKeys.length <= BATCH_SIZE) - { - processFrames(0, BATCH_SIZE); + resource.spritesheet = spritesheet; + resource.textures = spritesheet.textures; next(); - } - else - { - iteration(); - } + }); }); }; } diff --git a/src/loaders/textureParser.js b/src/loaders/textureParser.js index 5398a7f..480edc1 100644 --- a/src/loaders/textureParser.js +++ b/src/loaders/textureParser.js @@ -1,5 +1,5 @@ -import * as core from '../core'; import { Resource } from 'resource-loader'; +import Texture from '../core/textures/Texture'; export default function () { @@ -8,23 +8,12 @@ // create a new texture if the data is an Image object if (resource.data && resource.type === Resource.TYPE.IMAGE) { - const baseTexture = new core.BaseTexture(resource.data, null, core.utils.getResolutionOfUrl(resource.url)); - - baseTexture.imageUrl = resource.url; - resource.texture = new core.Texture(baseTexture); - - // lets also add the frame to pixi's global cache for fromFrame and fromImage fucntions - core.utils.BaseTextureCache[resource.name] = baseTexture; - core.utils.TextureCache[resource.name] = resource.texture; - - // also add references by url if they are different. - if (resource.name !== resource.url) - { - core.utils.BaseTextureCache[resource.url] = baseTexture; - core.utils.TextureCache[resource.url] = resource.texture; - } + resource.texture = Texture.fromLoader( + resource.data, + resource.url, + resource.name + ); } - next(); }; } diff --git a/test/core/Container.js b/test/core/Container.js index 8a10b40..d11be33 100644 --- a/test/core/Container.js +++ b/test/core/Container.js @@ -1,5 +1,33 @@ 'use strict'; +function testAddChild(fn) +{ + fn(function (container, obj) + { + container.addChild(obj); + }); + fn(function (container, obj) + { + container.addChildAt(obj); + }); +} + +function testRemoveChild(fn) +{ + fn(function (container, obj) + { + container.removeChild(obj); + }); + fn(function (container, obj) + { + container.removeChildAt(container.children.indexOf(obj)); + }); + fn(function (container, obj) + { + container.removeChildren(container.children.indexOf(obj), container.children.indexOf(obj) + 1); + }); +} + describe('PIXI.Container', function () { describe('parent', function () @@ -70,6 +98,49 @@ expect(spy).to.have.been.called; expect(spy).to.have.been.calledWith(0); }); + + it('should flag child transform and container bounds for recalculation', testAddChild(function (mockAddChild) + { + const container = new PIXI.Container(); + const child = new PIXI.Container(); + + container.getBounds(); + child.getBounds(); + + const boundsID = container._boundsID; + const childParentID = child.transform._parentID; + + mockAddChild(container, child); + + expect(boundsID).to.not.be.equals(container._boundsID); + expect(childParentID).to.not.be.equals(child.transform._parentID); + })); + + it('should recalculate added child correctly', testAddChild(function (mockAddChild) + { + const parent = new PIXI.Container(); + const container = new PIXI.Container(); + const graphics = new PIXI.Graphics(); + + parent.addChild(container); + + graphics.drawRect(0, 0, 10, 10); + container.position.set(100, 200); + container.updateTransform(); + + graphics.getBounds(); + // Oops, that can happen sometimes! + graphics.transform._parentID = container.transform._worldID + 1; + + mockAddChild(container, graphics); + + const bounds = graphics.getBounds(); + + expect(bounds.x).to.be.equal(100); + expect(bounds.y).to.be.equal(200); + expect(bounds.width).to.be.equal(10); + expect(bounds.height).to.be.equal(10); + })); }); describe('removeChildAt', function () @@ -202,6 +273,44 @@ expect(spy).to.have.been.called; expect(spy).to.have.been.calledWith(0); }); + + it('should flag transform for recalculation', testRemoveChild(function (mockRemoveChild) + { + const container = new PIXI.Container(); + const child = new PIXI.Container(); + + container.addChild(child); + container.getBounds(); + + const childParentID = child.transform._parentID; + const boundsID = container._boundsID; + + mockRemoveChild(container, child); + + expect(childParentID).to.not.be.equals(child.transform._parentID); + expect(boundsID).to.not.be.equals(container._boundsID); + })); + + it('should recalculate removed child correctly', testRemoveChild(function (mockRemoveChild) + { + const parent = new PIXI.Container(); + const container = new PIXI.Container(); + const graphics = new PIXI.Graphics(); + + parent.addChild(container); + + graphics.drawRect(0, 0, 10, 10); + container.position.set(100, 200); + container.addChild(graphics); + graphics.getBounds(); + + mockRemoveChild(container, graphics); + + const bounds = graphics.getBounds(); + + expect(bounds.x).to.be.equal(0); + expect(bounds.y).to.be.equal(0); + })); }); describe('getChildIndex', function () diff --git a/test/core/Graphics.js b/test/core/Graphics.js index 98c7584..11e47a0 100644 --- a/test/core/Graphics.js +++ b/test/core/Graphics.js @@ -1,5 +1,8 @@ 'use strict'; +const MockPointer = require('../interaction/MockPointer'); +const withGL = require('../withGL'); + describe('PIXI.Graphics', function () { describe('constructor', function () @@ -218,4 +221,153 @@ expect(spy).to.have.been.calledOnce; }); }); + + describe('mask', function () + { + it('should trigger interaction callback when no mask present', function () + { + const stage = new PIXI.Container(); + const pointer = new MockPointer(stage); + const graphics = new PIXI.Graphics(); + const mask = new PIXI.Graphics(); + const spy = sinon.spy(); + + graphics.interactive = true; + graphics.beginFill(0xFF0000); + graphics.drawRect(0, 0, 50, 50); + graphics.on('click', spy); + stage.addChild(graphics); + mask.beginFill(); + mask.drawRect(0, 0, 50, 50); + graphics.mask = mask; + + pointer.click(10, 10); + + expect(spy).to.have.been.calledOnce; + }); + it('should trigger interaction callback when mask uses beginFill', function () + { + const stage = new PIXI.Container(); + const pointer = new MockPointer(stage); + const graphics = new PIXI.Graphics(); + const mask = new PIXI.Graphics(); + const spy = sinon.spy(); + + graphics.interactive = true; + graphics.beginFill(0xFF0000); + graphics.drawRect(0, 0, 50, 50); + graphics.on('click', spy); + stage.addChild(graphics); + mask.beginFill(); + mask.drawRect(0, 0, 50, 50); + graphics.mask = mask; + + pointer.click(10, 10); + + expect(spy).to.have.been.calledOnce; + }); + + it('should not trigger interaction callback when mask doesn\'t use beginFill', function () + { + const stage = new PIXI.Container(); + const pointer = new MockPointer(stage); + const graphics = new PIXI.Graphics(); + const mask = new PIXI.Graphics(); + const spy = sinon.spy(); + + graphics.interactive = true; + graphics.beginFill(0xFF0000); + graphics.drawRect(0, 0, 50, 50); + graphics.on('click', spy); + stage.addChild(graphics); + mask.drawRect(0, 0, 50, 50); + graphics.mask = mask; + + pointer.click(10, 10); + + expect(spy).to.have.not.been.called; + }); + + it('should trigger interaction callback when mask doesn\'t use beginFill but hitArea is defined', function () + { + const stage = new PIXI.Container(); + const pointer = new MockPointer(stage); + const graphics = new PIXI.Graphics(); + const mask = new PIXI.Graphics(); + const spy = sinon.spy(); + + graphics.interactive = true; + graphics.beginFill(0xFF0000); + graphics.hitArea = new PIXI.Rectangle(0, 0, 50, 50); + graphics.drawRect(0, 0, 50, 50); + graphics.on('click', spy); + stage.addChild(graphics); + mask.drawRect(0, 0, 50, 50); + graphics.mask = mask; + + pointer.click(10, 10); + + expect(spy).to.have.been.calledOnce; + }); + + it('should trigger interaction callback when mask is a sprite', function () + { + const stage = new PIXI.Container(); + const pointer = new MockPointer(stage); + const graphics = new PIXI.Graphics(); + const mask = new PIXI.Graphics(); + const spy = sinon.spy(); + + graphics.interactive = true; + graphics.beginFill(0xFF0000); + graphics.drawRect(0, 0, 50, 50); + graphics.on('click', spy); + stage.addChild(graphics); + mask.drawRect(0, 0, 50, 50); + graphics.mask = new PIXI.Sprite(mask.generateCanvasTexture()); + + pointer.click(10, 10); + + expect(spy).to.have.been.calledOnce; + }); + + it('should calculate tint, alpha and blendMode of fastRect correctly', withGL(function () + { + const renderer = new PIXI.WebGLRenderer(200, 200, {}); + + try + { + const graphics = new PIXI.Graphics(); + + graphics.beginFill(0x102030, 0.6); + graphics.drawRect(2, 3, 100, 100); + graphics.endFill(); + graphics.tint = 0x101010; + graphics.blendMode = 2; + graphics.alpha = 0.3; + + renderer.render(graphics); + + expect(graphics.isFastRect()).to.be.true; + + const sprite = graphics._spriteRect; + + expect(sprite).to.not.be.equals(null); + expect(sprite.worldAlpha).to.equals(0.18); + expect(sprite.blendMode).to.equals(2); + expect(sprite.tint).to.equals(0x010203); + + const bounds = sprite.getBounds(); + + expect(bounds.x).to.equals(2); + expect(bounds.y).to.equals(3); + expect(bounds.width).to.equals(100); + expect(bounds.height).to.equals(100); + } + finally + { + renderer.destroy(); + } + })); + }); }); diff --git a/test/core/Spritesheet.js b/test/core/Spritesheet.js new file mode 100644 index 0000000..283adf5 --- /dev/null +++ b/test/core/Spritesheet.js @@ -0,0 +1,89 @@ +'use strict'; + +const path = require('path'); + +describe('PIXI.Spritesheet', function () +{ + before(function () + { + this.resources = path.join(__dirname, 'resources'); + this.validate = function (spritesheet, done) + { + spritesheet.parse(function (textures) + { + const id = 'goldmine_10_5.png'; + + expect(Object.keys(textures).length).to.equal(1); + expect(Object.keys(spritesheet.textures).length).to.equal(1); + expect(textures[id]).to.be.an.instanceof(PIXI.Texture); + expect(textures[id].width).to.equal(spritesheet.data.frames[id].frame.w / spritesheet.resolution); + expect(textures[id].height).to.equal(spritesheet.data.frames[id].frame.h / spritesheet.resolution); + spritesheet.destroy(true); + expect(spritesheet.textures).to.be.null; + expect(spritesheet.baseTexture).to.be.null; + done(); + }); + }; + }); + + it('should exist on PIXI', function () + { + expect(PIXI.Spritesheet).to.be.a.function; + expect(PIXI.Spritesheet.BATCH_SIZE).to.be.a.number; + }); + + it('should create an instance', function () + { + const baseTexture = new PIXI.BaseTexture(); + const data = { + frames: {}, + meta: {}, + }; + const spritesheet = new PIXI.Spritesheet(baseTexture, data); + + expect(spritesheet.data).to.equal(data); + expect(spritesheet.baseTexture).to.equal(baseTexture); + expect(spritesheet.resolution).to.equal(1); + + spritesheet.destroy(true); + }); + + it('should create instance with scale resolution', function (done) + { + const data = require(path.resolve(this.resources, 'building1.json')); // eslint-disable-line global-require + const image = new Image(); + + image.src = path.join(this.resources, data.meta.image); + image.onload = () => + { + const baseTexture = new PIXI.BaseTexture(image, null, 1); + const spritesheet = new PIXI.Spritesheet(baseTexture, data); + + expect(data).to.be.an.object; + expect(data.meta.image).to.equal('building1.png'); + expect(spritesheet.resolution).to.equal(0.5); + + this.validate(spritesheet, done); + }; + }); + + it('should create instance with filename resolution', function (done) + { + const uri = path.resolve(this.resources, 'building1@2x.json'); + const data = require(uri); // eslint-disable-line global-require + const image = new Image(); + + image.src = path.join(this.resources, data.meta.image); + image.onload = () => + { + const baseTexture = new PIXI.BaseTexture(image, null, 1); + const spritesheet = new PIXI.Spritesheet(baseTexture, data, uri); + + expect(data).to.be.an.object; + expect(data.meta.image).to.equal('building1@2x.png'); + expect(spritesheet.resolution).to.equal(2); + + this.validate(spritesheet, done); + }; + }); +}); diff --git a/test/core/Texture.js b/test/core/Texture.js new file mode 100644 index 0000000..8c4ef98 --- /dev/null +++ b/test/core/Texture.js @@ -0,0 +1,19 @@ +'use strict'; + +describe('PIXI.Texture', function () +{ + it('should register Texture from Loader', function () + { + const URL = 'foo.png'; + const NAME = 'bar'; + const image = new Image(); + + const texture = PIXI.Texture.fromLoader(image, URL, NAME); + + expect(texture.baseTexture.imageUrl).to.equal('foo.png'); + expect(PIXI.utils.TextureCache[NAME]).to.equal(texture); + expect(PIXI.utils.BaseTextureCache[NAME]).to.equal(texture.baseTexture); + expect(PIXI.utils.TextureCache[URL]).to.equal(texture); + expect(PIXI.utils.BaseTextureCache[URL]).to.equal(texture.baseTexture); + }); +}); diff --git a/test/core/WebGLRenderer.js b/test/core/WebGLRenderer.js index 9bc3bc2..3d6cf98 100644 --- a/test/core/WebGLRenderer.js +++ b/test/core/WebGLRenderer.js @@ -1,21 +1,21 @@ 'use strict'; -function isWebGLSupported(fn) -{ - return PIXI.utils.isWebGLSupported() ? fn : function () {}; // eslint-disable-line no-empty-function -} +const withGL = require('../withGL'); -describe('PIXI.WebGLRenderer', isWebGLSupported(function () +describe('PIXI.WebGLRenderer', function () { - it('setting option legacy should disable VAOs and set minimum SPRITE_MAX_TEXTURES to 1', function () + it('setting option legacy should disable VAOs and SPRITE_MAX_TEXTURES', withGL(function () { const renderer = new PIXI.WebGLRenderer(1, 1, { legacy: true }); - - expect(PIXI.glCore.VertexArrayObject.FORCE_NATIVE).to.equal(true); - expect(renderer.plugins.sprite.MAX_TEXTURES).to.equal(1); - - renderer.destroy(); - }); -})); - + try + { + expect(PIXI.glCore.VertexArrayObject.FORCE_NATIVE).to.equal(true); + expect(renderer.plugins.sprite.MAX_TEXTURES).to.equal(1); + } + finally + { + renderer.destroy(); + } + })); +}); diff --git a/package.json b/package.json index 6a366ba..71ade15 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pixi.js", - "version": "4.3.4", + "version": "4.3.5", "description": "Pixi.js is a fast lightweight 2D library that works across all devices.", "author": "Mat Groves", "contributors": [ @@ -61,7 +61,7 @@ "ismobilejs": "^0.4.0", "object-assign": "^4.0.1", "pixi-gl-core": "^1.0.3", - "resource-loader": "^2.0.4" + "resource-loader": "^2.0.6" }, "devDependencies": { "babel-cli": "^6.18.0", @@ -70,9 +70,9 @@ "babel-preset-es2015": "^6.14.0", "babelify": "^7.3.0", "del": "^2.2.0", - "electron-prebuilt": "^1.3.2", + "electron": "^1.4.15", "eslint": "^3.5.0", - "floss": "^1.2.0", + "floss": "^2.0.1", "gh-pages": "^0.11.0", "jaguarjs-jsdoc": "^1.0.1", "js-md5": "^0.4.1", diff --git a/src/accessibility/AccessibilityManager.js b/src/accessibility/AccessibilityManager.js index df2716a..f16bcb1 100644 --- a/src/accessibility/AccessibilityManager.js +++ b/src/accessibility/AccessibilityManager.js @@ -3,7 +3,7 @@ import accessibleTarget from './accessibleTarget'; // add some extra variables to the container.. -Object.assign( +core.utils.mixins.delayMixin( core.DisplayObject.prototype, accessibleTarget ); diff --git a/src/core/Application.js b/src/core/Application.js index ea0f058..5340578 100644 --- a/src/core/Application.js +++ b/src/core/Application.js @@ -1,6 +1,6 @@ import { autoDetectRenderer } from './autoDetectRenderer'; import Container from './display/Container'; -import Ticker from './ticker/Ticker'; +import { shared, Ticker } from './ticker'; /** * Convenience class to create a new PIXI application. @@ -35,9 +35,9 @@ * @param {boolean} [noWebGL=false] - prevents selection of WebGL renderer, even if such is present * @param {boolean} [options.legacy=false] - If true Pixi will aim to ensure compatibility * with older / less advanced devices. If you experience unexplained flickering try setting this to true. - + * @param {boolean} [useSharedTicker=false] - `true` to use PIXI.ticker.shared, `false` to create new ticker. */ - constructor(width, height, options, noWebGL) + constructor(width, height, options, noWebGL, useSharedTicker = false) { /** * WebGL renderer if available, otherwise CanvasRenderer @@ -46,23 +46,46 @@ this.renderer = autoDetectRenderer(width, height, options, noWebGL); /** - * The root display container that's renderered. + * The root display container that's rendered. * @member {PIXI.Container} */ this.stage = new Container(); /** + * Internal reference to the ticker + * @member {PIXI.ticker.Ticker} + * @private + */ + this._ticker = null; + + /** * Ticker for doing render updates. * @member {PIXI.ticker.Ticker} + * @default PIXI.ticker.shared */ - this.ticker = new Ticker(); - - this.ticker.add(this.render, this); + this.ticker = useSharedTicker ? shared : new Ticker(); // Start the rendering this.start(); } + set ticker(ticker) // eslint-disable-line require-jsdoc + { + if (this._ticker) + { + this._ticker.remove(this.render, this); + } + this._ticker = ticker; + if (ticker) + { + ticker.add(this.render, this); + } + } + get ticker() // eslint-disable-line require-jsdoc + { + return this._ticker; + } + /** * Render the current stage. */ @@ -76,7 +99,7 @@ */ stop() { - this.ticker.stop(); + this._ticker.stop(); } /** @@ -84,7 +107,7 @@ */ start() { - this.ticker.start(); + this._ticker.start(); } /** @@ -98,13 +121,22 @@ } /** + * Reference to the renderer's screen rectangle. Its safe to use as filterArea or hitArea for whole screen + * @member {PIXI.Rectangle} + * @readonly + */ + get screen() + { + return this.renderer.screen; + } + + /** * Destroy and don't use after this. * @param {Boolean} [removeView=false] Automatically remove canvas from DOM. */ destroy(removeView) { this.stop(); - this.ticker.remove(this.render, this); this.ticker = null; this.stage.destroy(); diff --git a/src/core/Shader.js b/src/core/Shader.js new file mode 100644 index 0000000..78b17e6 --- /dev/null +++ b/src/core/Shader.js @@ -0,0 +1,46 @@ +import { GLShader } from 'pixi-gl-core'; +import settings from './settings'; + +function checkPrecision(src, def) +{ + if (src instanceof Array) + { + if (src[0].substring(0, 9) !== 'precision') + { + const copy = src.slice(0); + + copy.unshift(`precision ${def} float;`); + + return copy; + } + } + else if (src.substring(0, 9) !== 'precision') + { + return `precision ${def} float;\n${src}`; + } + + return src; +} + +/** + * Wrapper class, webGL Shader for Pixi. + * Adds precision string if vertexSrc or fragmentSrc have no mention of it. + * + * @class + * @extends GLShader + * @memberof PIXI + */ +export default class Shader extends GLShader +{ + /** + * + * @param {WebGLRenderingContext} gl - The current WebGL rendering context + * @param {string|string[]} vertexSrc - The vertex shader source as an array of strings. + * @param {string|string[]} fragmentSrc - The fragment shader source as an array of strings. + */ + constructor(gl, vertexSrc, fragmentSrc) + { + super(gl, checkPrecision(vertexSrc, settings.PRECISION_VERTEX), + checkPrecision(fragmentSrc, settings.PRECISION_FRAGMENT)); + } +} diff --git a/src/core/const.js b/src/core/const.js index ef6f662..3de0efe 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -188,7 +188,7 @@ * The gc modes that are supported by pixi. * * The {@link PIXI.settings.GC_MODE} Garbage Collection mode for pixi textures is AUTO - * If set to GC_MODE, the renderer will occasianally check textures usage. If they are not + * If set to GC_MODE, the renderer will occasionally check textures usage. If they are not * used for a specified period of time they will be removed from the GPU. They will of course * be uploaded again when they are required. This is a silent behind the scenes process that * should ensure that the GPU does not get filled up. diff --git a/src/core/display/Container.js b/src/core/display/Container.js index ed8cd5f..17fde8d 100644 --- a/src/core/display/Container.js +++ b/src/core/display/Container.js @@ -73,11 +73,12 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.push(child); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -108,11 +109,12 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.splice(index, 0, child); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -225,10 +227,11 @@ if (index === -1) return null; child.parent = null; + // ensure child transform will be recalculated + child.transform._parentID = -1; removeItems(this.children, index, 1); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -249,11 +252,12 @@ { const child = this.getChildAt(index); + // ensure child transform will be recalculated.. child.parent = null; + child.transform._parentID = -1; removeItems(this.children, index, 1); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -284,13 +288,12 @@ for (let i = 0; i < removed.length; ++i) { removed[i].parent = null; + if (removed[i].transform) + { + removed[i].transform._parentID = -1; + } } - // ensure a transform will be recalculated.. - if (this.transform) - { - this.transform._parentID = -1; - } this._boundsID++; this.onChildrenChange(beginIndex); diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index f37f2f5..be4c4fb 100644 --- a/src/core/graphics/Graphics.js +++ b/src/core/graphics/Graphics.js @@ -28,8 +28,9 @@ { /** * + * @param {boolean} [nativeLines=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP */ - constructor() + constructor(nativeLines = false) { super(); @@ -50,6 +51,13 @@ this.lineWidth = 0; /** + * If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * + * @member {boolean} + */ + this.nativeLines = nativeLines; + + /** * The color of any lines drawn. * * @member {string} @@ -757,28 +765,14 @@ if (!this._spriteRect) { - if (!Graphics._SPRITE_TEXTURE) - { - Graphics._SPRITE_TEXTURE = RenderTexture.create(10, 10); - - const canvas = document.createElement('canvas'); - - canvas.width = 10; - canvas.height = 10; - - const context = canvas.getContext('2d'); - - context.fillStyle = 'white'; - context.fillRect(0, 0, 10, 10); - - Graphics._SPRITE_TEXTURE = Texture.fromCanvas(canvas); - } - - this._spriteRect = new Sprite(Graphics._SPRITE_TEXTURE); + this._spriteRect = new Sprite(new Texture(Texture.WHITE)); } + + const sprite = this._spriteRect; + if (this.tint === 0xffffff) { - this._spriteRect.tint = this.graphicsData[0].fillColor; + sprite.tint = this.graphicsData[0].fillColor; } else { @@ -792,20 +786,21 @@ t1[1] *= t2[1]; t1[2] *= t2[2]; - this._spriteRect.tint = rgb2hex(t1); + sprite.tint = rgb2hex(t1); } - this._spriteRect.alpha = this.graphicsData[0].fillAlpha; - this._spriteRect.worldAlpha = this.worldAlpha * this._spriteRect.alpha; + sprite.alpha = this.graphicsData[0].fillAlpha; + sprite.worldAlpha = this.worldAlpha * sprite.alpha; + sprite.blendMode = this.blendMode; - Graphics._SPRITE_TEXTURE._frame.width = rect.width; - Graphics._SPRITE_TEXTURE._frame.height = rect.height; + sprite.texture._frame.width = rect.width; + sprite.texture._frame.height = rect.height; - this._spriteRect.transform.worldTransform = this.transform.worldTransform; + sprite.transform.worldTransform = this.transform.worldTransform; - this._spriteRect.anchor.set(-rect.x / rect.width, -rect.y / rect.height); - this._spriteRect._onAnchorUpdate(); + sprite.anchor.set(-rect.x / rect.width, -rect.y / rect.height); + sprite._onAnchorUpdate(); - this._spriteRect._renderWebGL(renderer); + sprite._renderWebGL(renderer); } /** @@ -1031,6 +1026,7 @@ this.fillColor, this.fillAlpha, this.filling, + this.nativeLines, shape ); diff --git a/src/core/graphics/GraphicsData.js b/src/core/graphics/GraphicsData.js index f5a87ae..b6f03c0 100644 --- a/src/core/graphics/GraphicsData.js +++ b/src/core/graphics/GraphicsData.js @@ -14,14 +14,19 @@ * @param {number} fillColor - the color of the fill * @param {number} fillAlpha - the alpha of the fill * @param {boolean} fill - whether or not the shape is filled with a colour + * @param {boolean} nativeLines - the method for drawing lines * @param {PIXI.Circle|PIXI.Rectangle|PIXI.Ellipse|PIXI.Polygon} shape - The shape object to draw. */ - constructor(lineWidth, lineColor, lineAlpha, fillColor, fillAlpha, fill, shape) + constructor(lineWidth, lineColor, lineAlpha, fillColor, fillAlpha, fill, nativeLines, shape) { /** * @member {number} the width of the line to draw */ this.lineWidth = lineWidth; + /** + * @member {boolean} if true the liens will be draw using LINES instead of TRIANGLE_STRIP + */ + this.nativeLines = nativeLines; /** * @member {number} the color of the line to draw @@ -85,6 +90,7 @@ this.fillColor, this.fillAlpha, this.fill, + this.nativeLines, this.shape ); } diff --git a/src/core/graphics/webgl/GraphicsRenderer.js b/src/core/graphics/webgl/GraphicsRenderer.js index 14083fe..67f5e92 100644 --- a/src/core/graphics/webgl/GraphicsRenderer.js +++ b/src/core/graphics/webgl/GraphicsRenderer.js @@ -102,7 +102,15 @@ shaderTemp.uniforms.alpha = graphics.worldAlpha; renderer.bindVao(webGLData.vao); - webGLData.vao.draw(gl.TRIANGLE_STRIP, webGLData.indices.length); + + if (graphics.nativeLines) + { + gl.drawArrays(gl.LINES, 0, webGLData.points.length / 6); + } + else + { + webGLData.vao.draw(gl.TRIANGLE_STRIP, webGLData.indices.length); + } } } diff --git a/src/core/graphics/webgl/utils/buildLine.js b/src/core/graphics/webgl/utils/buildLine.js index ecf43f7..4d6862e 100644 --- a/src/core/graphics/webgl/utils/buildLine.js +++ b/src/core/graphics/webgl/utils/buildLine.js @@ -11,7 +11,29 @@ * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties * @param {object} webGLData - an object containing all the webGL-specific information to create this shape */ -export default function buildLine(graphicsData, webGLData) +export default function (graphicsData, webGLData) +{ + if (graphicsData.nativeLines) + { + buildNativeLine(graphicsData, webGLData); + } + else + { + buildLine(graphicsData, webGLData); + } +} + +/** + * Builds a line to draw using the poligon method. + * + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @private + * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties + * @param {object} webGLData - an object containing all the webGL-specific information to create this shape + */ +function buildLine(graphicsData, webGLData) { // TODO OPTIMISE! let points = graphicsData.points; @@ -224,3 +246,46 @@ indices.push(indexStart - 1); } + +/** + * Builds a line to draw using the gl.drawArrays(gl.LINES) method + * + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @private + * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties + * @param {object} webGLData - an object containing all the webGL-specific information to create this shape + */ +function buildNativeLine(graphicsData, webGLData) +{ + let i = 0; + const points = graphicsData.points; + + if (points.length === 0) return; + + const verts = webGLData.points; + const length = points.length / 2; + + // sort color + const color = hex2rgb(graphicsData.lineColor); + const alpha = graphicsData.lineAlpha; + const r = color[0] * alpha; + const g = color[1] * alpha; + const b = color[2] * alpha; + + for (i = 1; i < length; i++) + { + const p1x = points[(i - 1) * 2]; + const p1y = points[((i - 1) * 2) + 1]; + + const p2x = points[i * 2]; + const p2y = points[(i * 2) + 1]; + + verts.push(p1x, p1y); + verts.push(r, g, b, alpha); + + verts.push(p2x, p2y); + verts.push(r, g, b, alpha); + } +} diff --git a/src/core/graphics/webgl/utils/buildRoundedRectangle.js b/src/core/graphics/webgl/utils/buildRoundedRectangle.js index 9abc807..a6c8461 100644 --- a/src/core/graphics/webgl/utils/buildRoundedRectangle.js +++ b/src/core/graphics/webgl/utils/buildRoundedRectangle.js @@ -77,6 +77,26 @@ } /** + * Calculate a single point for a quadratic bezier curve. + * Utility function used by quadraticBezierCurve. + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @private + * @param {number} n1 - first number + * @param {number} n2 - second number + * @param {number} perc - percentage + * @return {number} the result + * + */ +function getPt(n1, n2, perc) +{ + const diff = n2 - n1; + + return n1 + (diff * perc); +} + +/** * Calculate the points for a quadratic bezier curve. (helper function..) * Based on: https://stackoverflow.com/questions/785097/how-do-i-implement-a-bezier-curve-in-c * @@ -105,13 +125,6 @@ let x = 0; let y = 0; - function getPt(n1, n2, perc) - { - const diff = n2 - n1; - - return n1 + (diff * perc); - } - for (let i = 0, j = 0; i <= n; ++i) { j = i / n; diff --git a/src/core/index.js b/src/core/index.js index f7a817c..4f414e8 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -30,6 +30,7 @@ export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; export { default as CanvasGraphicsRenderer } from './graphics/canvas/CanvasGraphicsRenderer'; +export { default as Spritesheet } from './textures/Spritesheet'; export { default as Texture } from './textures/Texture'; export { default as BaseTexture } from './textures/BaseTexture'; export { default as RenderTexture } from './textures/RenderTexture'; diff --git a/src/core/math/Matrix.js b/src/core/math/Matrix.js index 0b77477..3679860 100644 --- a/src/core/math/Matrix.js +++ b/src/core/math/Matrix.js @@ -13,45 +13,50 @@ export default class Matrix { /** - * + * @param {number} [a=1] - x scale + * @param {number} [b=0] - y skew + * @param {number} [c=0] - x skew + * @param {number} [d=1] - y scale + * @param {number} [tx=0] - x translation + * @param {number} [ty=0] - y translation */ - constructor() + constructor(a = 1, b = 0, c = 0, d = 1, tx = 0, ty = 0) { /** * @member {number} * @default 1 */ - this.a = 1; + this.a = a; /** * @member {number} * @default 0 */ - this.b = 0; + this.b = b; /** * @member {number} * @default 0 */ - this.c = 0; + this.c = c; /** * @member {number} * @default 1 */ - this.d = 1; + this.d = d; /** * @member {number} * @default 0 */ - this.tx = 0; + this.tx = tx; /** * @member {number} * @default 0 */ - this.ty = 0; + this.ty = ty; this.array = null; } diff --git a/src/core/renderers/SystemRenderer.js b/src/core/renderers/SystemRenderer.js index fa116a6..a24c775 100644 --- a/src/core/renderers/SystemRenderer.js +++ b/src/core/renderers/SystemRenderer.js @@ -1,5 +1,5 @@ import { sayHello, hex2string, hex2rgb } from '../utils'; -import { Matrix } from '../math'; +import { Matrix, Rectangle } from '../math'; import { RENDERER_TYPE } from '../const'; import settings from '../settings'; import Container from '../display/Container'; @@ -21,8 +21,8 @@ { /** * @param {string} system - The name of the system this renderer is for. - * @param {number} [width=800] - the width of the canvas view - * @param {number} [height=600] - the height of the canvas view + * @param {number} [screenWidth=800] - the width of the screen + * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false @@ -37,7 +37,7 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(system, width, height, options) + constructor(system, screenWidth, screenHeight, options) { super(); @@ -69,20 +69,13 @@ this.type = RENDERER_TYPE.UNKNOWN; /** - * The width of the canvas view + * Measurements of the screen. (0, 0, screenWidth, screenHeight) * - * @member {number} - * @default 800 - */ - this.width = width || 800; - - /** - * The height of the canvas view + * Its safe to use as filterArea or hitArea for whole stage * - * @member {number} - * @default 600 + * @member {PIXI.Rectangle} */ - this.height = height || 600; + this.screen = new Rectangle(0, 0, screenWidth || 800, screenHeight || 600); /** * The canvas element that everything is drawn to @@ -107,7 +100,7 @@ this.transparent = options.transparent; /** - * Whether the render view should be resized automatically + * Whether css dimensions of canvas view should be resized to screen dimensions automatically * * @member {boolean} */ @@ -192,23 +185,48 @@ } /** - * Resizes the canvas view to the specified width and height + * Same as view.width, actual number of pixels in the canvas by horizontal * - * @param {number} width - the new width of the canvas view - * @param {number} height - the new height of the canvas view + * @member {number} + * @readonly + * @default 800 */ - resize(width, height) + get width() { - this.width = width * this.resolution; - this.height = height * this.resolution; + return this.view.width; + } - this.view.width = this.width; - this.view.height = this.height; + /** + * Same as view.height, actual number of pixels in the canvas by vertical + * + * @member {number} + * @readonly + * @default 600 + */ + get height() + { + return this.view.height; + } + + /** + * Resizes the screen and canvas to the specified width and height + * Canvas dimensions are multiplied by resolution + * + * @param {number} screenWidth - the new width of the screen + * @param {number} screenHeight - the new height of the screen + */ + resize(screenWidth, screenHeight) + { + this.screen.width = screenWidth; + this.screen.height = screenHeight; + + this.view.width = screenWidth * this.resolution; + this.view.height = screenHeight * this.resolution; if (this.autoResize) { - this.view.style.width = `${this.width / this.resolution}px`; - this.view.style.height = `${this.height / this.resolution}px`; + this.view.style.width = `${screenWidth}px`; + this.view.style.height = `${screenHeight}px`; } } @@ -249,11 +267,10 @@ this.type = RENDERER_TYPE.UNKNOWN; - this.width = 0; - this.height = 0; - this.view = null; + this.screen = null; + this.resolution = 0; this.transparent = false; diff --git a/src/core/renderers/canvas/CanvasRenderer.js b/src/core/renderers/canvas/CanvasRenderer.js index 1bd2d5e..e84e49c 100644 --- a/src/core/renderers/canvas/CanvasRenderer.js +++ b/src/core/renderers/canvas/CanvasRenderer.js @@ -18,8 +18,8 @@ export default class CanvasRenderer extends SystemRenderer { /** - * @param {number} [width=800] - the width of the canvas view - * @param {number} [height=600] - the height of the canvas view + * @param {number} [screenWidth=800] - the width of the screen + * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false @@ -34,9 +34,9 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(width, height, options = {}) + constructor(screenWidth, screenHeight, options = {}) { - super('Canvas', width, height, options); + super('Canvas', screenWidth, screenHeight, options); this.type = RENDERER_TYPE.CANVAS; @@ -96,7 +96,7 @@ this.context = null; this.renderingToScreen = false; - this.resize(width, height); + this.resize(screenWidth, screenHeight); } /** @@ -283,12 +283,12 @@ * * @extends PIXI.SystemRenderer#resize * - * @param {number} width - The new width of the canvas view - * @param {number} height - The new height of the canvas view + * @param {number} screenWidth - the new width of the screen + * @param {number} screenHeight - the new height of the screen */ - resize(width, height) + resize(screenWidth, screenHeight) { - super.resize(width, height); + super.resize(screenWidth, screenHeight); // reset the scale mode.. oddly this seems to be reset when the canvas is resized. // surely a browser bug?? Let pixi fix that for you.. diff --git a/src/core/renderers/webgl/TextureManager.js b/src/core/renderers/webgl/TextureManager.js index b685c1a..8227289 100644 --- a/src/core/renderers/webgl/TextureManager.js +++ b/src/core/renderers/webgl/TextureManager.js @@ -61,7 +61,7 @@ * Updates and/or Creates a WebGL texture for the renderer's context. * * @param {PIXI.BaseTexture|PIXI.Texture} texture - the texture to update - * @param {Number} location - the location the texture will be bound to. + * @param {number} location - the location the texture will be bound to. * @return {GLTexture} The gl texture. */ updateTexture(texture, location) diff --git a/src/core/renderers/webgl/WebGLRenderer.js b/src/core/renderers/webgl/WebGLRenderer.js index e4cd8a0..830b382 100644 --- a/src/core/renderers/webgl/WebGLRenderer.js +++ b/src/core/renderers/webgl/WebGLRenderer.js @@ -31,8 +31,8 @@ { /** * - * @param {number} [width=0] - the width of the canvas view - * @param {number} [height=0] - the height of the canvas view + * @param {number} [screenWidth=800] - the width of the screen + * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false @@ -53,9 +53,9 @@ * @param {boolean} [options.legacy=false] - If true Pixi will aim to ensure compatibility * with older / less advanced devices. If you experiance unexplained flickering try setting this to true. */ - constructor(width, height, options = {}) + constructor(screenWidth, screenHeight, options = {}) { - super('WebGL', width, height, options); + super('WebGL', screenWidth, screenHeight, options); this.legacy = !!options.legacy; @@ -245,7 +245,7 @@ glCore._testingContext = gl; // setup the width/height properties and gl viewport - this.resize(this.width, this.height); + this.resize(this.screen.width, this.screen.height); } /** @@ -343,16 +343,16 @@ /** * Resizes the webGL view to the specified width and height. * - * @param {number} width - the new width of the webGL view - * @param {number} height - the new height of the webGL view + * @param {number} screenWidth - the new width of the screen + * @param {number} screenHeight - the new height of the screen */ - resize(width, height) + resize(screenWidth, screenHeight) { // if(width * this.resolution === this.width && height * this.resolution === this.height)return; - SystemRenderer.prototype.resize.call(this, width, height); + SystemRenderer.prototype.resize.call(this, screenWidth, screenHeight); - this.rootRenderTarget.resize(width, height); + this.rootRenderTarget.resize(screenWidth, screenHeight); if (this._activeRenderTarget === this.rootRenderTarget) { @@ -396,6 +396,26 @@ } /** + * Erases the render texture and fills the drawing area with a colour + * + * @param {PIXI.RenderTexture} renderTexture - The render texture to clear + * @param {number} [clearColor] - The colour + * @return {PIXI.WebGLRenderer} Returns itself. + */ + clearRenderTexture(renderTexture, clearColor) + { + const baseTexture = renderTexture.baseTexture; + const renderTarget = baseTexture._glRenderTargets[this.CONTEXT_UID]; + + if (renderTarget) + { + renderTarget.clear(clearColor); + } + + return this; + } + + /** * Binds a render texture for rendering * * @param {PIXI.RenderTexture} renderTexture - The render texture to render @@ -474,9 +494,10 @@ * Changes the current GLShader to the one given in parameter * * @param {PIXI.glCore.Shader} shader - the new glShader + * @param {boolean} [autoProject=true] - Whether automatically set the projection matrix * @return {PIXI.WebGLRenderer} Returns itself. */ - _bindGLShader(shader) + _bindGLShader(shader, autoProject) { // TODO cache if (this._activeShader !== shader) @@ -484,8 +505,14 @@ this._activeShader = shader; shader.bind(); - // automatically set the projection matrix - shader.uniforms.projectionMatrix = this._activeRenderTarget.projectionMatrix.toArray(true); + // `autoProject` normally would be a default parameter set to true + // but because of how Babel transpiles default parameters + // it hinders the performance of this method. + if (autoProject !== false) + { + // automatically set the projection matrix + shader.uniforms.projectionMatrix = this._activeRenderTarget.projectionMatrix.toArray(true); + } } return this; diff --git a/src/core/renderers/webgl/WebGLState.js b/src/core/renderers/webgl/WebGLState.js index ba6ecc5..6c846d5 100755 --- a/src/core/renderers/webgl/WebGLState.js +++ b/src/core/renderers/webgl/WebGLState.js @@ -82,18 +82,20 @@ push() { // next state.. - let state = this.stack[++this.stackIndex]; + let state = this.stack[this.stackIndex]; if (!state) { state = this.stack[this.stackIndex] = new Uint8Array(16); } + ++this.stackIndex; + // copy state.. // set active state so we can force overrides of gl state for (let i = 0; i < this.activeState.length; i++) { - this.activeState[i] = state[i]; + state[i] = this.activeState[i]; } } diff --git a/src/core/renderers/webgl/filters/Filter.js b/src/core/renderers/webgl/filters/Filter.js index 2c3eaad..4eb6128 100644 --- a/src/core/renderers/webgl/filters/Filter.js +++ b/src/core/renderers/webgl/filters/Filter.js @@ -9,7 +9,7 @@ * @memberof PIXI * @extends PIXI.Shader */ -class Filter extends Shader +export default class Filter extends Shader { /** * @param {string} [vertexSrc] - The source of the vertex shader. @@ -56,15 +56,16 @@ * @param {PIXI.RenderTarget} input - The input render target. * @param {PIXI.RenderTarget} output - The target to output to. * @param {boolean} clear - Should the output be cleared before rendering to it + * @param {object} [currentState] - It's current state of filter. + * There are some useful properties in the currentState : + * target, filters, sourceFrame, destinationFrame, renderTarget, resolution */ - apply(filterManager, input, output, clear) + apply(filterManager, input, output, clear, currentState) // eslint-disable-line no-unused-vars { // do as you please! - filterManager.applyFilter(this, input, output, clear); + filterManager.applyFilter(this, input, output, clear, currentState); // or just do a regular render.. } } - -export default Filter; diff --git a/src/core/renderers/webgl/filters/extractUniformsFromSrc.js b/src/core/renderers/webgl/filters/extractUniformsFromSrc.js index d78a0b1..4893214 100644 --- a/src/core/renderers/webgl/filters/extractUniformsFromSrc.js +++ b/src/core/renderers/webgl/filters/extractUniformsFromSrc.js @@ -12,7 +12,7 @@ function extractUniformsFromString(string) { - const maskRegex = new RegExp('^(projectionMatrix|uSampler|filterArea)$'); + const maskRegex = new RegExp('^(projectionMatrix|uSampler|filterArea|filterClamp)$'); const uniforms = {}; let nameSplit; diff --git a/src/core/renderers/webgl/managers/FilterManager.js b/src/core/renderers/webgl/managers/FilterManager.js index 43fc869..6c7422d 100644 --- a/src/core/renderers/webgl/managers/FilterManager.js +++ b/src/core/renderers/webgl/managers/FilterManager.js @@ -2,6 +2,7 @@ import RenderTarget from '../utils/RenderTarget'; import Quad from '../utils/Quad'; import { Rectangle } from '../../../math'; +import Shader from '../../../Shader'; import * as filterTransforms from '../filters/filterTransforms'; import bitTwiddle from 'bit-twiddle'; @@ -22,8 +23,6 @@ this.filters = []; this.target = null; this.resolution = 1; - - this.firstRun = true; } } @@ -155,7 +154,7 @@ if (filters.length === 1) { - filters[0].apply(this, currentState.renderTarget, lastState.renderTarget, false); + filters[0].apply(this, currentState.renderTarget, lastState.renderTarget, false, currentState); this.freePotRenderTarget(currentState.renderTarget); } else @@ -177,7 +176,7 @@ for (i = 0; i < filters.length - 1; ++i) { - filters[i].apply(this, flip, flop, true); + filters[i].apply(this, flip, flop, true, currentState); const t = flip; @@ -185,7 +184,7 @@ flop = t; } - filters[i].apply(this, flip, lastState.renderTarget, false); + filters[i].apply(this, flip, lastState.renderTarget, false, currentState); this.freePotRenderTarget(flip); this.freePotRenderTarget(flop); @@ -212,16 +211,28 @@ const renderer = this.renderer; const gl = renderer.gl; - renderer.bindShader(filter, true); + let shader = filter.glShaders[renderer.CONTEXT_UID]; - // TODO theres a better way! - // quad can be a mesh and we then should be able to pull this off without accessing the shader directly - const shader = renderer.shaderManager.getGLShader(); - - // TODO change quad to mesh.. - if (!this.firstRun) + // cacheing.. + if (!shader) { - this.firstRun = true; + if (filter.glShaderKey) + { + shader = this.shaderCache[filter.glShaderKey]; + + if (!shader) + { + shader = new Shader(this.gl, filter.vertexSrc, filter.fragmentSrc); + + filter.glShaders[renderer.CONTEXT_UID] = this.shaderCache[filter.glShaderKey] = shader; + } + } + else + { + shader = filter.glShaders[renderer.CONTEXT_UID] = new Shader(this.gl, filter.vertexSrc, filter.fragmentSrc); + } + + // TODO - this only needs to be done once? renderer.bindVao(null); this.quad.initVao(shader); @@ -244,58 +255,146 @@ renderer.maskManager.pushScissorMask(null, renderer.maskManager.scissorData); } - // renderer._bindGLShader(shader); + renderer.bindShader(shader); - let currentState; - const uniforms = filter.uniforms; + // free unit 0 for us, doesn't matter what was there + // don't try to restore it, because syncUniforms can upload it to another slot + // and it'll be a problem + const tex = this.renderer.emptyTextures[0]; - if (uniforms.filterArea) - { - currentState = this.filterData.stack[this.filterData.index]; - - const filterArea = uniforms.filterArea; - - filterArea[0] = currentState.renderTarget.size.width; - filterArea[1] = currentState.renderTarget.size.height; - filterArea[2] = currentState.sourceFrame.x; - filterArea[3] = currentState.sourceFrame.y; - - filter.uniforms.filterArea = filterArea; - } - - // use this to clamp displaced texture coords so they belong to filterArea - // see displacementFilter fragment shader for an example - if (uniforms.filterClamp) - { - currentState = this.filterData.stack[this.filterData.index]; - - const filterClamp = uniforms.filterClamp; - - filterClamp[0] = 0; - filterClamp[1] = 0; - filterClamp[2] = (currentState.sourceFrame.width - 1) / currentState.renderTarget.size.width; - filterClamp[3] = (currentState.sourceFrame.height - 1) / currentState.renderTarget.size.height; - - filter.uniforms.filterClamp = filterClamp; - } - - renderer.shaderManager.setUniforms(filter.uniforms); + this.renderer.boundTextures[0] = tex; + // this syncs the pixi filters uniforms with glsl uniforms + this.syncUniforms(shader, filter); renderer.state.setBlendMode(filter.blendMode); - // temporary bypass cache.. - const tex = this.renderer.boundTextures[0]; - gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, input.texture.texture); this.quad.vao.draw(this.renderer.gl.TRIANGLES, 6, 0); - // restore cache. gl.bindTexture(gl.TEXTURE_2D, tex._glTextures[this.renderer.CONTEXT_UID].texture); } /** + * Uploads the uniforms of the filter. + * + * @param {GLShader} shader - The underlying gl shader. + * @param {PIXI.Filter} filter - The filter we are synchronizing. + */ + syncUniforms(shader, filter) + { + const uniformData = filter.uniformData; + const uniforms = filter.uniforms; + + // 0 is reserved for the pixi texture so we start at 1! + let textureCount = 1; + let currentState; + + // filterArea and filterClamp that are handled by FilterManager directly + // they must not appear in uniformData + + if (shader.uniforms.filterArea) + { + currentState = this.filterData.stack[this.filterData.index]; + + const filterArea = shader.uniforms.filterArea; + + filterArea[0] = currentState.renderTarget.size.width; + filterArea[1] = currentState.renderTarget.size.height; + filterArea[2] = currentState.sourceFrame.x; + filterArea[3] = currentState.sourceFrame.y; + + shader.uniforms.filterArea = filterArea; + } + + // use this to clamp displaced texture coords so they belong to filterArea + // see displacementFilter fragment shader for an example + if (shader.uniforms.filterClamp) + { + currentState = currentState || this.filterData.stack[this.filterData.index]; + + const filterClamp = shader.uniforms.filterClamp; + + filterClamp[0] = 0; + filterClamp[1] = 0; + filterClamp[2] = (currentState.sourceFrame.width - 1) / currentState.renderTarget.size.width; + filterClamp[3] = (currentState.sourceFrame.height - 1) / currentState.renderTarget.size.height; + + shader.uniforms.filterClamp = filterClamp; + } + + // TODO Cacheing layer.. + for (const i in uniformData) + { + if (uniformData[i].type === 'sampler2D' && uniforms[i] !== 0) + { + if (uniforms[i].baseTexture) + { + shader.uniforms[i] = this.renderer.bindTexture(uniforms[i].baseTexture, textureCount); + } + else + { + shader.uniforms[i] = textureCount; + + // TODO + // this is helpful as renderTargets can also be set. + // Although thinking about it, we could probably + // make the filter texture cache return a RenderTexture + // rather than a renderTarget + const gl = this.renderer.gl; + + this.renderer.boundTextures[textureCount] = this.renderer.emptyTextures[textureCount]; + gl.activeTexture(gl.TEXTURE0 + textureCount); + + uniforms[i].texture.bind(); + } + + textureCount++; + } + else if (uniformData[i].type === 'mat3') + { + // check if its pixi matrix.. + if (uniforms[i].a !== undefined) + { + shader.uniforms[i] = uniforms[i].toArray(true); + } + else + { + shader.uniforms[i] = uniforms[i]; + } + } + else if (uniformData[i].type === 'vec2') + { + // check if its a point.. + if (uniforms[i].x !== undefined) + { + const val = shader.uniforms[i] || new Float32Array(2); + + val[0] = uniforms[i].x; + val[1] = uniforms[i].y; + shader.uniforms[i] = val; + } + else + { + shader.uniforms[i] = uniforms[i]; + } + } + else if (uniformData[i].type === 'float') + { + if (shader.uniforms.data[i].value !== uniformData[i]) + { + shader.uniforms[i] = uniforms[i]; + } + } + else + { + shader.uniforms[i] = uniforms[i]; + } + } + } + + /** * Gets a render target from the pool, or creates a new one. * * @param {boolean} clear - Should we clear the render texture when we get it? @@ -442,7 +541,6 @@ renderTarget.resolution = resolution; renderTarget.defaultFrame.width = renderTarget.size.width = minWidth / resolution; renderTarget.defaultFrame.height = renderTarget.size.height = minHeight / resolution; - renderTarget.attachStencilBuffer(); return renderTarget; } diff --git a/src/core/settings.js b/src/core/settings.js index bb08cb0..f4dd087 100644 --- a/src/core/settings.js +++ b/src/core/settings.js @@ -77,11 +77,11 @@ * * @static * @memberof PIXI.settings - * @type {RegExp|string} + * @type {RegExp} * @example `@2x` - * @default /@(.+)x/ + * @default /@([0-9\.]+)x/ */ - RETINA_PREFIX: /@(.+)x/, + RETINA_PREFIX: /@([0-9\.]+)x/, /** * The default render options if none are supplied to {@link PIXI.WebGLRenderer} @@ -175,14 +175,24 @@ SCALE_MODE: 0, /** - * Default specify float precision in shaders. + * Default specify float precision in vertex shader. + * + * @static + * @memberof PIXI.settings + * @type {PIXI.PRECISION} + * @default PIXI.PRECISION.HIGH + */ + PRECISION_VERTEX: 'highp', + + /** + * Default specify float precision in fragment shader. * * @static * @memberof PIXI.settings * @type {PIXI.PRECISION} * @default PIXI.PRECISION.MEDIUM */ - PRECISION: 'mediump', + PRECISION_FRAGMENT: 'mediump', /** * Can we upload the same buffer in a single frame? diff --git a/src/core/sprites/Sprite.js b/src/core/sprites/Sprite.js index 28b0b18..121bf58 100644 --- a/src/core/sprites/Sprite.js +++ b/src/core/sprites/Sprite.js @@ -212,11 +212,11 @@ } else { - w0 = orig.width * (1 - anchor._x); - w1 = orig.width * -anchor._x; + w1 = -anchor._x * orig.width; + w0 = w1 + orig.width; - h0 = orig.height * (1 - anchor._y); - h1 = orig.height * -anchor._y; + h1 = -anchor._y * orig.height; + h0 = h1 + orig.height; } // xy @@ -269,11 +269,11 @@ const tx = wt.tx; const ty = wt.ty; - const w0 = (orig.width) * (1 - anchor._x); - const w1 = (orig.width) * -anchor._x; + const w1 = -anchor._x * orig.width; + const w0 = w1 + orig.width; - const h0 = orig.height * (1 - anchor._y); - const h1 = orig.height * -anchor._y; + const h1 = -anchor._y * orig.height; + const h0 = h1 + orig.height; // xy vertexData[0] = (a * w1) + (c * h1) + tx; @@ -440,7 +440,7 @@ * * @static * @param {number|string|PIXI.BaseTexture|HTMLCanvasElement|HTMLVideoElement} source Source to create texture from - * @return {PIXI.Sprite} The newly created texture + * @return {PIXI.Sprite} The newly created sprite */ static from(source) { @@ -538,8 +538,8 @@ } /** - * The tint applied to the sprite. This is a hex value. A value of - * 0xFFFFFF will remove any tint effect. + * The tint applied to the sprite. This is a hex value. + * A value of 0xFFFFFF will remove any tint effect. * * @member {number} * @default 0xFFFFFF diff --git a/src/core/sprites/canvas/CanvasTinter.js b/src/core/sprites/canvas/CanvasTinter.js index 1cf316b..27e5e0c 100644 --- a/src/core/sprites/canvas/CanvasTinter.js +++ b/src/core/sprites/canvas/CanvasTinter.js @@ -75,8 +75,8 @@ crop.width *= resolution; crop.height *= resolution; - canvas.width = crop.width; - canvas.height = crop.height; + canvas.width = Math.ceil(crop.width); + canvas.height = Math.ceil(crop.height); context.fillStyle = `#${(`00000${(color | 0).toString(16)}`).substr(-6)}`; @@ -130,8 +130,8 @@ crop.width *= resolution; crop.height *= resolution; - canvas.width = crop.width; - canvas.height = crop.height; + canvas.width = Math.ceil(crop.width); + canvas.height = Math.ceil(crop.height); context.globalCompositeOperation = 'copy'; context.fillStyle = `#${(`00000${(color | 0).toString(16)}`).substr(-6)}`; @@ -172,8 +172,8 @@ crop.width *= resolution; crop.height *= resolution; - canvas.width = crop.width; - canvas.height = crop.height; + canvas.width = Math.ceil(crop.width); + canvas.height = Math.ceil(crop.height); context.globalCompositeOperation = 'copy'; context.drawImage( diff --git a/src/core/sprites/webgl/SpriteRenderer.js b/src/core/sprites/webgl/SpriteRenderer.js index 2affcd5..7c352e2 100644 --- a/src/core/sprites/webgl/SpriteRenderer.js +++ b/src/core/sprites/webgl/SpriteRenderer.js @@ -30,7 +30,7 @@ /** * Number of values sent in the vertex buffer. - * positionX, positionY, colorR, colorG, colorB = 5 + * aVertexPosition(2), aTextureCoord(1), aColor(1), aTextureId(1) = 5 * * @member {number} */ @@ -75,7 +75,6 @@ this.shader = null; this.currentIndex = 0; - TICK = 0; this.groups = []; for (let k = 0; k < this.size; k++) diff --git a/src/core/textures/BaseTexture.js b/src/core/textures/BaseTexture.js index 853de7e..aeccf39 100644 --- a/src/core/textures/BaseTexture.js +++ b/src/core/textures/BaseTexture.js @@ -232,16 +232,24 @@ this.realWidth = this.source.naturalWidth || this.source.videoWidth || this.source.width; this.realHeight = this.source.naturalHeight || this.source.videoHeight || this.source.height; - this.width = this.realWidth / this.resolution; - this.height = this.realHeight / this.resolution; - - this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); + this._updateDimensions(); } this.emit('update', this); } /** + * Update dimensions from real values + */ + _updateDimensions() + { + this.width = this.realWidth / this.resolution; + this.height = this.realHeight / this.resolution; + + this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); + } + + /** * Load a source. * * If the source is not-immediately-available, such as an image that needs to be @@ -525,11 +533,7 @@ this.realWidth = Math.round(svgWidth * this.sourceScale); this.realHeight = Math.round(svgHeight * this.sourceScale); - this.width = this.realWidth / this.resolution; - this.height = this.realHeight / this.resolution; - - // Check pow2 after scale - this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); + this._updateDimensions(); // Create a canvas element const canvas = document.createElement('canvas'); @@ -690,4 +694,52 @@ return baseTexture; } + + /** + * Helper function that creates a base texture based on the source you provide. + * The source can be - image url, image element, canvas element. + * + * @static + * @param {string|HTMLImageElement|HTMLCanvasElement} source - The source to create base texture from. + * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values + * @param {number} [sourceScale=(auto)] - Scale for the original image, used with Svg images. + * @return {PIXI.BaseTexture} The new base texture. + */ + static from(source, scaleMode, sourceScale) + { + if (typeof source === 'string') + { + return BaseTexture.fromImage(source, undefined, scaleMode, sourceScale); + } + else if (source instanceof HTMLImageElement) + { + const imageUrl = source.src; + let baseTexture = BaseTextureCache[imageUrl]; + + if (!baseTexture) + { + baseTexture = new BaseTexture(source, scaleMode); + baseTexture.imageUrl = imageUrl; + + if (sourceScale) + { + baseTexture.sourceScale = sourceScale; + } + + // if there is an @2x at the end of the url we are going to assume its a highres image + baseTexture.resolution = getResolutionOfUrl(imageUrl); + + BaseTextureCache[imageUrl] = baseTexture; + } + + return baseTexture; + } + else if (source instanceof HTMLCanvasElement) + { + return BaseTexture.fromCanvas(source, scaleMode); + } + + // lets assume its a base texture! + return source; + } } diff --git a/src/core/textures/Spritesheet.js b/src/core/textures/Spritesheet.js new file mode 100644 index 0000000..d469c62 --- /dev/null +++ b/src/core/textures/Spritesheet.js @@ -0,0 +1,274 @@ +import { Rectangle, Texture } from '../'; +import { getResolutionOfUrl, TextureCache } from '../utils'; + +/** + * Utility class for maintaining reference to a collection + * of Textures on a single Spritesheet. + * + * @class + * @memberof PIXI + */ +export default class Spritesheet +{ + /** + * The maximum number of Textures to build per process. + * + * @type {number} + * @default 1000 + */ + static get BATCH_SIZE() + { + return 1000; + } + + /** + * @param {PIXI.BaseTexture} baseTexture Reference to the source BaseTexture object. + * @param {Object} data - Spritesheet image data. + * @param {string} [resolutionFilename] - The filename to consider when determining + * the resolution of the spritesheet. If not provided, the imageUrl will + * be used on the BaseTexture. + */ + constructor(baseTexture, data, resolutionFilename = null) + { + /** + * Reference to ths source texture + * @type {PIXI.BaseTexture} + */ + this.baseTexture = baseTexture; + + /** + * Map of spritesheet textures. + * @type {Object} + */ + this.textures = {}; + + /** + * Reference to the original JSON data. + * @type {Object} + */ + this.data = data; + + /** + * The resolution of the spritesheet. + * @type {number} + */ + this.resolution = this._updateResolution( + resolutionFilename || this.baseTexture.imageUrl + ); + + /** + * Map of spritesheet frames. + * @type {Object} + * @private + */ + this._frames = this.data.frames; + + /** + * Collection of frame names. + * @type {string[]} + * @private + */ + this._frameKeys = Object.keys(this._frames); + + /** + * Current batch index being processed. + * @type {number} + * @private + */ + this._batchIndex = 0; + + /** + * Callback when parse is completed. + * @type {Function} + * @private + */ + this._callback = null; + } + + /** + * Generate the resolution from the filename or fallback + * to the meta.scale field of the JSON data. + * + * @private + * @param {string} resolutionFilename - The filename to use for resolving + * the default resolution. + * @return {number} Resolution to use for spritesheet. + */ + _updateResolution(resolutionFilename) + { + const scale = this.data.meta.scale; + + // Use a defaultValue of `null` to check if a url-based resolution is set + let resolution = getResolutionOfUrl(resolutionFilename, null); + + // No resolution found via URL + if (resolution === null) + { + // Use the scale value or default to 1 + resolution = scale !== undefined ? parseFloat(scale) : 1; + } + + // For non-1 resolutions, update baseTexture + if (resolution !== 1) + { + this.baseTexture.resolution = resolution; + this.baseTexture.update(); + } + + return resolution; + } + + /** + * Parser spritesheet from loaded data. This is done asynchronously + * to prevent creating too many Texture within a single process. + * + * @param {Function} callback - Callback when complete returns + * a map of the Textures for this spritesheet. + */ + parse(callback) + { + this._batchIndex = 0; + this._callback = callback; + + if (this._frameKeys.length <= Spritesheet.BATCH_SIZE) + { + this._processFrames(0); + this._parseComplete(); + } + else + { + this._nextBatch(); + } + } + + /** + * Process a batch of frames + * + * @private + * @param {number} initialFrameIndex - The index of frame to start. + */ + _processFrames(initialFrameIndex) + { + let frameIndex = initialFrameIndex; + const maxFrames = Spritesheet.BATCH_SIZE; + + while (frameIndex - initialFrameIndex < maxFrames && frameIndex < this._frameKeys.length) + { + const i = this._frameKeys[frameIndex]; + const rect = this._frames[i].frame; + + if (rect) + { + let frame = null; + let trim = null; + const orig = new Rectangle( + 0, + 0, + this._frames[i].sourceSize.w / this.resolution, + this._frames[i].sourceSize.h / this.resolution + ); + + if (this._frames[i].rotated) + { + frame = new Rectangle( + rect.x / this.resolution, + rect.y / this.resolution, + rect.h / this.resolution, + rect.w / this.resolution + ); + } + else + { + frame = new Rectangle( + rect.x / this.resolution, + rect.y / this.resolution, + rect.w / this.resolution, + rect.h / this.resolution + ); + } + + // Check to see if the sprite is trimmed + if (this._frames[i].trimmed) + { + trim = new Rectangle( + this._frames[i].spriteSourceSize.x / this.resolution, + this._frames[i].spriteSourceSize.y / this.resolution, + rect.w / this.resolution, + rect.h / this.resolution + ); + } + + this.textures[i] = new Texture( + this.baseTexture, + frame, + orig, + trim, + this._frames[i].rotated ? 2 : 0 + ); + + // lets also add the frame to pixi's global cache for fromFrame and fromImage functions + TextureCache[i] = this.textures[i]; + } + + frameIndex++; + } + } + + /** + * The parse has completed. + * + * @private + */ + _parseComplete() + { + const callback = this._callback; + + this._callback = null; + this._batchIndex = 0; + callback.call(this, this.textures); + } + + /** + * Begin the next batch of textures. + * + * @private + */ + _nextBatch() + { + this._processFrames(this._batchIndex * Spritesheet.BATCH_SIZE); + this._batchIndex++; + setTimeout(() => + { + if (this._batchIndex * Spritesheet.BATCH_SIZE < this._frameKeys.length) + { + this._nextBatch(); + } + else + { + this._parseComplete(); + } + }, 0); + } + + /** + * Destroy Spritesheet and don't use after this. + * + * @param {boolean} [destroyBase=false] Whether to destroy the base texture as well + */ + destroy(destroyBase = false) + { + for (const i in this.textures) + { + this.textures[i].destroy(); + } + this._frames = null; + this._frameKeys = null; + this.data = null; + this.textures = null; + if (destroyBase) + { + this.baseTexture.destroy(); + } + this.baseTexture = null; + } +} diff --git a/src/core/textures/Texture.js b/src/core/textures/Texture.js index 6403d4f..afe4254 100644 --- a/src/core/textures/Texture.js +++ b/src/core/textures/Texture.js @@ -3,7 +3,7 @@ import TextureUvs from './TextureUvs'; import EventEmitter from 'eventemitter3'; import { Rectangle } from '../math'; -import { TextureCache, BaseTextureCache } from '../utils'; +import { TextureCache, BaseTextureCache, getResolutionOfUrl } from '../utils'; /** * A texture stores the information that represents an image or part of an image. It cannot be added @@ -365,7 +365,8 @@ * The source can be - frame id, image url, video url, canvas element, video element, base texture * * @static - * @param {number|string|PIXI.BaseTexture|HTMLCanvasElement|HTMLVideoElement} source - Source to create texture from + * @param {number|string|HTMLImageElement|HTMLCanvasElement|HTMLVideoElement|PIXI.BaseTexture} + * source - Source to create texture from * @return {PIXI.Texture} The newly created texture */ static from(source) @@ -393,7 +394,7 @@ } else if (source instanceof HTMLImageElement) { - return new Texture(new BaseTexture(source)); + return new Texture(BaseTexture.from(source)); } else if (source instanceof HTMLCanvasElement) { @@ -413,6 +414,43 @@ } /** + * Create a texture from a source and add to the cache. + * + * @static + * @param {HTMLImageElement|HTMLCanvasElement} source - The input source. + * @param {String} imageUrl - File name of texture, for cache and resolving resolution. + * @param {String} [name] - Human readible name for the texture cache. If no name is + * specified, only `imageUrl` will be used as the cache ID. + * @return {PIXI.Texture} Output texture + */ + static fromLoader(source, imageUrl, name) + { + const baseTexture = new BaseTexture(source, null, getResolutionOfUrl(imageUrl)); + const texture = new Texture(baseTexture); + + baseTexture.imageUrl = imageUrl; + + // No name, use imageUrl instead + if (!name) + { + name = imageUrl; + } + + // lets also add the frame to pixi's global cache for fromFrame and fromImage fucntions + BaseTextureCache[name] = baseTexture; + TextureCache[name] = texture; + + // also add references by url if they are different. + if (name !== imageUrl) + { + BaseTextureCache[imageUrl] = baseTexture; + TextureCache[imageUrl] = texture; + } + + return texture; + } + + /** * Adds a texture to the global TextureCache. This cache is shared across the whole PIXI object. * * @static @@ -522,6 +560,29 @@ } } +function createWhiteTexture() +{ + const canvas = document.createElement('canvas'); + + canvas.width = 10; + canvas.height = 10; + + const context = canvas.getContext('2d'); + + context.fillStyle = 'white'; + context.fillRect(0, 0, 10, 10); + + return new Texture(new BaseTexture(canvas)); +} + +function removeAllHandlers(tex) +{ + tex.destroy = function _emptyDestroy() { /* empty */ }; + tex.on = function _emptyOn() { /* empty */ }; + tex.once = function _emptyOnce() { /* empty */ }; + tex.emit = function _emptyEmit() { /* empty */ }; +} + /** * An empty texture, used often to not have to create multiple empty textures. * Can not be destroyed. @@ -530,7 +591,14 @@ * @constant */ Texture.EMPTY = new Texture(new BaseTexture()); -Texture.EMPTY.destroy = function _emptyDestroy() { /* empty */ }; -Texture.EMPTY.on = function _emptyOn() { /* empty */ }; -Texture.EMPTY.once = function _emptyOnce() { /* empty */ }; -Texture.EMPTY.emit = function _emptyEmit() { /* empty */ }; +removeAllHandlers(Texture.EMPTY); + +/** + * A white texture of 10x10 size, used for graphics and other things + * Can not be destroyed. + * + * @static + * @constant + */ +Texture.WHITE = createWhiteTexture(); +removeAllHandlers(Texture.WHITE); diff --git a/src/core/ticker/Ticker.js b/src/core/ticker/Ticker.js index d98a1f0..17a1517 100644 --- a/src/core/ticker/Ticker.js +++ b/src/core/ticker/Ticker.js @@ -67,11 +67,12 @@ * is based, this value is neither capped nor scaled. * If the platform supports DOMHighResTimeStamp, * this value will have a precision of 1 µs. + * Defaults to target frame time * * @member {number} - * @default 1 / TARGET_FPMS + * @default 16.66 */ - this.elapsedMS = 1 / settings.TARGET_FPMS; // default to target frame time + this.elapsedMS = 1 / settings.TARGET_FPMS; /** * The last time {@link PIXI.ticker.Ticker#update} was invoked. diff --git a/src/core/utils/index.js b/src/core/utils/index.js index 6ff609f..be44750 100644 --- a/src/core/utils/index.js +++ b/src/core/utils/index.js @@ -2,6 +2,7 @@ import settings from '../settings'; import EventEmitter from 'eventemitter3'; import pluginTarget from './pluginTarget'; +import * as mixins from './mixin'; import * as isMobile from 'ismobilejs'; let nextUid = 0; @@ -33,6 +34,7 @@ * @type {mixin} */ pluginTarget, + mixins, }; /** @@ -93,7 +95,7 @@ */ export function rgb2hex(rgb) { - return (((rgb[0] * 255) << 16) + ((rgb[1] * 255) << 8) + (rgb[2] * 255)); + return (((rgb[0] * 255) << 16) + ((rgb[1] * 255) << 8) + (rgb[2] * 255 | 0)); } /** @@ -368,3 +370,43 @@ * @private */ export const ProgramCache = {}; + +/** + * Destroys all texture in the cache + * + * @memberof PIXI.utils + * @function destroyTextureCache + */ +export function destroyTextureCache() +{ + let key; + + for (key in TextureCache) + { + TextureCache[key].destroy(); + } + for (key in BaseTextureCache) + { + BaseTextureCache[key].destroy(); + } +} + +/** + * Removes all textures from cache, but does not destroy them + * + * @memberof PIXI.utils + * @function clearTextureCache + */ +export function clearTextureCache() +{ + let key; + + for (key in TextureCache) + { + delete TextureCache[key]; + } + for (key in BaseTextureCache) + { + delete BaseTextureCache[key]; + } +} diff --git a/src/core/utils/mixin.js b/src/core/utils/mixin.js new file mode 100644 index 0000000..114de2a --- /dev/null +++ b/src/core/utils/mixin.js @@ -0,0 +1,59 @@ +/** + * Mixes all enumerable properties and methods from a source object to a target object. + * + * @memberof PIXI.utils.mixins + * @function mixin + * @param {object} target The prototype or instance that properties and methods should be added to. + * @param {object} source The source of properties and methods to mix in. + */ +export function mixin(target, source) +{ + if (!target || !source) return; + // in ES8/ES2017, this would be really easy: + // Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); + + // get all the enumerable property keys + const keys = Object.keys(source); + + // loop through properties + for (let i = 0; i < keys.length; ++i) + { + const propertyName = keys[i]; + + // Set the property using the property descriptor - this works for accessors and normal value properties + Object.defineProperty(target, propertyName, Object.getOwnPropertyDescriptor(source, propertyName)); + } +} + +const mixins = []; + +/** + * Queues a mixin to be handled towards the end of the initialization of PIXI, so that deprecation + * can take effect. + * + * @memberof PIXI.utils.mixins + * @function delayMixin + * @private + * @param {object} target The prototype or instance that properties and methods should be added to. + * @param {object} source The source of properties and methods to mix in. + */ +export function delayMixin(target, source) +{ + mixins.push(target, source); +} + +/** + * Handles all mixins queued via delayMixin(). + * + * @memberof PIXI.utils.mixins + * @function performMixins + * @private + */ +export function performMixins() +{ + for (let i = 0; i < mixins.length; i += 2) + { + mixin(mixins[i], mixins[i + 1]); + } + mixins.length = 0; +} diff --git a/src/deprecation.js b/src/deprecation.js index 045a19f..b9c73b3 100644 --- a/src/deprecation.js +++ b/src/deprecation.js @@ -5,6 +5,7 @@ import * as filters from './filters'; import * as prepare from './prepare'; import * as loaders from './loaders'; +import * as interaction from './interaction'; // provide method to give a stack track for warnings // useful for tracking-down where deprecated methods/properties/classes @@ -534,7 +535,7 @@ { parent: 'GC_MODES', target: 'GC_MODE' }, { parent: 'WRAP_MODES', target: 'WRAP_MODE' }, { parent: 'SCALE_MODES', target: 'SCALE_MODE' }, - { parent: 'PRECISION', target: 'PRECISION' }, + { parent: 'PRECISION', target: 'PRECISION_FRAGMENT' }, ]; for (let i = 0; i < defaults.length; i++) @@ -558,6 +559,32 @@ }); } +Object.defineProperties(core.settings, { + + /** + * @static + * @name PRECISION + * @memberof PIXI.settings + * @see PIXI.PRECISION + * @deprecated since version 4.4.0 + */ + PRECISION: { + enumerable: true, + get() + { + warn('PIXI.settings.PRECISION has been deprecated, please use PIXI.settings.PRECISION_FRAGMENT'); + + return core.settings.PRECISION_FRAGMENT; + }, + set(value) + { + warn('PIXI.settings.PRECISION has been deprecated, please use PIXI.settings.PRECISION_FRAGMENT'); + + core.settings.PRECISION_FRAGMENT = value; + }, + }, +}); + Object.defineProperties(extras, { /** @@ -694,7 +721,7 @@ warn('determineFontProperties is now deprecated, please use the static calculateFontProperties method, ' + 'e.g : Text.calculateFontProperties(fontStyle);'); - return Text.calculateFontProperties(fontStyle); + return core.Text.calculateFontProperties(fontStyle); }; Object.defineProperties(core.TextStyle.prototype, { @@ -1003,3 +1030,69 @@ }, }, }); + +/** + * @name PIXI.interaction.interactiveTarget#defaultCursor + * @static + * @type {number} + * @see PIXI.interaction.interactiveTarget#cursor + * @deprecated since 4.3.0 + */ +Object.defineProperty(interaction.interactiveTarget, 'defaultCursor', { + set(value) + { + warn('Property defaultCursor has been replaced with \'cursor\'. '); + this.cursor = value; + }, + get() + { + warn('Property defaultCursor has been replaced with \'cursor\'. '); + + return this.cursor; + }, + enumerable: true, +}); + +/** + * @name PIXI.interaction.InteractionManager#defaultCursorStyle + * @static + * @type {string} + * @see PIXI.interaction.InteractionManager#cursorStyles + * @deprecated since 4.3.0 + */ +Object.defineProperty(interaction.InteractionManager, 'defaultCursorStyle', { + set(value) + { + warn('Property defaultCursorStyle has been replaced with \'cursorStyles.default\'. '); + this.cursorStyles.default = value; + }, + get() + { + warn('Property defaultCursorStyle has been replaced with \'cursorStyles.default\'. '); + + return this.cursorStyles.default; + }, +}); + +/** + * @name PIXI.interaction.InteractionManager#currentCursorStyle + * @static + * @type {string} + * @see PIXI.interaction.InteractionManager#cursorStyles + * @deprecated since 4.3.0 + */ +Object.defineProperty(interaction.InteractionManager, 'currentCursorStyle', { + set(value) + { + warn('Property currentCursorStyle has been removed.' + + 'See the currentCursorMode property, which works differently.'); + this.currentCursorMode = value; + }, + get() + { + warn('Property currentCursorStyle has been removed.' + + 'See the currentCursorMode property, which works differently.'); + + return this.currentCursorMode; + }, +}); diff --git a/src/extras/BitmapText.js b/src/extras/BitmapText.js index fb6c343..0a0e150 100644 --- a/src/extras/BitmapText.js +++ b/src/extras/BitmapText.js @@ -455,6 +455,73 @@ return this._textHeight; } + + /** + * Register a bitmap font with data and a texture. + * + * @static + * @param {XMLDocument} xml - The XML document data. + * @param {PIXI.Texture} texture - Texture with all symbols. + * @return {Object} Result font object with font, size, lineHeight and char fields. + */ + static registerFont(xml, texture) + { + const data = {}; + const info = xml.getElementsByTagName('info')[0]; + const common = xml.getElementsByTagName('common')[0]; + + data.font = info.getAttribute('face'); + data.size = parseInt(info.getAttribute('size'), 10); + data.lineHeight = parseInt(common.getAttribute('lineHeight'), 10); + data.chars = {}; + + // parse letters + const letters = xml.getElementsByTagName('char'); + + for (let i = 0; i < letters.length; i++) + { + const letter = letters[i]; + const charCode = parseInt(letter.getAttribute('id'), 10); + + const textureRect = new core.Rectangle( + parseInt(letter.getAttribute('x'), 10) + texture.frame.x, + parseInt(letter.getAttribute('y'), 10) + texture.frame.y, + parseInt(letter.getAttribute('width'), 10), + parseInt(letter.getAttribute('height'), 10) + ); + + data.chars[charCode] = { + xOffset: parseInt(letter.getAttribute('xoffset'), 10), + yOffset: parseInt(letter.getAttribute('yoffset'), 10), + xAdvance: parseInt(letter.getAttribute('xadvance'), 10), + kerning: {}, + texture: new core.Texture(texture.baseTexture, textureRect), + + }; + } + + // parse kernings + const kernings = xml.getElementsByTagName('kerning'); + + for (let i = 0; i < kernings.length; i++) + { + const kerning = kernings[i]; + const first = parseInt(kerning.getAttribute('first'), 10); + const second = parseInt(kerning.getAttribute('second'), 10); + const amount = parseInt(kerning.getAttribute('amount'), 10); + + if (data.chars[second]) + { + data.chars[second].kerning[first] = amount; + } + } + + // I'm leaving this as a temporary fix so we can test the bitmap fonts in v3 + // but it's very likely to change + BitmapText.fonts[data.font] = data; + + return data; + } } BitmapText.fonts = {}; diff --git a/src/index.js b/src/index.js index 59356cf..c553427 100644 --- a/src/index.js +++ b/src/index.js @@ -16,6 +16,10 @@ import * as particles from './particles'; import * as prepare from './prepare'; +// handle mixins now, after all code has been added, including deprecation +import { utils } from './core'; +utils.mixins.performMixins(); + export { accessibility, extract, diff --git a/src/interaction/InteractionManager.js b/src/interaction/InteractionManager.js index 7d51134..8baf13a 100644 --- a/src/interaction/InteractionManager.js +++ b/src/interaction/InteractionManager.js @@ -1,18 +1,20 @@ 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'; -// Mix interactiveTarget into core.DisplayObject.prototype -Object.assign( +// Mix interactiveTarget into core.DisplayObject.prototype, after deprecation has been handled +core.utils.mixins.delayMixin( core.DisplayObject.prototype, 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 +70,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 +101,6 @@ this.eventData = new InteractionEvent(); /** - * Tiny little interactiveData pool ! - * - * @member {PIXI.interaction.InteractionData[]} - */ - this.interactiveDataPool = []; - - /** * The DOM element to bind to. * * @private @@ -153,66 +155,12 @@ */ this.supportsPointerEvents = !!window.PointerEvent; - /** - * Are touch events being 'normalized' and converted into pointer events if pointer events are not supported - * For example, on a touch screen mobile device, a touchstart would also be emitted as a pointerdown - * - * @private - * @readonly - * @member {boolean} - */ - this.normalizeTouchEvents = !this.supportsPointerEvents && this.supportsTouchEvents; - - /** - * Are mouse events being 'normalized' and converted into pointer events if pointer events are not supported - * For example, on a desktop pc, a mousedown would also be emitted as a pointerdown - * - * @private - * @readonly - * @member {boolean} - */ - this.normalizeMouseEvents = !this.supportsPointerEvents && !MobileDevice.any; - // this will make it so that you don't have to call bind all the time /** * @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 +168,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,41 +199,32 @@ this.onPointerOver = this.onPointerOver.bind(this); /** - * @private - * @member {Function} + * Dictionary of how different cursor modes are handled. Strings are handled as CSS cursor + * values, objects are handled as dictionaries of CSS values for interactionDOMElement, + * and functions are called instead of changing the CSS. + * Default CSS cursor values are provided for 'default' and 'pointer' modes. + * @member {Object.)>} */ - this.onTouchStart = this.onTouchStart.bind(this); - this.processTouchStart = this.processTouchStart.bind(this); + this.cursorStyles = { + default: 'inherit', + pointer: 'pointer', + }; /** - * @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. - * - * @member {string} - * @default 'inherit' - */ - this.defaultCursorStyle = 'inherit'; - - /** - * The css style of the cursor that is being used. + * The mode of the cursor that is being used. + * The value of this is a key from the cursorStyles dictionary. * * @member {string} */ - this.currentCursorStyle = 'inherit'; + this.currentCursorMode = null; + + /** + * Internal cached let. + * + * @private + * @member {string} + */ + this.cursor = null; /** * Internal cached let. @@ -303,6 +249,7 @@ * object. * * @event mousedown + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -311,6 +258,7 @@ * on the display object. * * @event rightdown + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -319,6 +267,7 @@ * object. * * @event mouseup + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -327,6 +276,7 @@ * over the display object. * * @event rightup + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -335,6 +285,7 @@ * the display object. * * @event click + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -343,6 +294,7 @@ * and released on the display object. * * @event rightclick + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -352,6 +304,7 @@ * [mousedown]{@link PIXI.interaction.InteractionManager#event:mousedown}. * * @event mouseupoutside + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -361,6 +314,7 @@ * [rightdown]{@link PIXI.interaction.InteractionManager#event:rightdown}. * * @event rightupoutside + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -368,6 +322,7 @@ * Fired when a pointer device (usually a mouse) is moved while over the display object * * @event mousemove + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -375,6 +330,7 @@ * Fired when a pointer device (usually a mouse) is moved onto the display object * * @event mouseover + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -382,6 +338,7 @@ * Fired when a pointer device (usually a mouse) is moved off the display object * * @event mouseout + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -389,6 +346,7 @@ * Fired when a pointer device button is pressed on the display object. * * @event pointerdown + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -396,6 +354,14 @@ * Fired when a pointer device button is released over the display object. * * @event pointerup + * @type {PIXI.interaction.InteractionData} + * @memberof PIXI.interaction.InteractionManager# + */ + + /** + * Fired when the operating system cancels a pointer event + * + * @event pointercancel * @memberof PIXI.interaction.InteractionManager# */ @@ -403,6 +369,7 @@ * Fired when a pointer device button is pressed and released on the display object. * * @event pointertap + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -411,6 +378,7 @@ * registered a [pointerdown]{@link PIXI.interaction.InteractionManager#event:pointerdown}. * * @event pointerupoutside + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -418,6 +386,7 @@ * Fired when a pointer device is moved while over the display object * * @event pointermove + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -425,6 +394,7 @@ * Fired when a pointer device is moved onto the display object * * @event pointerover + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -432,6 +402,7 @@ * Fired when a pointer device is moved off the display object * * @event pointerout + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -439,6 +410,7 @@ * Fired when a touch point is placed on the display object. * * @event touchstart + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -446,6 +418,14 @@ * Fired when a touch point is removed from the display object. * * @event touchend + * @type {PIXI.interaction.InteractionData} + * @memberof PIXI.interaction.InteractionManager# + */ + + /** + * Fired when the operating system cancels a touch + * + * @event touchcancel * @memberof PIXI.interaction.InteractionManager# */ @@ -453,6 +433,7 @@ * Fired when a touch point is placed and removed from the display object. * * @event tap + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -461,6 +442,7 @@ * registered a [touchstart]{@link PIXI.interaction.InteractionManager#event:touchstart}. * * @event touchendoutside + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -468,6 +450,7 @@ * Fired when a touch point is moved along the display object. * * @event touchmove + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ } @@ -524,45 +507,30 @@ { window.document.addEventListener('pointermove', this.onPointerMove, true); this.interactionDOMElement.addEventListener('pointerdown', this.onPointerDown, true); - this.interactionDOMElement.addEventListener('pointerout', this.onPointerOut, true); + // pointerout is fired in addition to pointerup (for touch events) and pointercancel + // we already handle those, so for the purposes of what we do in onPointerOut, we only + // care about the pointerleave event + this.interactionDOMElement.addEventListener('pointerleave', 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; @@ -596,45 +564,26 @@ { window.document.removeEventListener('pointermove', this.onPointerMove, true); this.interactionDOMElement.removeEventListener('pointerdown', this.onPointerDown, true); - this.interactionDOMElement.removeEventListener('pointerout', this.onPointerOut, true); + this.interactionDOMElement.removeEventListener('pointerleave', 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) + 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.onPointerDown, true); + this.interactionDOMElement.removeEventListener('touchcancel', this.onPointerCancel, 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); - - 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 = null; @@ -664,7 +613,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; @@ -672,25 +621,80 @@ return; } - this.cursor = this.defaultCursorStyle; + this.cursor = null; // 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(); - - this.processInteractive(this.mouse.global, this.renderer._lastObjectRendered, this.processMouseOverOut, true); - - if (this.currentCursorStyle !== this.cursor) + for (const k in this.activeInteractionData) { - this.currentCursorStyle = this.cursor; - this.interactionDOMElement.style.cursor = this.cursor; + // eslint-disable-next-line no-prototype-builtins + if (this.activeInteractionData.hasOwnProperty(k)) + { + const interactionData = this.activeInteractionData[k]; + + if (interactionData.originalEvent && interactionData.pointerType !== 'touch') + { + const interactionEvent = this.configureInteractionEventForDOMEvent( + this.eventData, + interactionData.originalEvent, + interactionData + ); + + this.processInteractive( + interactionEvent, + this.renderer._lastObjectRendered, + this.processPointerOverOut, + true + ); + } + } } + this.setCursorMode(this.cursor); + // TODO } /** + * Sets the current cursor mode, handling any callbacks or CSS style changes. + * + * @param {string} mode - cursor mode, a key from the cursorStyles dictionary + */ + setCursorMode(mode) + { + mode = mode || 'default'; + // if the mode didn't actually change, bail early + if (this.currentCursorMode === mode) + { + return; + } + this.currentCursorMode = mode; + const style = this.cursorStyles[mode]; + + // only do things if there is a cursor style for it + if (style) + { + switch (typeof style) + { + case 'string': + // string styles are handled as cursor CSS + this.interactionDOMElement.style.cursor = style; + break; + case 'function': + // functions are just called, and passed the cursor mode + style(mode); + break; + case 'object': + // if it is an object, assume that it is a dictionary of CSS styles, + // apply it to the interactionDOMElement + Object.assign(this.interactionDOMElement.style, style); + break; + } + } + } + + /** * Dispatches an event on the display object that was interacted with * * @param {PIXI.Container|PIXI.Sprite|PIXI.extras.TilingSprite} displayObject - the display object in question @@ -748,22 +752,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 @@ -787,9 +795,8 @@ { interactiveParent = false; } - - // it has a mask! Then lets hit test that before continuing.. - if (hitTest && displayObject._mask) + // it has a mask! Then lets hit test that before continuing + else if (hitTest && displayObject._mask) { if (!displayObject._mask.containsPoint(point)) { @@ -797,14 +804,7 @@ } } - // it has a filterArea! Same as mask but easier, its a rectangle - if (hitTest && displayObject.filterArea) - { - if (!displayObject.filterArea.contains(point.x, point.y)) - { - hitTest = false; - } - } + let keepHitTestingAfterChildren = hitTest; // ** FREE TIP **! If an object is not interactive or has no buttons in it // (such as a game scene!) set interactiveChildren to false for that displayObject. @@ -818,7 +818,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 @@ -838,15 +838,20 @@ // This means we no longer need to hit test anything else. We still need to run // through all objects, but we don't need to perform any hit tests. - // { - hitTest = false; - // } + keepHitTestingAfterChildren = false; + + if (child.interactive) + { + hitTest = false; + } // we can break now as we have hit an object. } } } + hitTest = keepHitTestingAfterChildren; + // no point running this if the item is not interactive or does not have an interactive parent. if (interactive) { @@ -868,14 +873,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 +886,175 @@ } /** - * 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].isNormalized) { - 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); + if (!displayObject.trackedPointers[id]) + { + displayObject.trackedPointers[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.trackedPointers[id].rightDown = true; + } + else + { + displayObject.trackedPointers[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.trackedPointers[id] !== undefined) + { + delete displayObject.trackedPointers[id]; + this.dispatchEvent(displayObject, 'pointercancel', interactionEvent); + + if (e.type === 'touchcancel' || e.pointerType === 'touch') + { + this.dispatchEvent(displayObject, 'touchcancel', interactionEvent); + } } } @@ -1159,42 +1066,94 @@ */ 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) { - if (hit) - { - this.dispatchEvent(displayObject, 'pointerup', this.eventData); + const e = interactionEvent.data.originalEvent; - if (displayObject._pointerDown) + const id = interactionEvent.data.identifier; + + const trackingData = displayObject.trackedPointers[id]; + + const isTouch = (e.type === 'touchend' || e.pointerType === 'touch'); + + const isMouse = (e.type.indexOf('mouse') === 0 || e.pointerType === 'mouse'); + + // 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) { - displayObject._pointerDown = false; - this.dispatchEvent(displayObject, 'pointertap', this.eventData); + 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); + } + // update the down state of the tracking data + if (trackingData) + { + if (isRightButton) + { + trackingData.rightDown = hit; + } + else + { + trackingData.leftDown = hit; + } } } - else if (displayObject._pointerDown) + + // Pointers and Touches, and Mouse + if (hit) { - displayObject._pointerDown = false; - this.dispatchEvent(displayObject, 'pointerupoutside', this.eventData); + this.dispatchEvent(displayObject, 'pointerup', interactionEvent); + if (isTouch) this.dispatchEvent(displayObject, 'touchend', interactionEvent); + + if (trackingData) + { + this.dispatchEvent(displayObject, 'pointertap', interactionEvent); + if (isTouch) + { + this.dispatchEvent(displayObject, 'tap', interactionEvent); + // touches are no longer over (if they ever were) when we get the touchend + // so we should ensure that we don't keep pretending that they are + trackingData.over = false; + } + } + } + else if (trackingData) + { + this.dispatchEvent(displayObject, 'pointerupoutside', interactionEvent); + if (isTouch) this.dispatchEvent(displayObject, 'touchendoutside', interactionEvent); + } + // Only remove the tracking data if there is no over/down state still associated with it + if (trackingData && trackingData.none) + { + delete displayObject.trackedPointers[id]; } } @@ -1202,39 +1161,78 @@ * 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 = null; + } - 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') + { + this.setCursorMode(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 (isMouse) { - 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 +1240,94 @@ * 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.setCursorMode(null); + } - 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'); + + let trackingData = displayObject.trackedPointers[id]; + + // if we just moused over the display object, then we need to track that state + if (hit && !trackingData) + { + trackingData = displayObject.trackedPointers[id] = new InteractionTrackingData(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); + } + } + + // only change the cursor if it has not already been changed (by something deeper in the + // display tree) + if (isMouse && this.cursor === null) + { + this.cursor = displayObject.cursor; } } - 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); + } + // if there is no mouse down information for the pointer, then it is safe to delete + if (trackingData.none) + { + delete displayObject.trackedPointers[id]; + } } } @@ -1287,253 +1335,154 @@ * 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 && pointerEvent.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; + + // mark the touch as normalized, just so that we know we did it + touch.isNormalized = true; + + normalizedEvents.push(touch); + } } - else if (this.normalizeMouseEvents) + // apparently PointerEvent subclasses MouseEvent, so yay + else if (event instanceof MouseEvent && (!this.supportsPointerEvents || !(event instanceof window.PointerEvent))) { if (typeof event.isPrimary === 'undefined') event.isPrimary = true; if (typeof event.width === 'undefined') event.width = 1; @@ -1541,10 +1490,21 @@ 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; + + // mark the mouse event as normalized, just so that we know we did it + event.isNormalized = true; + + normalizedEvents.push(event); } + else + { + normalizedEvents.push(event); + } + + return normalizedEvents; } /** @@ -1563,30 +1523,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 +1542,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..6a1f69e --- /dev/null +++ b/src/interaction/InteractionTrackingData.js @@ -0,0 +1,147 @@ +/** + * 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 inactive (not over or down)? + * + * @member {number} + * @memberof PIXI.interaction.InteractionTrackingData# + */ + get none() + { + return this._flags === this.constructor.FLAGS.NONE; + } + + /** + * 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..d252fba 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 * ); */ @@ -38,68 +38,56 @@ hitArea: null, /** - * If enabled, the mouse cursor will change when hovered over the displayObject if it is interactive + * If enabled, the mouse cursor use the pointer behavior when hovered over the displayObject if it is interactive + * Setting this changes the 'cursor' property to `'pointer'`. * - * @inner {boolean} + * @member {boolean} + * @memberof PIXI.interaction.interactiveTarget# */ - buttonMode: false, + get buttonMode() + { + return this.cursor === 'pointer'; + }, + set buttonMode(value) + { + if (value) + { + this.cursor = 'pointer'; + } + else if (this.cursor === 'pointer') + { + this.cursor = null; + } + }, /** - * If buttonMode is enabled, this defines what CSS cursor property is used when the mouse cursor - * is hovered over the displayObject + * This defines what cursor mode is used when the mouse cursor + * is hovered over the displayObject. * * @see https://developer.mozilla.org/en/docs/Web/CSS/cursor * * @inner {string} */ - defaultCursor: 'pointer', - - // some internal checks.. - /** - * Internal check to detect if the mouse cursor is hovered over the displayObject - * - * @inner {boolean} - * @private - */ - _over: false, + cursor: null, /** - * Internal check to detect if the left mouse button is pressed on the displayObject + * Internal set of all active pointers, by identifier * - * @inner {boolean} + * @member {Map} + * @memberof PIXI.interaction.interactiveTarget# * @private */ - _isLeftDown: false, + get trackedPointers() + { + if (this._trackedPointers === undefined) this._trackedPointers = {}; + + return this._trackedPointers; + }, /** - * Internal check to detect if the right mouse button is pressed on the displayObject + * Map of all tracked pointers, by identifier. Use trackedPointers to access. * - * @inner {boolean} - * @private + * @private {Map} */ - _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, + _trackedPointers: undefined, }; diff --git a/src/loaders/bitmapFontParser.js b/src/loaders/bitmapFontParser.js index cc2618c..f4cf996 100644 --- a/src/loaders/bitmapFontParser.js +++ b/src/loaders/bitmapFontParser.js @@ -1,63 +1,19 @@ import * as path from 'path'; -import { Rectangle, Texture, utils } from '../core'; +import { utils } from '../core'; import { Resource } from 'resource-loader'; import { BitmapText } from '../extras'; +/** + * Register a BitmapText font from loader resource. + * + * @function parseBitmapFontData + * @memberof PIXI.loaders + * @param {PIXI.loaders.Resource} resource - Loader resource. + * @param {PIXI.Texture} texture - Reference to texture. + */ export function parse(resource, texture) { - const data = {}; - const info = resource.data.getElementsByTagName('info')[0]; - const common = resource.data.getElementsByTagName('common')[0]; - - data.font = info.getAttribute('face'); - data.size = parseInt(info.getAttribute('size'), 10); - data.lineHeight = parseInt(common.getAttribute('lineHeight'), 10); - data.chars = {}; - - // parse letters - const letters = resource.data.getElementsByTagName('char'); - - for (let i = 0; i < letters.length; i++) - { - const charCode = parseInt(letters[i].getAttribute('id'), 10); - - const textureRect = new Rectangle( - parseInt(letters[i].getAttribute('x'), 10) + texture.frame.x, - parseInt(letters[i].getAttribute('y'), 10) + texture.frame.y, - parseInt(letters[i].getAttribute('width'), 10), - parseInt(letters[i].getAttribute('height'), 10) - ); - - data.chars[charCode] = { - xOffset: parseInt(letters[i].getAttribute('xoffset'), 10), - yOffset: parseInt(letters[i].getAttribute('yoffset'), 10), - xAdvance: parseInt(letters[i].getAttribute('xadvance'), 10), - kerning: {}, - texture: new Texture(texture.baseTexture, textureRect), - - }; - } - - // parse kernings - const kernings = resource.data.getElementsByTagName('kerning'); - - for (let i = 0; i < kernings.length; i++) - { - const first = parseInt(kernings[i].getAttribute('first'), 10); - const second = parseInt(kernings[i].getAttribute('second'), 10); - const amount = parseInt(kernings[i].getAttribute('amount'), 10); - - if (data.chars[second]) - { - data.chars[second].kerning[first] = amount; - } - } - - resource.bitmapFont = data; - - // I'm leaving this as a temporary fix so we can test the bitmap fonts in v3 - // but it's very likely to change - BitmapText.fonts[data.font] = data; + resource.bitmapFont = BitmapText.registerFont(resource.data, texture); } export default function () diff --git a/src/loaders/index.js b/src/loaders/index.js index 8942436..5189793 100644 --- a/src/loaders/index.js +++ b/src/loaders/index.js @@ -5,4 +5,11 @@ export { default as bitmapFontParser, parse as parseBitmapFontData } from './bitmapFontParser'; export { default as spritesheetParser } from './spritesheetParser'; export { default as textureParser } from './textureParser'; + +/** + * Reference to **resource-loader**'s Resource class. + * See https://github.com/englercj/resource-loader + * @class Resource + * @memberof PIXI.loaders + */ export { Resource } from 'resource-loader'; diff --git a/src/loaders/loader.js b/src/loaders/loader.js index 0a670cb..767a411 100644 --- a/src/loaders/loader.js +++ b/src/loaders/loader.js @@ -7,20 +7,47 @@ /** * - * The new loader, extends Resource Loader by Chad Engler : https://github.com/englercj/resource-loader + * The new loader, extends Resource Loader by Chad Engler: https://github.com/englercj/resource-loader * * ```js - * let loader = PIXI.loader; // pixi exposes a premade instance for you to use. + * const loader = PIXI.loader; // pixi exposes a premade instance for you to use. * //or - * let loader = new PIXI.loaders.Loader(); // you can also create your own if you want + * const loader = new PIXI.loaders.Loader(); // you can also create your own if you want * - * loader.add('bunny', 'data/bunny.png'); - * loader.add('spaceship', 'assets/spritesheet.json'); + * const sprites = {}; + * + * // Chainable `add` to enqueue a resource + * loader.add('bunny', 'data/bunny.png') + * .add('spaceship', 'assets/spritesheet.json'); * loader.add('scoreFont', 'assets/score.fnt'); * - * loader.once('complete',onAssetsLoaded); + * // Chainable `pre` to add a middleware that runs for each resource, *before* loading that resource. + * // This is useful to implement custom caching modules (using filesystem, indexeddb, memory, etc). + * loader.pre(cachingMiddleware); * - * loader.load(); + * // Chainable `use` to add a middleware that runs for each resource, *after* loading that resource. + * // This is useful to implement custom parsing modules (like spritesheet parsers, spine parser, etc). + * loader.use(parsingMiddleware); + * + * // The `load` method loads the queue of resources, and calls the passed in callback called once all + * // resources have loaded. + * loader.load((loader, resources) => { + * // resources is an object where the key is the name of the resource loaded and the value is the resource object. + * // They have a couple default properties: + * // - `url`: The URL that the resource was loaded from + * // - `error`: The error that happened when trying to load (if any) + * // - `data`: The raw data that was loaded + * // also may contain other properties based on the middleware that runs. + * sprites.bunny = new PIXI.TilingSprite(resources.bunny.texture); + * sprites.spaceship = new PIXI.TilingSprite(resources.spaceship.texture); + * sprites.scoreFont = new PIXI.TilingSprite(resources.scoreFont.texture); + * }); + * + * // throughout the process multiple signals can be dispatched. + * loader.onProgress.add(() => {}); // called once per loaded/errored file + * loader.onError.add(() => {}); // called once per errored file + * loader.onLoad.add(() => {}); // called once per loaded file + * loader.onComplete.add(() => {}); // called once when the queued resources all load. * ``` * * @see https://github.com/englercj/resource-loader diff --git a/src/loaders/spritesheetParser.js b/src/loaders/spritesheetParser.js index 7570332..e57dfb2 100644 --- a/src/loaders/spritesheetParser.js +++ b/src/loaders/spritesheetParser.js @@ -1,8 +1,6 @@ import { Resource } from 'resource-loader'; import path from 'path'; -import * as core from '../core'; - -const BATCH_SIZE = 1000; +import { Spritesheet } from '../core'; export default function () { @@ -43,134 +41,18 @@ // load the image for this sheet this.add(imageResourceName, resourcePath, loadOptions, function onImageLoad(res) { - resource.textures = {}; + const spritesheet = new Spritesheet( + res.texture.baseTexture, + resource.data, + resource.url + ); - const frames = resource.data.frames; - const frameKeys = Object.keys(frames); - const baseTexture = res.texture.baseTexture; - const scale = resource.data.meta.scale; - - // Use a defaultValue of `null` to check if a url-based resolution is set - let resolution = core.utils.getResolutionOfUrl(resource.url, null); - - // No resolution found via URL - if (resolution === null) + spritesheet.parse(() => { - // Use the scale value or default to 1 - resolution = scale !== undefined ? scale : 1; - } - - // For non-1 resolutions, update baseTexture - if (resolution !== 1) - { - baseTexture.resolution = resolution; - baseTexture.update(); - } - - let batchIndex = 0; - - function processFrames(initialFrameIndex, maxFrames) - { - let frameIndex = initialFrameIndex; - - while (frameIndex - initialFrameIndex < maxFrames && frameIndex < frameKeys.length) - { - const i = frameKeys[frameIndex]; - const rect = frames[i].frame; - - if (rect) - { - let frame = null; - let trim = null; - const orig = new core.Rectangle( - 0, - 0, - frames[i].sourceSize.w / resolution, - frames[i].sourceSize.h / resolution - ); - - if (frames[i].rotated) - { - frame = new core.Rectangle( - rect.x / resolution, - rect.y / resolution, - rect.h / resolution, - rect.w / resolution - ); - } - else - { - frame = new core.Rectangle( - rect.x / resolution, - rect.y / resolution, - rect.w / resolution, - rect.h / resolution - ); - } - - // Check to see if the sprite is trimmed - if (frames[i].trimmed) - { - trim = new core.Rectangle( - frames[i].spriteSourceSize.x / resolution, - frames[i].spriteSourceSize.y / resolution, - rect.w / resolution, - rect.h / resolution - ); - } - - resource.textures[i] = new core.Texture( - baseTexture, - frame, - orig, - trim, - frames[i].rotated ? 2 : 0 - ); - - // lets also add the frame to pixi's global cache for fromFrame and fromImage functions - core.utils.TextureCache[i] = resource.textures[i]; - } - - frameIndex++; - } - } - - function shouldProcessNextBatch() - { - return batchIndex * BATCH_SIZE < frameKeys.length; - } - - function processNextBatch(done) - { - processFrames(batchIndex * BATCH_SIZE, BATCH_SIZE); - batchIndex++; - setTimeout(done, 0); - } - - function iteration() - { - processNextBatch(() => - { - if (shouldProcessNextBatch()) - { - iteration(); - } - else - { - next(); - } - }); - } - - if (frameKeys.length <= BATCH_SIZE) - { - processFrames(0, BATCH_SIZE); + resource.spritesheet = spritesheet; + resource.textures = spritesheet.textures; next(); - } - else - { - iteration(); - } + }); }); }; } diff --git a/src/loaders/textureParser.js b/src/loaders/textureParser.js index 5398a7f..480edc1 100644 --- a/src/loaders/textureParser.js +++ b/src/loaders/textureParser.js @@ -1,5 +1,5 @@ -import * as core from '../core'; import { Resource } from 'resource-loader'; +import Texture from '../core/textures/Texture'; export default function () { @@ -8,23 +8,12 @@ // create a new texture if the data is an Image object if (resource.data && resource.type === Resource.TYPE.IMAGE) { - const baseTexture = new core.BaseTexture(resource.data, null, core.utils.getResolutionOfUrl(resource.url)); - - baseTexture.imageUrl = resource.url; - resource.texture = new core.Texture(baseTexture); - - // lets also add the frame to pixi's global cache for fromFrame and fromImage fucntions - core.utils.BaseTextureCache[resource.name] = baseTexture; - core.utils.TextureCache[resource.name] = resource.texture; - - // also add references by url if they are different. - if (resource.name !== resource.url) - { - core.utils.BaseTextureCache[resource.url] = baseTexture; - core.utils.TextureCache[resource.url] = resource.texture; - } + resource.texture = Texture.fromLoader( + resource.data, + resource.url, + resource.name + ); } - next(); }; } diff --git a/test/core/Container.js b/test/core/Container.js index 8a10b40..d11be33 100644 --- a/test/core/Container.js +++ b/test/core/Container.js @@ -1,5 +1,33 @@ 'use strict'; +function testAddChild(fn) +{ + fn(function (container, obj) + { + container.addChild(obj); + }); + fn(function (container, obj) + { + container.addChildAt(obj); + }); +} + +function testRemoveChild(fn) +{ + fn(function (container, obj) + { + container.removeChild(obj); + }); + fn(function (container, obj) + { + container.removeChildAt(container.children.indexOf(obj)); + }); + fn(function (container, obj) + { + container.removeChildren(container.children.indexOf(obj), container.children.indexOf(obj) + 1); + }); +} + describe('PIXI.Container', function () { describe('parent', function () @@ -70,6 +98,49 @@ expect(spy).to.have.been.called; expect(spy).to.have.been.calledWith(0); }); + + it('should flag child transform and container bounds for recalculation', testAddChild(function (mockAddChild) + { + const container = new PIXI.Container(); + const child = new PIXI.Container(); + + container.getBounds(); + child.getBounds(); + + const boundsID = container._boundsID; + const childParentID = child.transform._parentID; + + mockAddChild(container, child); + + expect(boundsID).to.not.be.equals(container._boundsID); + expect(childParentID).to.not.be.equals(child.transform._parentID); + })); + + it('should recalculate added child correctly', testAddChild(function (mockAddChild) + { + const parent = new PIXI.Container(); + const container = new PIXI.Container(); + const graphics = new PIXI.Graphics(); + + parent.addChild(container); + + graphics.drawRect(0, 0, 10, 10); + container.position.set(100, 200); + container.updateTransform(); + + graphics.getBounds(); + // Oops, that can happen sometimes! + graphics.transform._parentID = container.transform._worldID + 1; + + mockAddChild(container, graphics); + + const bounds = graphics.getBounds(); + + expect(bounds.x).to.be.equal(100); + expect(bounds.y).to.be.equal(200); + expect(bounds.width).to.be.equal(10); + expect(bounds.height).to.be.equal(10); + })); }); describe('removeChildAt', function () @@ -202,6 +273,44 @@ expect(spy).to.have.been.called; expect(spy).to.have.been.calledWith(0); }); + + it('should flag transform for recalculation', testRemoveChild(function (mockRemoveChild) + { + const container = new PIXI.Container(); + const child = new PIXI.Container(); + + container.addChild(child); + container.getBounds(); + + const childParentID = child.transform._parentID; + const boundsID = container._boundsID; + + mockRemoveChild(container, child); + + expect(childParentID).to.not.be.equals(child.transform._parentID); + expect(boundsID).to.not.be.equals(container._boundsID); + })); + + it('should recalculate removed child correctly', testRemoveChild(function (mockRemoveChild) + { + const parent = new PIXI.Container(); + const container = new PIXI.Container(); + const graphics = new PIXI.Graphics(); + + parent.addChild(container); + + graphics.drawRect(0, 0, 10, 10); + container.position.set(100, 200); + container.addChild(graphics); + graphics.getBounds(); + + mockRemoveChild(container, graphics); + + const bounds = graphics.getBounds(); + + expect(bounds.x).to.be.equal(0); + expect(bounds.y).to.be.equal(0); + })); }); describe('getChildIndex', function () diff --git a/test/core/Graphics.js b/test/core/Graphics.js index 98c7584..11e47a0 100644 --- a/test/core/Graphics.js +++ b/test/core/Graphics.js @@ -1,5 +1,8 @@ 'use strict'; +const MockPointer = require('../interaction/MockPointer'); +const withGL = require('../withGL'); + describe('PIXI.Graphics', function () { describe('constructor', function () @@ -218,4 +221,153 @@ expect(spy).to.have.been.calledOnce; }); }); + + describe('mask', function () + { + it('should trigger interaction callback when no mask present', function () + { + const stage = new PIXI.Container(); + const pointer = new MockPointer(stage); + const graphics = new PIXI.Graphics(); + const mask = new PIXI.Graphics(); + const spy = sinon.spy(); + + graphics.interactive = true; + graphics.beginFill(0xFF0000); + graphics.drawRect(0, 0, 50, 50); + graphics.on('click', spy); + stage.addChild(graphics); + mask.beginFill(); + mask.drawRect(0, 0, 50, 50); + graphics.mask = mask; + + pointer.click(10, 10); + + expect(spy).to.have.been.calledOnce; + }); + it('should trigger interaction callback when mask uses beginFill', function () + { + const stage = new PIXI.Container(); + const pointer = new MockPointer(stage); + const graphics = new PIXI.Graphics(); + const mask = new PIXI.Graphics(); + const spy = sinon.spy(); + + graphics.interactive = true; + graphics.beginFill(0xFF0000); + graphics.drawRect(0, 0, 50, 50); + graphics.on('click', spy); + stage.addChild(graphics); + mask.beginFill(); + mask.drawRect(0, 0, 50, 50); + graphics.mask = mask; + + pointer.click(10, 10); + + expect(spy).to.have.been.calledOnce; + }); + + it('should not trigger interaction callback when mask doesn\'t use beginFill', function () + { + const stage = new PIXI.Container(); + const pointer = new MockPointer(stage); + const graphics = new PIXI.Graphics(); + const mask = new PIXI.Graphics(); + const spy = sinon.spy(); + + graphics.interactive = true; + graphics.beginFill(0xFF0000); + graphics.drawRect(0, 0, 50, 50); + graphics.on('click', spy); + stage.addChild(graphics); + mask.drawRect(0, 0, 50, 50); + graphics.mask = mask; + + pointer.click(10, 10); + + expect(spy).to.have.not.been.called; + }); + + it('should trigger interaction callback when mask doesn\'t use beginFill but hitArea is defined', function () + { + const stage = new PIXI.Container(); + const pointer = new MockPointer(stage); + const graphics = new PIXI.Graphics(); + const mask = new PIXI.Graphics(); + const spy = sinon.spy(); + + graphics.interactive = true; + graphics.beginFill(0xFF0000); + graphics.hitArea = new PIXI.Rectangle(0, 0, 50, 50); + graphics.drawRect(0, 0, 50, 50); + graphics.on('click', spy); + stage.addChild(graphics); + mask.drawRect(0, 0, 50, 50); + graphics.mask = mask; + + pointer.click(10, 10); + + expect(spy).to.have.been.calledOnce; + }); + + it('should trigger interaction callback when mask is a sprite', function () + { + const stage = new PIXI.Container(); + const pointer = new MockPointer(stage); + const graphics = new PIXI.Graphics(); + const mask = new PIXI.Graphics(); + const spy = sinon.spy(); + + graphics.interactive = true; + graphics.beginFill(0xFF0000); + graphics.drawRect(0, 0, 50, 50); + graphics.on('click', spy); + stage.addChild(graphics); + mask.drawRect(0, 0, 50, 50); + graphics.mask = new PIXI.Sprite(mask.generateCanvasTexture()); + + pointer.click(10, 10); + + expect(spy).to.have.been.calledOnce; + }); + + it('should calculate tint, alpha and blendMode of fastRect correctly', withGL(function () + { + const renderer = new PIXI.WebGLRenderer(200, 200, {}); + + try + { + const graphics = new PIXI.Graphics(); + + graphics.beginFill(0x102030, 0.6); + graphics.drawRect(2, 3, 100, 100); + graphics.endFill(); + graphics.tint = 0x101010; + graphics.blendMode = 2; + graphics.alpha = 0.3; + + renderer.render(graphics); + + expect(graphics.isFastRect()).to.be.true; + + const sprite = graphics._spriteRect; + + expect(sprite).to.not.be.equals(null); + expect(sprite.worldAlpha).to.equals(0.18); + expect(sprite.blendMode).to.equals(2); + expect(sprite.tint).to.equals(0x010203); + + const bounds = sprite.getBounds(); + + expect(bounds.x).to.equals(2); + expect(bounds.y).to.equals(3); + expect(bounds.width).to.equals(100); + expect(bounds.height).to.equals(100); + } + finally + { + renderer.destroy(); + } + })); + }); }); diff --git a/test/core/Spritesheet.js b/test/core/Spritesheet.js new file mode 100644 index 0000000..283adf5 --- /dev/null +++ b/test/core/Spritesheet.js @@ -0,0 +1,89 @@ +'use strict'; + +const path = require('path'); + +describe('PIXI.Spritesheet', function () +{ + before(function () + { + this.resources = path.join(__dirname, 'resources'); + this.validate = function (spritesheet, done) + { + spritesheet.parse(function (textures) + { + const id = 'goldmine_10_5.png'; + + expect(Object.keys(textures).length).to.equal(1); + expect(Object.keys(spritesheet.textures).length).to.equal(1); + expect(textures[id]).to.be.an.instanceof(PIXI.Texture); + expect(textures[id].width).to.equal(spritesheet.data.frames[id].frame.w / spritesheet.resolution); + expect(textures[id].height).to.equal(spritesheet.data.frames[id].frame.h / spritesheet.resolution); + spritesheet.destroy(true); + expect(spritesheet.textures).to.be.null; + expect(spritesheet.baseTexture).to.be.null; + done(); + }); + }; + }); + + it('should exist on PIXI', function () + { + expect(PIXI.Spritesheet).to.be.a.function; + expect(PIXI.Spritesheet.BATCH_SIZE).to.be.a.number; + }); + + it('should create an instance', function () + { + const baseTexture = new PIXI.BaseTexture(); + const data = { + frames: {}, + meta: {}, + }; + const spritesheet = new PIXI.Spritesheet(baseTexture, data); + + expect(spritesheet.data).to.equal(data); + expect(spritesheet.baseTexture).to.equal(baseTexture); + expect(spritesheet.resolution).to.equal(1); + + spritesheet.destroy(true); + }); + + it('should create instance with scale resolution', function (done) + { + const data = require(path.resolve(this.resources, 'building1.json')); // eslint-disable-line global-require + const image = new Image(); + + image.src = path.join(this.resources, data.meta.image); + image.onload = () => + { + const baseTexture = new PIXI.BaseTexture(image, null, 1); + const spritesheet = new PIXI.Spritesheet(baseTexture, data); + + expect(data).to.be.an.object; + expect(data.meta.image).to.equal('building1.png'); + expect(spritesheet.resolution).to.equal(0.5); + + this.validate(spritesheet, done); + }; + }); + + it('should create instance with filename resolution', function (done) + { + const uri = path.resolve(this.resources, 'building1@2x.json'); + const data = require(uri); // eslint-disable-line global-require + const image = new Image(); + + image.src = path.join(this.resources, data.meta.image); + image.onload = () => + { + const baseTexture = new PIXI.BaseTexture(image, null, 1); + const spritesheet = new PIXI.Spritesheet(baseTexture, data, uri); + + expect(data).to.be.an.object; + expect(data.meta.image).to.equal('building1@2x.png'); + expect(spritesheet.resolution).to.equal(2); + + this.validate(spritesheet, done); + }; + }); +}); diff --git a/test/core/Texture.js b/test/core/Texture.js new file mode 100644 index 0000000..8c4ef98 --- /dev/null +++ b/test/core/Texture.js @@ -0,0 +1,19 @@ +'use strict'; + +describe('PIXI.Texture', function () +{ + it('should register Texture from Loader', function () + { + const URL = 'foo.png'; + const NAME = 'bar'; + const image = new Image(); + + const texture = PIXI.Texture.fromLoader(image, URL, NAME); + + expect(texture.baseTexture.imageUrl).to.equal('foo.png'); + expect(PIXI.utils.TextureCache[NAME]).to.equal(texture); + expect(PIXI.utils.BaseTextureCache[NAME]).to.equal(texture.baseTexture); + expect(PIXI.utils.TextureCache[URL]).to.equal(texture); + expect(PIXI.utils.BaseTextureCache[URL]).to.equal(texture.baseTexture); + }); +}); diff --git a/test/core/WebGLRenderer.js b/test/core/WebGLRenderer.js index 9bc3bc2..3d6cf98 100644 --- a/test/core/WebGLRenderer.js +++ b/test/core/WebGLRenderer.js @@ -1,21 +1,21 @@ 'use strict'; -function isWebGLSupported(fn) -{ - return PIXI.utils.isWebGLSupported() ? fn : function () {}; // eslint-disable-line no-empty-function -} +const withGL = require('../withGL'); -describe('PIXI.WebGLRenderer', isWebGLSupported(function () +describe('PIXI.WebGLRenderer', function () { - it('setting option legacy should disable VAOs and set minimum SPRITE_MAX_TEXTURES to 1', function () + it('setting option legacy should disable VAOs and SPRITE_MAX_TEXTURES', withGL(function () { const renderer = new PIXI.WebGLRenderer(1, 1, { legacy: true }); - - expect(PIXI.glCore.VertexArrayObject.FORCE_NATIVE).to.equal(true); - expect(renderer.plugins.sprite.MAX_TEXTURES).to.equal(1); - - renderer.destroy(); - }); -})); - + try + { + expect(PIXI.glCore.VertexArrayObject.FORCE_NATIVE).to.equal(true); + expect(renderer.plugins.sprite.MAX_TEXTURES).to.equal(1); + } + finally + { + renderer.destroy(); + } + })); +}); diff --git a/test/core/filters.js b/test/core/filters.js new file mode 100644 index 0000000..df459f6 --- /dev/null +++ b/test/core/filters.js @@ -0,0 +1,21 @@ +'use strict'; + +describe('PIXI.filters', function () +{ + it('should correctly form uniformData', function () + { + const sprite = new PIXI.Sprite(PIXI.Texture.EMPTY); + const displ = new PIXI.filters.DisplacementFilter(sprite); + + expect(!!displ.uniformData.scale).to.be.true; + expect(!!displ.uniformData.filterMatrix).to.be.true; + expect(!!displ.uniformData.mapSampler).to.be.true; + // it does have filterClamp, but it is handled by FilterManager + expect(!!displ.uniformData.filterClamp).to.be.false; + + const fxaa = new PIXI.filters.FXAAFilter(); + + // it does have filterArea, but it is handled by FilterManager + expect(!!fxaa.uniformData.filterArea).to.be.false; + }); +}); diff --git a/package.json b/package.json index 6a366ba..71ade15 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pixi.js", - "version": "4.3.4", + "version": "4.3.5", "description": "Pixi.js is a fast lightweight 2D library that works across all devices.", "author": "Mat Groves", "contributors": [ @@ -61,7 +61,7 @@ "ismobilejs": "^0.4.0", "object-assign": "^4.0.1", "pixi-gl-core": "^1.0.3", - "resource-loader": "^2.0.4" + "resource-loader": "^2.0.6" }, "devDependencies": { "babel-cli": "^6.18.0", @@ -70,9 +70,9 @@ "babel-preset-es2015": "^6.14.0", "babelify": "^7.3.0", "del": "^2.2.0", - "electron-prebuilt": "^1.3.2", + "electron": "^1.4.15", "eslint": "^3.5.0", - "floss": "^1.2.0", + "floss": "^2.0.1", "gh-pages": "^0.11.0", "jaguarjs-jsdoc": "^1.0.1", "js-md5": "^0.4.1", diff --git a/src/accessibility/AccessibilityManager.js b/src/accessibility/AccessibilityManager.js index df2716a..f16bcb1 100644 --- a/src/accessibility/AccessibilityManager.js +++ b/src/accessibility/AccessibilityManager.js @@ -3,7 +3,7 @@ import accessibleTarget from './accessibleTarget'; // add some extra variables to the container.. -Object.assign( +core.utils.mixins.delayMixin( core.DisplayObject.prototype, accessibleTarget ); diff --git a/src/core/Application.js b/src/core/Application.js index ea0f058..5340578 100644 --- a/src/core/Application.js +++ b/src/core/Application.js @@ -1,6 +1,6 @@ import { autoDetectRenderer } from './autoDetectRenderer'; import Container from './display/Container'; -import Ticker from './ticker/Ticker'; +import { shared, Ticker } from './ticker'; /** * Convenience class to create a new PIXI application. @@ -35,9 +35,9 @@ * @param {boolean} [noWebGL=false] - prevents selection of WebGL renderer, even if such is present * @param {boolean} [options.legacy=false] - If true Pixi will aim to ensure compatibility * with older / less advanced devices. If you experience unexplained flickering try setting this to true. - + * @param {boolean} [useSharedTicker=false] - `true` to use PIXI.ticker.shared, `false` to create new ticker. */ - constructor(width, height, options, noWebGL) + constructor(width, height, options, noWebGL, useSharedTicker = false) { /** * WebGL renderer if available, otherwise CanvasRenderer @@ -46,23 +46,46 @@ this.renderer = autoDetectRenderer(width, height, options, noWebGL); /** - * The root display container that's renderered. + * The root display container that's rendered. * @member {PIXI.Container} */ this.stage = new Container(); /** + * Internal reference to the ticker + * @member {PIXI.ticker.Ticker} + * @private + */ + this._ticker = null; + + /** * Ticker for doing render updates. * @member {PIXI.ticker.Ticker} + * @default PIXI.ticker.shared */ - this.ticker = new Ticker(); - - this.ticker.add(this.render, this); + this.ticker = useSharedTicker ? shared : new Ticker(); // Start the rendering this.start(); } + set ticker(ticker) // eslint-disable-line require-jsdoc + { + if (this._ticker) + { + this._ticker.remove(this.render, this); + } + this._ticker = ticker; + if (ticker) + { + ticker.add(this.render, this); + } + } + get ticker() // eslint-disable-line require-jsdoc + { + return this._ticker; + } + /** * Render the current stage. */ @@ -76,7 +99,7 @@ */ stop() { - this.ticker.stop(); + this._ticker.stop(); } /** @@ -84,7 +107,7 @@ */ start() { - this.ticker.start(); + this._ticker.start(); } /** @@ -98,13 +121,22 @@ } /** + * Reference to the renderer's screen rectangle. Its safe to use as filterArea or hitArea for whole screen + * @member {PIXI.Rectangle} + * @readonly + */ + get screen() + { + return this.renderer.screen; + } + + /** * Destroy and don't use after this. * @param {Boolean} [removeView=false] Automatically remove canvas from DOM. */ destroy(removeView) { this.stop(); - this.ticker.remove(this.render, this); this.ticker = null; this.stage.destroy(); diff --git a/src/core/Shader.js b/src/core/Shader.js new file mode 100644 index 0000000..78b17e6 --- /dev/null +++ b/src/core/Shader.js @@ -0,0 +1,46 @@ +import { GLShader } from 'pixi-gl-core'; +import settings from './settings'; + +function checkPrecision(src, def) +{ + if (src instanceof Array) + { + if (src[0].substring(0, 9) !== 'precision') + { + const copy = src.slice(0); + + copy.unshift(`precision ${def} float;`); + + return copy; + } + } + else if (src.substring(0, 9) !== 'precision') + { + return `precision ${def} float;\n${src}`; + } + + return src; +} + +/** + * Wrapper class, webGL Shader for Pixi. + * Adds precision string if vertexSrc or fragmentSrc have no mention of it. + * + * @class + * @extends GLShader + * @memberof PIXI + */ +export default class Shader extends GLShader +{ + /** + * + * @param {WebGLRenderingContext} gl - The current WebGL rendering context + * @param {string|string[]} vertexSrc - The vertex shader source as an array of strings. + * @param {string|string[]} fragmentSrc - The fragment shader source as an array of strings. + */ + constructor(gl, vertexSrc, fragmentSrc) + { + super(gl, checkPrecision(vertexSrc, settings.PRECISION_VERTEX), + checkPrecision(fragmentSrc, settings.PRECISION_FRAGMENT)); + } +} diff --git a/src/core/const.js b/src/core/const.js index ef6f662..3de0efe 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -188,7 +188,7 @@ * The gc modes that are supported by pixi. * * The {@link PIXI.settings.GC_MODE} Garbage Collection mode for pixi textures is AUTO - * If set to GC_MODE, the renderer will occasianally check textures usage. If they are not + * If set to GC_MODE, the renderer will occasionally check textures usage. If they are not * used for a specified period of time they will be removed from the GPU. They will of course * be uploaded again when they are required. This is a silent behind the scenes process that * should ensure that the GPU does not get filled up. diff --git a/src/core/display/Container.js b/src/core/display/Container.js index ed8cd5f..17fde8d 100644 --- a/src/core/display/Container.js +++ b/src/core/display/Container.js @@ -73,11 +73,12 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.push(child); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -108,11 +109,12 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.splice(index, 0, child); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -225,10 +227,11 @@ if (index === -1) return null; child.parent = null; + // ensure child transform will be recalculated + child.transform._parentID = -1; removeItems(this.children, index, 1); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -249,11 +252,12 @@ { const child = this.getChildAt(index); + // ensure child transform will be recalculated.. child.parent = null; + child.transform._parentID = -1; removeItems(this.children, index, 1); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -284,13 +288,12 @@ for (let i = 0; i < removed.length; ++i) { removed[i].parent = null; + if (removed[i].transform) + { + removed[i].transform._parentID = -1; + } } - // ensure a transform will be recalculated.. - if (this.transform) - { - this.transform._parentID = -1; - } this._boundsID++; this.onChildrenChange(beginIndex); diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index f37f2f5..be4c4fb 100644 --- a/src/core/graphics/Graphics.js +++ b/src/core/graphics/Graphics.js @@ -28,8 +28,9 @@ { /** * + * @param {boolean} [nativeLines=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP */ - constructor() + constructor(nativeLines = false) { super(); @@ -50,6 +51,13 @@ this.lineWidth = 0; /** + * If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * + * @member {boolean} + */ + this.nativeLines = nativeLines; + + /** * The color of any lines drawn. * * @member {string} @@ -757,28 +765,14 @@ if (!this._spriteRect) { - if (!Graphics._SPRITE_TEXTURE) - { - Graphics._SPRITE_TEXTURE = RenderTexture.create(10, 10); - - const canvas = document.createElement('canvas'); - - canvas.width = 10; - canvas.height = 10; - - const context = canvas.getContext('2d'); - - context.fillStyle = 'white'; - context.fillRect(0, 0, 10, 10); - - Graphics._SPRITE_TEXTURE = Texture.fromCanvas(canvas); - } - - this._spriteRect = new Sprite(Graphics._SPRITE_TEXTURE); + this._spriteRect = new Sprite(new Texture(Texture.WHITE)); } + + const sprite = this._spriteRect; + if (this.tint === 0xffffff) { - this._spriteRect.tint = this.graphicsData[0].fillColor; + sprite.tint = this.graphicsData[0].fillColor; } else { @@ -792,20 +786,21 @@ t1[1] *= t2[1]; t1[2] *= t2[2]; - this._spriteRect.tint = rgb2hex(t1); + sprite.tint = rgb2hex(t1); } - this._spriteRect.alpha = this.graphicsData[0].fillAlpha; - this._spriteRect.worldAlpha = this.worldAlpha * this._spriteRect.alpha; + sprite.alpha = this.graphicsData[0].fillAlpha; + sprite.worldAlpha = this.worldAlpha * sprite.alpha; + sprite.blendMode = this.blendMode; - Graphics._SPRITE_TEXTURE._frame.width = rect.width; - Graphics._SPRITE_TEXTURE._frame.height = rect.height; + sprite.texture._frame.width = rect.width; + sprite.texture._frame.height = rect.height; - this._spriteRect.transform.worldTransform = this.transform.worldTransform; + sprite.transform.worldTransform = this.transform.worldTransform; - this._spriteRect.anchor.set(-rect.x / rect.width, -rect.y / rect.height); - this._spriteRect._onAnchorUpdate(); + sprite.anchor.set(-rect.x / rect.width, -rect.y / rect.height); + sprite._onAnchorUpdate(); - this._spriteRect._renderWebGL(renderer); + sprite._renderWebGL(renderer); } /** @@ -1031,6 +1026,7 @@ this.fillColor, this.fillAlpha, this.filling, + this.nativeLines, shape ); diff --git a/src/core/graphics/GraphicsData.js b/src/core/graphics/GraphicsData.js index f5a87ae..b6f03c0 100644 --- a/src/core/graphics/GraphicsData.js +++ b/src/core/graphics/GraphicsData.js @@ -14,14 +14,19 @@ * @param {number} fillColor - the color of the fill * @param {number} fillAlpha - the alpha of the fill * @param {boolean} fill - whether or not the shape is filled with a colour + * @param {boolean} nativeLines - the method for drawing lines * @param {PIXI.Circle|PIXI.Rectangle|PIXI.Ellipse|PIXI.Polygon} shape - The shape object to draw. */ - constructor(lineWidth, lineColor, lineAlpha, fillColor, fillAlpha, fill, shape) + constructor(lineWidth, lineColor, lineAlpha, fillColor, fillAlpha, fill, nativeLines, shape) { /** * @member {number} the width of the line to draw */ this.lineWidth = lineWidth; + /** + * @member {boolean} if true the liens will be draw using LINES instead of TRIANGLE_STRIP + */ + this.nativeLines = nativeLines; /** * @member {number} the color of the line to draw @@ -85,6 +90,7 @@ this.fillColor, this.fillAlpha, this.fill, + this.nativeLines, this.shape ); } diff --git a/src/core/graphics/webgl/GraphicsRenderer.js b/src/core/graphics/webgl/GraphicsRenderer.js index 14083fe..67f5e92 100644 --- a/src/core/graphics/webgl/GraphicsRenderer.js +++ b/src/core/graphics/webgl/GraphicsRenderer.js @@ -102,7 +102,15 @@ shaderTemp.uniforms.alpha = graphics.worldAlpha; renderer.bindVao(webGLData.vao); - webGLData.vao.draw(gl.TRIANGLE_STRIP, webGLData.indices.length); + + if (graphics.nativeLines) + { + gl.drawArrays(gl.LINES, 0, webGLData.points.length / 6); + } + else + { + webGLData.vao.draw(gl.TRIANGLE_STRIP, webGLData.indices.length); + } } } diff --git a/src/core/graphics/webgl/utils/buildLine.js b/src/core/graphics/webgl/utils/buildLine.js index ecf43f7..4d6862e 100644 --- a/src/core/graphics/webgl/utils/buildLine.js +++ b/src/core/graphics/webgl/utils/buildLine.js @@ -11,7 +11,29 @@ * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties * @param {object} webGLData - an object containing all the webGL-specific information to create this shape */ -export default function buildLine(graphicsData, webGLData) +export default function (graphicsData, webGLData) +{ + if (graphicsData.nativeLines) + { + buildNativeLine(graphicsData, webGLData); + } + else + { + buildLine(graphicsData, webGLData); + } +} + +/** + * Builds a line to draw using the poligon method. + * + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @private + * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties + * @param {object} webGLData - an object containing all the webGL-specific information to create this shape + */ +function buildLine(graphicsData, webGLData) { // TODO OPTIMISE! let points = graphicsData.points; @@ -224,3 +246,46 @@ indices.push(indexStart - 1); } + +/** + * Builds a line to draw using the gl.drawArrays(gl.LINES) method + * + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @private + * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties + * @param {object} webGLData - an object containing all the webGL-specific information to create this shape + */ +function buildNativeLine(graphicsData, webGLData) +{ + let i = 0; + const points = graphicsData.points; + + if (points.length === 0) return; + + const verts = webGLData.points; + const length = points.length / 2; + + // sort color + const color = hex2rgb(graphicsData.lineColor); + const alpha = graphicsData.lineAlpha; + const r = color[0] * alpha; + const g = color[1] * alpha; + const b = color[2] * alpha; + + for (i = 1; i < length; i++) + { + const p1x = points[(i - 1) * 2]; + const p1y = points[((i - 1) * 2) + 1]; + + const p2x = points[i * 2]; + const p2y = points[(i * 2) + 1]; + + verts.push(p1x, p1y); + verts.push(r, g, b, alpha); + + verts.push(p2x, p2y); + verts.push(r, g, b, alpha); + } +} diff --git a/src/core/graphics/webgl/utils/buildRoundedRectangle.js b/src/core/graphics/webgl/utils/buildRoundedRectangle.js index 9abc807..a6c8461 100644 --- a/src/core/graphics/webgl/utils/buildRoundedRectangle.js +++ b/src/core/graphics/webgl/utils/buildRoundedRectangle.js @@ -77,6 +77,26 @@ } /** + * Calculate a single point for a quadratic bezier curve. + * Utility function used by quadraticBezierCurve. + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @private + * @param {number} n1 - first number + * @param {number} n2 - second number + * @param {number} perc - percentage + * @return {number} the result + * + */ +function getPt(n1, n2, perc) +{ + const diff = n2 - n1; + + return n1 + (diff * perc); +} + +/** * Calculate the points for a quadratic bezier curve. (helper function..) * Based on: https://stackoverflow.com/questions/785097/how-do-i-implement-a-bezier-curve-in-c * @@ -105,13 +125,6 @@ let x = 0; let y = 0; - function getPt(n1, n2, perc) - { - const diff = n2 - n1; - - return n1 + (diff * perc); - } - for (let i = 0, j = 0; i <= n; ++i) { j = i / n; diff --git a/src/core/index.js b/src/core/index.js index f7a817c..4f414e8 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -30,6 +30,7 @@ export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; export { default as CanvasGraphicsRenderer } from './graphics/canvas/CanvasGraphicsRenderer'; +export { default as Spritesheet } from './textures/Spritesheet'; export { default as Texture } from './textures/Texture'; export { default as BaseTexture } from './textures/BaseTexture'; export { default as RenderTexture } from './textures/RenderTexture'; diff --git a/src/core/math/Matrix.js b/src/core/math/Matrix.js index 0b77477..3679860 100644 --- a/src/core/math/Matrix.js +++ b/src/core/math/Matrix.js @@ -13,45 +13,50 @@ export default class Matrix { /** - * + * @param {number} [a=1] - x scale + * @param {number} [b=0] - y skew + * @param {number} [c=0] - x skew + * @param {number} [d=1] - y scale + * @param {number} [tx=0] - x translation + * @param {number} [ty=0] - y translation */ - constructor() + constructor(a = 1, b = 0, c = 0, d = 1, tx = 0, ty = 0) { /** * @member {number} * @default 1 */ - this.a = 1; + this.a = a; /** * @member {number} * @default 0 */ - this.b = 0; + this.b = b; /** * @member {number} * @default 0 */ - this.c = 0; + this.c = c; /** * @member {number} * @default 1 */ - this.d = 1; + this.d = d; /** * @member {number} * @default 0 */ - this.tx = 0; + this.tx = tx; /** * @member {number} * @default 0 */ - this.ty = 0; + this.ty = ty; this.array = null; } diff --git a/src/core/renderers/SystemRenderer.js b/src/core/renderers/SystemRenderer.js index fa116a6..a24c775 100644 --- a/src/core/renderers/SystemRenderer.js +++ b/src/core/renderers/SystemRenderer.js @@ -1,5 +1,5 @@ import { sayHello, hex2string, hex2rgb } from '../utils'; -import { Matrix } from '../math'; +import { Matrix, Rectangle } from '../math'; import { RENDERER_TYPE } from '../const'; import settings from '../settings'; import Container from '../display/Container'; @@ -21,8 +21,8 @@ { /** * @param {string} system - The name of the system this renderer is for. - * @param {number} [width=800] - the width of the canvas view - * @param {number} [height=600] - the height of the canvas view + * @param {number} [screenWidth=800] - the width of the screen + * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false @@ -37,7 +37,7 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(system, width, height, options) + constructor(system, screenWidth, screenHeight, options) { super(); @@ -69,20 +69,13 @@ this.type = RENDERER_TYPE.UNKNOWN; /** - * The width of the canvas view + * Measurements of the screen. (0, 0, screenWidth, screenHeight) * - * @member {number} - * @default 800 - */ - this.width = width || 800; - - /** - * The height of the canvas view + * Its safe to use as filterArea or hitArea for whole stage * - * @member {number} - * @default 600 + * @member {PIXI.Rectangle} */ - this.height = height || 600; + this.screen = new Rectangle(0, 0, screenWidth || 800, screenHeight || 600); /** * The canvas element that everything is drawn to @@ -107,7 +100,7 @@ this.transparent = options.transparent; /** - * Whether the render view should be resized automatically + * Whether css dimensions of canvas view should be resized to screen dimensions automatically * * @member {boolean} */ @@ -192,23 +185,48 @@ } /** - * Resizes the canvas view to the specified width and height + * Same as view.width, actual number of pixels in the canvas by horizontal * - * @param {number} width - the new width of the canvas view - * @param {number} height - the new height of the canvas view + * @member {number} + * @readonly + * @default 800 */ - resize(width, height) + get width() { - this.width = width * this.resolution; - this.height = height * this.resolution; + return this.view.width; + } - this.view.width = this.width; - this.view.height = this.height; + /** + * Same as view.height, actual number of pixels in the canvas by vertical + * + * @member {number} + * @readonly + * @default 600 + */ + get height() + { + return this.view.height; + } + + /** + * Resizes the screen and canvas to the specified width and height + * Canvas dimensions are multiplied by resolution + * + * @param {number} screenWidth - the new width of the screen + * @param {number} screenHeight - the new height of the screen + */ + resize(screenWidth, screenHeight) + { + this.screen.width = screenWidth; + this.screen.height = screenHeight; + + this.view.width = screenWidth * this.resolution; + this.view.height = screenHeight * this.resolution; if (this.autoResize) { - this.view.style.width = `${this.width / this.resolution}px`; - this.view.style.height = `${this.height / this.resolution}px`; + this.view.style.width = `${screenWidth}px`; + this.view.style.height = `${screenHeight}px`; } } @@ -249,11 +267,10 @@ this.type = RENDERER_TYPE.UNKNOWN; - this.width = 0; - this.height = 0; - this.view = null; + this.screen = null; + this.resolution = 0; this.transparent = false; diff --git a/src/core/renderers/canvas/CanvasRenderer.js b/src/core/renderers/canvas/CanvasRenderer.js index 1bd2d5e..e84e49c 100644 --- a/src/core/renderers/canvas/CanvasRenderer.js +++ b/src/core/renderers/canvas/CanvasRenderer.js @@ -18,8 +18,8 @@ export default class CanvasRenderer extends SystemRenderer { /** - * @param {number} [width=800] - the width of the canvas view - * @param {number} [height=600] - the height of the canvas view + * @param {number} [screenWidth=800] - the width of the screen + * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false @@ -34,9 +34,9 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(width, height, options = {}) + constructor(screenWidth, screenHeight, options = {}) { - super('Canvas', width, height, options); + super('Canvas', screenWidth, screenHeight, options); this.type = RENDERER_TYPE.CANVAS; @@ -96,7 +96,7 @@ this.context = null; this.renderingToScreen = false; - this.resize(width, height); + this.resize(screenWidth, screenHeight); } /** @@ -283,12 +283,12 @@ * * @extends PIXI.SystemRenderer#resize * - * @param {number} width - The new width of the canvas view - * @param {number} height - The new height of the canvas view + * @param {number} screenWidth - the new width of the screen + * @param {number} screenHeight - the new height of the screen */ - resize(width, height) + resize(screenWidth, screenHeight) { - super.resize(width, height); + super.resize(screenWidth, screenHeight); // reset the scale mode.. oddly this seems to be reset when the canvas is resized. // surely a browser bug?? Let pixi fix that for you.. diff --git a/src/core/renderers/webgl/TextureManager.js b/src/core/renderers/webgl/TextureManager.js index b685c1a..8227289 100644 --- a/src/core/renderers/webgl/TextureManager.js +++ b/src/core/renderers/webgl/TextureManager.js @@ -61,7 +61,7 @@ * Updates and/or Creates a WebGL texture for the renderer's context. * * @param {PIXI.BaseTexture|PIXI.Texture} texture - the texture to update - * @param {Number} location - the location the texture will be bound to. + * @param {number} location - the location the texture will be bound to. * @return {GLTexture} The gl texture. */ updateTexture(texture, location) diff --git a/src/core/renderers/webgl/WebGLRenderer.js b/src/core/renderers/webgl/WebGLRenderer.js index e4cd8a0..830b382 100644 --- a/src/core/renderers/webgl/WebGLRenderer.js +++ b/src/core/renderers/webgl/WebGLRenderer.js @@ -31,8 +31,8 @@ { /** * - * @param {number} [width=0] - the width of the canvas view - * @param {number} [height=0] - the height of the canvas view + * @param {number} [screenWidth=800] - the width of the screen + * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false @@ -53,9 +53,9 @@ * @param {boolean} [options.legacy=false] - If true Pixi will aim to ensure compatibility * with older / less advanced devices. If you experiance unexplained flickering try setting this to true. */ - constructor(width, height, options = {}) + constructor(screenWidth, screenHeight, options = {}) { - super('WebGL', width, height, options); + super('WebGL', screenWidth, screenHeight, options); this.legacy = !!options.legacy; @@ -245,7 +245,7 @@ glCore._testingContext = gl; // setup the width/height properties and gl viewport - this.resize(this.width, this.height); + this.resize(this.screen.width, this.screen.height); } /** @@ -343,16 +343,16 @@ /** * Resizes the webGL view to the specified width and height. * - * @param {number} width - the new width of the webGL view - * @param {number} height - the new height of the webGL view + * @param {number} screenWidth - the new width of the screen + * @param {number} screenHeight - the new height of the screen */ - resize(width, height) + resize(screenWidth, screenHeight) { // if(width * this.resolution === this.width && height * this.resolution === this.height)return; - SystemRenderer.prototype.resize.call(this, width, height); + SystemRenderer.prototype.resize.call(this, screenWidth, screenHeight); - this.rootRenderTarget.resize(width, height); + this.rootRenderTarget.resize(screenWidth, screenHeight); if (this._activeRenderTarget === this.rootRenderTarget) { @@ -396,6 +396,26 @@ } /** + * Erases the render texture and fills the drawing area with a colour + * + * @param {PIXI.RenderTexture} renderTexture - The render texture to clear + * @param {number} [clearColor] - The colour + * @return {PIXI.WebGLRenderer} Returns itself. + */ + clearRenderTexture(renderTexture, clearColor) + { + const baseTexture = renderTexture.baseTexture; + const renderTarget = baseTexture._glRenderTargets[this.CONTEXT_UID]; + + if (renderTarget) + { + renderTarget.clear(clearColor); + } + + return this; + } + + /** * Binds a render texture for rendering * * @param {PIXI.RenderTexture} renderTexture - The render texture to render @@ -474,9 +494,10 @@ * Changes the current GLShader to the one given in parameter * * @param {PIXI.glCore.Shader} shader - the new glShader + * @param {boolean} [autoProject=true] - Whether automatically set the projection matrix * @return {PIXI.WebGLRenderer} Returns itself. */ - _bindGLShader(shader) + _bindGLShader(shader, autoProject) { // TODO cache if (this._activeShader !== shader) @@ -484,8 +505,14 @@ this._activeShader = shader; shader.bind(); - // automatically set the projection matrix - shader.uniforms.projectionMatrix = this._activeRenderTarget.projectionMatrix.toArray(true); + // `autoProject` normally would be a default parameter set to true + // but because of how Babel transpiles default parameters + // it hinders the performance of this method. + if (autoProject !== false) + { + // automatically set the projection matrix + shader.uniforms.projectionMatrix = this._activeRenderTarget.projectionMatrix.toArray(true); + } } return this; diff --git a/src/core/renderers/webgl/WebGLState.js b/src/core/renderers/webgl/WebGLState.js index ba6ecc5..6c846d5 100755 --- a/src/core/renderers/webgl/WebGLState.js +++ b/src/core/renderers/webgl/WebGLState.js @@ -82,18 +82,20 @@ push() { // next state.. - let state = this.stack[++this.stackIndex]; + let state = this.stack[this.stackIndex]; if (!state) { state = this.stack[this.stackIndex] = new Uint8Array(16); } + ++this.stackIndex; + // copy state.. // set active state so we can force overrides of gl state for (let i = 0; i < this.activeState.length; i++) { - this.activeState[i] = state[i]; + state[i] = this.activeState[i]; } } diff --git a/src/core/renderers/webgl/filters/Filter.js b/src/core/renderers/webgl/filters/Filter.js index 2c3eaad..4eb6128 100644 --- a/src/core/renderers/webgl/filters/Filter.js +++ b/src/core/renderers/webgl/filters/Filter.js @@ -9,7 +9,7 @@ * @memberof PIXI * @extends PIXI.Shader */ -class Filter extends Shader +export default class Filter extends Shader { /** * @param {string} [vertexSrc] - The source of the vertex shader. @@ -56,15 +56,16 @@ * @param {PIXI.RenderTarget} input - The input render target. * @param {PIXI.RenderTarget} output - The target to output to. * @param {boolean} clear - Should the output be cleared before rendering to it + * @param {object} [currentState] - It's current state of filter. + * There are some useful properties in the currentState : + * target, filters, sourceFrame, destinationFrame, renderTarget, resolution */ - apply(filterManager, input, output, clear) + apply(filterManager, input, output, clear, currentState) // eslint-disable-line no-unused-vars { // do as you please! - filterManager.applyFilter(this, input, output, clear); + filterManager.applyFilter(this, input, output, clear, currentState); // or just do a regular render.. } } - -export default Filter; diff --git a/src/core/renderers/webgl/filters/extractUniformsFromSrc.js b/src/core/renderers/webgl/filters/extractUniformsFromSrc.js index d78a0b1..4893214 100644 --- a/src/core/renderers/webgl/filters/extractUniformsFromSrc.js +++ b/src/core/renderers/webgl/filters/extractUniformsFromSrc.js @@ -12,7 +12,7 @@ function extractUniformsFromString(string) { - const maskRegex = new RegExp('^(projectionMatrix|uSampler|filterArea)$'); + const maskRegex = new RegExp('^(projectionMatrix|uSampler|filterArea|filterClamp)$'); const uniforms = {}; let nameSplit; diff --git a/src/core/renderers/webgl/managers/FilterManager.js b/src/core/renderers/webgl/managers/FilterManager.js index 43fc869..6c7422d 100644 --- a/src/core/renderers/webgl/managers/FilterManager.js +++ b/src/core/renderers/webgl/managers/FilterManager.js @@ -2,6 +2,7 @@ import RenderTarget from '../utils/RenderTarget'; import Quad from '../utils/Quad'; import { Rectangle } from '../../../math'; +import Shader from '../../../Shader'; import * as filterTransforms from '../filters/filterTransforms'; import bitTwiddle from 'bit-twiddle'; @@ -22,8 +23,6 @@ this.filters = []; this.target = null; this.resolution = 1; - - this.firstRun = true; } } @@ -155,7 +154,7 @@ if (filters.length === 1) { - filters[0].apply(this, currentState.renderTarget, lastState.renderTarget, false); + filters[0].apply(this, currentState.renderTarget, lastState.renderTarget, false, currentState); this.freePotRenderTarget(currentState.renderTarget); } else @@ -177,7 +176,7 @@ for (i = 0; i < filters.length - 1; ++i) { - filters[i].apply(this, flip, flop, true); + filters[i].apply(this, flip, flop, true, currentState); const t = flip; @@ -185,7 +184,7 @@ flop = t; } - filters[i].apply(this, flip, lastState.renderTarget, false); + filters[i].apply(this, flip, lastState.renderTarget, false, currentState); this.freePotRenderTarget(flip); this.freePotRenderTarget(flop); @@ -212,16 +211,28 @@ const renderer = this.renderer; const gl = renderer.gl; - renderer.bindShader(filter, true); + let shader = filter.glShaders[renderer.CONTEXT_UID]; - // TODO theres a better way! - // quad can be a mesh and we then should be able to pull this off without accessing the shader directly - const shader = renderer.shaderManager.getGLShader(); - - // TODO change quad to mesh.. - if (!this.firstRun) + // cacheing.. + if (!shader) { - this.firstRun = true; + if (filter.glShaderKey) + { + shader = this.shaderCache[filter.glShaderKey]; + + if (!shader) + { + shader = new Shader(this.gl, filter.vertexSrc, filter.fragmentSrc); + + filter.glShaders[renderer.CONTEXT_UID] = this.shaderCache[filter.glShaderKey] = shader; + } + } + else + { + shader = filter.glShaders[renderer.CONTEXT_UID] = new Shader(this.gl, filter.vertexSrc, filter.fragmentSrc); + } + + // TODO - this only needs to be done once? renderer.bindVao(null); this.quad.initVao(shader); @@ -244,58 +255,146 @@ renderer.maskManager.pushScissorMask(null, renderer.maskManager.scissorData); } - // renderer._bindGLShader(shader); + renderer.bindShader(shader); - let currentState; - const uniforms = filter.uniforms; + // free unit 0 for us, doesn't matter what was there + // don't try to restore it, because syncUniforms can upload it to another slot + // and it'll be a problem + const tex = this.renderer.emptyTextures[0]; - if (uniforms.filterArea) - { - currentState = this.filterData.stack[this.filterData.index]; - - const filterArea = uniforms.filterArea; - - filterArea[0] = currentState.renderTarget.size.width; - filterArea[1] = currentState.renderTarget.size.height; - filterArea[2] = currentState.sourceFrame.x; - filterArea[3] = currentState.sourceFrame.y; - - filter.uniforms.filterArea = filterArea; - } - - // use this to clamp displaced texture coords so they belong to filterArea - // see displacementFilter fragment shader for an example - if (uniforms.filterClamp) - { - currentState = this.filterData.stack[this.filterData.index]; - - const filterClamp = uniforms.filterClamp; - - filterClamp[0] = 0; - filterClamp[1] = 0; - filterClamp[2] = (currentState.sourceFrame.width - 1) / currentState.renderTarget.size.width; - filterClamp[3] = (currentState.sourceFrame.height - 1) / currentState.renderTarget.size.height; - - filter.uniforms.filterClamp = filterClamp; - } - - renderer.shaderManager.setUniforms(filter.uniforms); + this.renderer.boundTextures[0] = tex; + // this syncs the pixi filters uniforms with glsl uniforms + this.syncUniforms(shader, filter); renderer.state.setBlendMode(filter.blendMode); - // temporary bypass cache.. - const tex = this.renderer.boundTextures[0]; - gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, input.texture.texture); this.quad.vao.draw(this.renderer.gl.TRIANGLES, 6, 0); - // restore cache. gl.bindTexture(gl.TEXTURE_2D, tex._glTextures[this.renderer.CONTEXT_UID].texture); } /** + * Uploads the uniforms of the filter. + * + * @param {GLShader} shader - The underlying gl shader. + * @param {PIXI.Filter} filter - The filter we are synchronizing. + */ + syncUniforms(shader, filter) + { + const uniformData = filter.uniformData; + const uniforms = filter.uniforms; + + // 0 is reserved for the pixi texture so we start at 1! + let textureCount = 1; + let currentState; + + // filterArea and filterClamp that are handled by FilterManager directly + // they must not appear in uniformData + + if (shader.uniforms.filterArea) + { + currentState = this.filterData.stack[this.filterData.index]; + + const filterArea = shader.uniforms.filterArea; + + filterArea[0] = currentState.renderTarget.size.width; + filterArea[1] = currentState.renderTarget.size.height; + filterArea[2] = currentState.sourceFrame.x; + filterArea[3] = currentState.sourceFrame.y; + + shader.uniforms.filterArea = filterArea; + } + + // use this to clamp displaced texture coords so they belong to filterArea + // see displacementFilter fragment shader for an example + if (shader.uniforms.filterClamp) + { + currentState = currentState || this.filterData.stack[this.filterData.index]; + + const filterClamp = shader.uniforms.filterClamp; + + filterClamp[0] = 0; + filterClamp[1] = 0; + filterClamp[2] = (currentState.sourceFrame.width - 1) / currentState.renderTarget.size.width; + filterClamp[3] = (currentState.sourceFrame.height - 1) / currentState.renderTarget.size.height; + + shader.uniforms.filterClamp = filterClamp; + } + + // TODO Cacheing layer.. + for (const i in uniformData) + { + if (uniformData[i].type === 'sampler2D' && uniforms[i] !== 0) + { + if (uniforms[i].baseTexture) + { + shader.uniforms[i] = this.renderer.bindTexture(uniforms[i].baseTexture, textureCount); + } + else + { + shader.uniforms[i] = textureCount; + + // TODO + // this is helpful as renderTargets can also be set. + // Although thinking about it, we could probably + // make the filter texture cache return a RenderTexture + // rather than a renderTarget + const gl = this.renderer.gl; + + this.renderer.boundTextures[textureCount] = this.renderer.emptyTextures[textureCount]; + gl.activeTexture(gl.TEXTURE0 + textureCount); + + uniforms[i].texture.bind(); + } + + textureCount++; + } + else if (uniformData[i].type === 'mat3') + { + // check if its pixi matrix.. + if (uniforms[i].a !== undefined) + { + shader.uniforms[i] = uniforms[i].toArray(true); + } + else + { + shader.uniforms[i] = uniforms[i]; + } + } + else if (uniformData[i].type === 'vec2') + { + // check if its a point.. + if (uniforms[i].x !== undefined) + { + const val = shader.uniforms[i] || new Float32Array(2); + + val[0] = uniforms[i].x; + val[1] = uniforms[i].y; + shader.uniforms[i] = val; + } + else + { + shader.uniforms[i] = uniforms[i]; + } + } + else if (uniformData[i].type === 'float') + { + if (shader.uniforms.data[i].value !== uniformData[i]) + { + shader.uniforms[i] = uniforms[i]; + } + } + else + { + shader.uniforms[i] = uniforms[i]; + } + } + } + + /** * Gets a render target from the pool, or creates a new one. * * @param {boolean} clear - Should we clear the render texture when we get it? @@ -442,7 +541,6 @@ renderTarget.resolution = resolution; renderTarget.defaultFrame.width = renderTarget.size.width = minWidth / resolution; renderTarget.defaultFrame.height = renderTarget.size.height = minHeight / resolution; - renderTarget.attachStencilBuffer(); return renderTarget; } diff --git a/src/core/settings.js b/src/core/settings.js index bb08cb0..f4dd087 100644 --- a/src/core/settings.js +++ b/src/core/settings.js @@ -77,11 +77,11 @@ * * @static * @memberof PIXI.settings - * @type {RegExp|string} + * @type {RegExp} * @example `@2x` - * @default /@(.+)x/ + * @default /@([0-9\.]+)x/ */ - RETINA_PREFIX: /@(.+)x/, + RETINA_PREFIX: /@([0-9\.]+)x/, /** * The default render options if none are supplied to {@link PIXI.WebGLRenderer} @@ -175,14 +175,24 @@ SCALE_MODE: 0, /** - * Default specify float precision in shaders. + * Default specify float precision in vertex shader. + * + * @static + * @memberof PIXI.settings + * @type {PIXI.PRECISION} + * @default PIXI.PRECISION.HIGH + */ + PRECISION_VERTEX: 'highp', + + /** + * Default specify float precision in fragment shader. * * @static * @memberof PIXI.settings * @type {PIXI.PRECISION} * @default PIXI.PRECISION.MEDIUM */ - PRECISION: 'mediump', + PRECISION_FRAGMENT: 'mediump', /** * Can we upload the same buffer in a single frame? diff --git a/src/core/sprites/Sprite.js b/src/core/sprites/Sprite.js index 28b0b18..121bf58 100644 --- a/src/core/sprites/Sprite.js +++ b/src/core/sprites/Sprite.js @@ -212,11 +212,11 @@ } else { - w0 = orig.width * (1 - anchor._x); - w1 = orig.width * -anchor._x; + w1 = -anchor._x * orig.width; + w0 = w1 + orig.width; - h0 = orig.height * (1 - anchor._y); - h1 = orig.height * -anchor._y; + h1 = -anchor._y * orig.height; + h0 = h1 + orig.height; } // xy @@ -269,11 +269,11 @@ const tx = wt.tx; const ty = wt.ty; - const w0 = (orig.width) * (1 - anchor._x); - const w1 = (orig.width) * -anchor._x; + const w1 = -anchor._x * orig.width; + const w0 = w1 + orig.width; - const h0 = orig.height * (1 - anchor._y); - const h1 = orig.height * -anchor._y; + const h1 = -anchor._y * orig.height; + const h0 = h1 + orig.height; // xy vertexData[0] = (a * w1) + (c * h1) + tx; @@ -440,7 +440,7 @@ * * @static * @param {number|string|PIXI.BaseTexture|HTMLCanvasElement|HTMLVideoElement} source Source to create texture from - * @return {PIXI.Sprite} The newly created texture + * @return {PIXI.Sprite} The newly created sprite */ static from(source) { @@ -538,8 +538,8 @@ } /** - * The tint applied to the sprite. This is a hex value. A value of - * 0xFFFFFF will remove any tint effect. + * The tint applied to the sprite. This is a hex value. + * A value of 0xFFFFFF will remove any tint effect. * * @member {number} * @default 0xFFFFFF diff --git a/src/core/sprites/canvas/CanvasTinter.js b/src/core/sprites/canvas/CanvasTinter.js index 1cf316b..27e5e0c 100644 --- a/src/core/sprites/canvas/CanvasTinter.js +++ b/src/core/sprites/canvas/CanvasTinter.js @@ -75,8 +75,8 @@ crop.width *= resolution; crop.height *= resolution; - canvas.width = crop.width; - canvas.height = crop.height; + canvas.width = Math.ceil(crop.width); + canvas.height = Math.ceil(crop.height); context.fillStyle = `#${(`00000${(color | 0).toString(16)}`).substr(-6)}`; @@ -130,8 +130,8 @@ crop.width *= resolution; crop.height *= resolution; - canvas.width = crop.width; - canvas.height = crop.height; + canvas.width = Math.ceil(crop.width); + canvas.height = Math.ceil(crop.height); context.globalCompositeOperation = 'copy'; context.fillStyle = `#${(`00000${(color | 0).toString(16)}`).substr(-6)}`; @@ -172,8 +172,8 @@ crop.width *= resolution; crop.height *= resolution; - canvas.width = crop.width; - canvas.height = crop.height; + canvas.width = Math.ceil(crop.width); + canvas.height = Math.ceil(crop.height); context.globalCompositeOperation = 'copy'; context.drawImage( diff --git a/src/core/sprites/webgl/SpriteRenderer.js b/src/core/sprites/webgl/SpriteRenderer.js index 2affcd5..7c352e2 100644 --- a/src/core/sprites/webgl/SpriteRenderer.js +++ b/src/core/sprites/webgl/SpriteRenderer.js @@ -30,7 +30,7 @@ /** * Number of values sent in the vertex buffer. - * positionX, positionY, colorR, colorG, colorB = 5 + * aVertexPosition(2), aTextureCoord(1), aColor(1), aTextureId(1) = 5 * * @member {number} */ @@ -75,7 +75,6 @@ this.shader = null; this.currentIndex = 0; - TICK = 0; this.groups = []; for (let k = 0; k < this.size; k++) diff --git a/src/core/textures/BaseTexture.js b/src/core/textures/BaseTexture.js index 853de7e..aeccf39 100644 --- a/src/core/textures/BaseTexture.js +++ b/src/core/textures/BaseTexture.js @@ -232,16 +232,24 @@ this.realWidth = this.source.naturalWidth || this.source.videoWidth || this.source.width; this.realHeight = this.source.naturalHeight || this.source.videoHeight || this.source.height; - this.width = this.realWidth / this.resolution; - this.height = this.realHeight / this.resolution; - - this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); + this._updateDimensions(); } this.emit('update', this); } /** + * Update dimensions from real values + */ + _updateDimensions() + { + this.width = this.realWidth / this.resolution; + this.height = this.realHeight / this.resolution; + + this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); + } + + /** * Load a source. * * If the source is not-immediately-available, such as an image that needs to be @@ -525,11 +533,7 @@ this.realWidth = Math.round(svgWidth * this.sourceScale); this.realHeight = Math.round(svgHeight * this.sourceScale); - this.width = this.realWidth / this.resolution; - this.height = this.realHeight / this.resolution; - - // Check pow2 after scale - this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); + this._updateDimensions(); // Create a canvas element const canvas = document.createElement('canvas'); @@ -690,4 +694,52 @@ return baseTexture; } + + /** + * Helper function that creates a base texture based on the source you provide. + * The source can be - image url, image element, canvas element. + * + * @static + * @param {string|HTMLImageElement|HTMLCanvasElement} source - The source to create base texture from. + * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values + * @param {number} [sourceScale=(auto)] - Scale for the original image, used with Svg images. + * @return {PIXI.BaseTexture} The new base texture. + */ + static from(source, scaleMode, sourceScale) + { + if (typeof source === 'string') + { + return BaseTexture.fromImage(source, undefined, scaleMode, sourceScale); + } + else if (source instanceof HTMLImageElement) + { + const imageUrl = source.src; + let baseTexture = BaseTextureCache[imageUrl]; + + if (!baseTexture) + { + baseTexture = new BaseTexture(source, scaleMode); + baseTexture.imageUrl = imageUrl; + + if (sourceScale) + { + baseTexture.sourceScale = sourceScale; + } + + // if there is an @2x at the end of the url we are going to assume its a highres image + baseTexture.resolution = getResolutionOfUrl(imageUrl); + + BaseTextureCache[imageUrl] = baseTexture; + } + + return baseTexture; + } + else if (source instanceof HTMLCanvasElement) + { + return BaseTexture.fromCanvas(source, scaleMode); + } + + // lets assume its a base texture! + return source; + } } diff --git a/src/core/textures/Spritesheet.js b/src/core/textures/Spritesheet.js new file mode 100644 index 0000000..d469c62 --- /dev/null +++ b/src/core/textures/Spritesheet.js @@ -0,0 +1,274 @@ +import { Rectangle, Texture } from '../'; +import { getResolutionOfUrl, TextureCache } from '../utils'; + +/** + * Utility class for maintaining reference to a collection + * of Textures on a single Spritesheet. + * + * @class + * @memberof PIXI + */ +export default class Spritesheet +{ + /** + * The maximum number of Textures to build per process. + * + * @type {number} + * @default 1000 + */ + static get BATCH_SIZE() + { + return 1000; + } + + /** + * @param {PIXI.BaseTexture} baseTexture Reference to the source BaseTexture object. + * @param {Object} data - Spritesheet image data. + * @param {string} [resolutionFilename] - The filename to consider when determining + * the resolution of the spritesheet. If not provided, the imageUrl will + * be used on the BaseTexture. + */ + constructor(baseTexture, data, resolutionFilename = null) + { + /** + * Reference to ths source texture + * @type {PIXI.BaseTexture} + */ + this.baseTexture = baseTexture; + + /** + * Map of spritesheet textures. + * @type {Object} + */ + this.textures = {}; + + /** + * Reference to the original JSON data. + * @type {Object} + */ + this.data = data; + + /** + * The resolution of the spritesheet. + * @type {number} + */ + this.resolution = this._updateResolution( + resolutionFilename || this.baseTexture.imageUrl + ); + + /** + * Map of spritesheet frames. + * @type {Object} + * @private + */ + this._frames = this.data.frames; + + /** + * Collection of frame names. + * @type {string[]} + * @private + */ + this._frameKeys = Object.keys(this._frames); + + /** + * Current batch index being processed. + * @type {number} + * @private + */ + this._batchIndex = 0; + + /** + * Callback when parse is completed. + * @type {Function} + * @private + */ + this._callback = null; + } + + /** + * Generate the resolution from the filename or fallback + * to the meta.scale field of the JSON data. + * + * @private + * @param {string} resolutionFilename - The filename to use for resolving + * the default resolution. + * @return {number} Resolution to use for spritesheet. + */ + _updateResolution(resolutionFilename) + { + const scale = this.data.meta.scale; + + // Use a defaultValue of `null` to check if a url-based resolution is set + let resolution = getResolutionOfUrl(resolutionFilename, null); + + // No resolution found via URL + if (resolution === null) + { + // Use the scale value or default to 1 + resolution = scale !== undefined ? parseFloat(scale) : 1; + } + + // For non-1 resolutions, update baseTexture + if (resolution !== 1) + { + this.baseTexture.resolution = resolution; + this.baseTexture.update(); + } + + return resolution; + } + + /** + * Parser spritesheet from loaded data. This is done asynchronously + * to prevent creating too many Texture within a single process. + * + * @param {Function} callback - Callback when complete returns + * a map of the Textures for this spritesheet. + */ + parse(callback) + { + this._batchIndex = 0; + this._callback = callback; + + if (this._frameKeys.length <= Spritesheet.BATCH_SIZE) + { + this._processFrames(0); + this._parseComplete(); + } + else + { + this._nextBatch(); + } + } + + /** + * Process a batch of frames + * + * @private + * @param {number} initialFrameIndex - The index of frame to start. + */ + _processFrames(initialFrameIndex) + { + let frameIndex = initialFrameIndex; + const maxFrames = Spritesheet.BATCH_SIZE; + + while (frameIndex - initialFrameIndex < maxFrames && frameIndex < this._frameKeys.length) + { + const i = this._frameKeys[frameIndex]; + const rect = this._frames[i].frame; + + if (rect) + { + let frame = null; + let trim = null; + const orig = new Rectangle( + 0, + 0, + this._frames[i].sourceSize.w / this.resolution, + this._frames[i].sourceSize.h / this.resolution + ); + + if (this._frames[i].rotated) + { + frame = new Rectangle( + rect.x / this.resolution, + rect.y / this.resolution, + rect.h / this.resolution, + rect.w / this.resolution + ); + } + else + { + frame = new Rectangle( + rect.x / this.resolution, + rect.y / this.resolution, + rect.w / this.resolution, + rect.h / this.resolution + ); + } + + // Check to see if the sprite is trimmed + if (this._frames[i].trimmed) + { + trim = new Rectangle( + this._frames[i].spriteSourceSize.x / this.resolution, + this._frames[i].spriteSourceSize.y / this.resolution, + rect.w / this.resolution, + rect.h / this.resolution + ); + } + + this.textures[i] = new Texture( + this.baseTexture, + frame, + orig, + trim, + this._frames[i].rotated ? 2 : 0 + ); + + // lets also add the frame to pixi's global cache for fromFrame and fromImage functions + TextureCache[i] = this.textures[i]; + } + + frameIndex++; + } + } + + /** + * The parse has completed. + * + * @private + */ + _parseComplete() + { + const callback = this._callback; + + this._callback = null; + this._batchIndex = 0; + callback.call(this, this.textures); + } + + /** + * Begin the next batch of textures. + * + * @private + */ + _nextBatch() + { + this._processFrames(this._batchIndex * Spritesheet.BATCH_SIZE); + this._batchIndex++; + setTimeout(() => + { + if (this._batchIndex * Spritesheet.BATCH_SIZE < this._frameKeys.length) + { + this._nextBatch(); + } + else + { + this._parseComplete(); + } + }, 0); + } + + /** + * Destroy Spritesheet and don't use after this. + * + * @param {boolean} [destroyBase=false] Whether to destroy the base texture as well + */ + destroy(destroyBase = false) + { + for (const i in this.textures) + { + this.textures[i].destroy(); + } + this._frames = null; + this._frameKeys = null; + this.data = null; + this.textures = null; + if (destroyBase) + { + this.baseTexture.destroy(); + } + this.baseTexture = null; + } +} diff --git a/src/core/textures/Texture.js b/src/core/textures/Texture.js index 6403d4f..afe4254 100644 --- a/src/core/textures/Texture.js +++ b/src/core/textures/Texture.js @@ -3,7 +3,7 @@ import TextureUvs from './TextureUvs'; import EventEmitter from 'eventemitter3'; import { Rectangle } from '../math'; -import { TextureCache, BaseTextureCache } from '../utils'; +import { TextureCache, BaseTextureCache, getResolutionOfUrl } from '../utils'; /** * A texture stores the information that represents an image or part of an image. It cannot be added @@ -365,7 +365,8 @@ * The source can be - frame id, image url, video url, canvas element, video element, base texture * * @static - * @param {number|string|PIXI.BaseTexture|HTMLCanvasElement|HTMLVideoElement} source - Source to create texture from + * @param {number|string|HTMLImageElement|HTMLCanvasElement|HTMLVideoElement|PIXI.BaseTexture} + * source - Source to create texture from * @return {PIXI.Texture} The newly created texture */ static from(source) @@ -393,7 +394,7 @@ } else if (source instanceof HTMLImageElement) { - return new Texture(new BaseTexture(source)); + return new Texture(BaseTexture.from(source)); } else if (source instanceof HTMLCanvasElement) { @@ -413,6 +414,43 @@ } /** + * Create a texture from a source and add to the cache. + * + * @static + * @param {HTMLImageElement|HTMLCanvasElement} source - The input source. + * @param {String} imageUrl - File name of texture, for cache and resolving resolution. + * @param {String} [name] - Human readible name for the texture cache. If no name is + * specified, only `imageUrl` will be used as the cache ID. + * @return {PIXI.Texture} Output texture + */ + static fromLoader(source, imageUrl, name) + { + const baseTexture = new BaseTexture(source, null, getResolutionOfUrl(imageUrl)); + const texture = new Texture(baseTexture); + + baseTexture.imageUrl = imageUrl; + + // No name, use imageUrl instead + if (!name) + { + name = imageUrl; + } + + // lets also add the frame to pixi's global cache for fromFrame and fromImage fucntions + BaseTextureCache[name] = baseTexture; + TextureCache[name] = texture; + + // also add references by url if they are different. + if (name !== imageUrl) + { + BaseTextureCache[imageUrl] = baseTexture; + TextureCache[imageUrl] = texture; + } + + return texture; + } + + /** * Adds a texture to the global TextureCache. This cache is shared across the whole PIXI object. * * @static @@ -522,6 +560,29 @@ } } +function createWhiteTexture() +{ + const canvas = document.createElement('canvas'); + + canvas.width = 10; + canvas.height = 10; + + const context = canvas.getContext('2d'); + + context.fillStyle = 'white'; + context.fillRect(0, 0, 10, 10); + + return new Texture(new BaseTexture(canvas)); +} + +function removeAllHandlers(tex) +{ + tex.destroy = function _emptyDestroy() { /* empty */ }; + tex.on = function _emptyOn() { /* empty */ }; + tex.once = function _emptyOnce() { /* empty */ }; + tex.emit = function _emptyEmit() { /* empty */ }; +} + /** * An empty texture, used often to not have to create multiple empty textures. * Can not be destroyed. @@ -530,7 +591,14 @@ * @constant */ Texture.EMPTY = new Texture(new BaseTexture()); -Texture.EMPTY.destroy = function _emptyDestroy() { /* empty */ }; -Texture.EMPTY.on = function _emptyOn() { /* empty */ }; -Texture.EMPTY.once = function _emptyOnce() { /* empty */ }; -Texture.EMPTY.emit = function _emptyEmit() { /* empty */ }; +removeAllHandlers(Texture.EMPTY); + +/** + * A white texture of 10x10 size, used for graphics and other things + * Can not be destroyed. + * + * @static + * @constant + */ +Texture.WHITE = createWhiteTexture(); +removeAllHandlers(Texture.WHITE); diff --git a/src/core/ticker/Ticker.js b/src/core/ticker/Ticker.js index d98a1f0..17a1517 100644 --- a/src/core/ticker/Ticker.js +++ b/src/core/ticker/Ticker.js @@ -67,11 +67,12 @@ * is based, this value is neither capped nor scaled. * If the platform supports DOMHighResTimeStamp, * this value will have a precision of 1 µs. + * Defaults to target frame time * * @member {number} - * @default 1 / TARGET_FPMS + * @default 16.66 */ - this.elapsedMS = 1 / settings.TARGET_FPMS; // default to target frame time + this.elapsedMS = 1 / settings.TARGET_FPMS; /** * The last time {@link PIXI.ticker.Ticker#update} was invoked. diff --git a/src/core/utils/index.js b/src/core/utils/index.js index 6ff609f..be44750 100644 --- a/src/core/utils/index.js +++ b/src/core/utils/index.js @@ -2,6 +2,7 @@ import settings from '../settings'; import EventEmitter from 'eventemitter3'; import pluginTarget from './pluginTarget'; +import * as mixins from './mixin'; import * as isMobile from 'ismobilejs'; let nextUid = 0; @@ -33,6 +34,7 @@ * @type {mixin} */ pluginTarget, + mixins, }; /** @@ -93,7 +95,7 @@ */ export function rgb2hex(rgb) { - return (((rgb[0] * 255) << 16) + ((rgb[1] * 255) << 8) + (rgb[2] * 255)); + return (((rgb[0] * 255) << 16) + ((rgb[1] * 255) << 8) + (rgb[2] * 255 | 0)); } /** @@ -368,3 +370,43 @@ * @private */ export const ProgramCache = {}; + +/** + * Destroys all texture in the cache + * + * @memberof PIXI.utils + * @function destroyTextureCache + */ +export function destroyTextureCache() +{ + let key; + + for (key in TextureCache) + { + TextureCache[key].destroy(); + } + for (key in BaseTextureCache) + { + BaseTextureCache[key].destroy(); + } +} + +/** + * Removes all textures from cache, but does not destroy them + * + * @memberof PIXI.utils + * @function clearTextureCache + */ +export function clearTextureCache() +{ + let key; + + for (key in TextureCache) + { + delete TextureCache[key]; + } + for (key in BaseTextureCache) + { + delete BaseTextureCache[key]; + } +} diff --git a/src/core/utils/mixin.js b/src/core/utils/mixin.js new file mode 100644 index 0000000..114de2a --- /dev/null +++ b/src/core/utils/mixin.js @@ -0,0 +1,59 @@ +/** + * Mixes all enumerable properties and methods from a source object to a target object. + * + * @memberof PIXI.utils.mixins + * @function mixin + * @param {object} target The prototype or instance that properties and methods should be added to. + * @param {object} source The source of properties and methods to mix in. + */ +export function mixin(target, source) +{ + if (!target || !source) return; + // in ES8/ES2017, this would be really easy: + // Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); + + // get all the enumerable property keys + const keys = Object.keys(source); + + // loop through properties + for (let i = 0; i < keys.length; ++i) + { + const propertyName = keys[i]; + + // Set the property using the property descriptor - this works for accessors and normal value properties + Object.defineProperty(target, propertyName, Object.getOwnPropertyDescriptor(source, propertyName)); + } +} + +const mixins = []; + +/** + * Queues a mixin to be handled towards the end of the initialization of PIXI, so that deprecation + * can take effect. + * + * @memberof PIXI.utils.mixins + * @function delayMixin + * @private + * @param {object} target The prototype or instance that properties and methods should be added to. + * @param {object} source The source of properties and methods to mix in. + */ +export function delayMixin(target, source) +{ + mixins.push(target, source); +} + +/** + * Handles all mixins queued via delayMixin(). + * + * @memberof PIXI.utils.mixins + * @function performMixins + * @private + */ +export function performMixins() +{ + for (let i = 0; i < mixins.length; i += 2) + { + mixin(mixins[i], mixins[i + 1]); + } + mixins.length = 0; +} diff --git a/src/deprecation.js b/src/deprecation.js index 045a19f..b9c73b3 100644 --- a/src/deprecation.js +++ b/src/deprecation.js @@ -5,6 +5,7 @@ import * as filters from './filters'; import * as prepare from './prepare'; import * as loaders from './loaders'; +import * as interaction from './interaction'; // provide method to give a stack track for warnings // useful for tracking-down where deprecated methods/properties/classes @@ -534,7 +535,7 @@ { parent: 'GC_MODES', target: 'GC_MODE' }, { parent: 'WRAP_MODES', target: 'WRAP_MODE' }, { parent: 'SCALE_MODES', target: 'SCALE_MODE' }, - { parent: 'PRECISION', target: 'PRECISION' }, + { parent: 'PRECISION', target: 'PRECISION_FRAGMENT' }, ]; for (let i = 0; i < defaults.length; i++) @@ -558,6 +559,32 @@ }); } +Object.defineProperties(core.settings, { + + /** + * @static + * @name PRECISION + * @memberof PIXI.settings + * @see PIXI.PRECISION + * @deprecated since version 4.4.0 + */ + PRECISION: { + enumerable: true, + get() + { + warn('PIXI.settings.PRECISION has been deprecated, please use PIXI.settings.PRECISION_FRAGMENT'); + + return core.settings.PRECISION_FRAGMENT; + }, + set(value) + { + warn('PIXI.settings.PRECISION has been deprecated, please use PIXI.settings.PRECISION_FRAGMENT'); + + core.settings.PRECISION_FRAGMENT = value; + }, + }, +}); + Object.defineProperties(extras, { /** @@ -694,7 +721,7 @@ warn('determineFontProperties is now deprecated, please use the static calculateFontProperties method, ' + 'e.g : Text.calculateFontProperties(fontStyle);'); - return Text.calculateFontProperties(fontStyle); + return core.Text.calculateFontProperties(fontStyle); }; Object.defineProperties(core.TextStyle.prototype, { @@ -1003,3 +1030,69 @@ }, }, }); + +/** + * @name PIXI.interaction.interactiveTarget#defaultCursor + * @static + * @type {number} + * @see PIXI.interaction.interactiveTarget#cursor + * @deprecated since 4.3.0 + */ +Object.defineProperty(interaction.interactiveTarget, 'defaultCursor', { + set(value) + { + warn('Property defaultCursor has been replaced with \'cursor\'. '); + this.cursor = value; + }, + get() + { + warn('Property defaultCursor has been replaced with \'cursor\'. '); + + return this.cursor; + }, + enumerable: true, +}); + +/** + * @name PIXI.interaction.InteractionManager#defaultCursorStyle + * @static + * @type {string} + * @see PIXI.interaction.InteractionManager#cursorStyles + * @deprecated since 4.3.0 + */ +Object.defineProperty(interaction.InteractionManager, 'defaultCursorStyle', { + set(value) + { + warn('Property defaultCursorStyle has been replaced with \'cursorStyles.default\'. '); + this.cursorStyles.default = value; + }, + get() + { + warn('Property defaultCursorStyle has been replaced with \'cursorStyles.default\'. '); + + return this.cursorStyles.default; + }, +}); + +/** + * @name PIXI.interaction.InteractionManager#currentCursorStyle + * @static + * @type {string} + * @see PIXI.interaction.InteractionManager#cursorStyles + * @deprecated since 4.3.0 + */ +Object.defineProperty(interaction.InteractionManager, 'currentCursorStyle', { + set(value) + { + warn('Property currentCursorStyle has been removed.' + + 'See the currentCursorMode property, which works differently.'); + this.currentCursorMode = value; + }, + get() + { + warn('Property currentCursorStyle has been removed.' + + 'See the currentCursorMode property, which works differently.'); + + return this.currentCursorMode; + }, +}); diff --git a/src/extras/BitmapText.js b/src/extras/BitmapText.js index fb6c343..0a0e150 100644 --- a/src/extras/BitmapText.js +++ b/src/extras/BitmapText.js @@ -455,6 +455,73 @@ return this._textHeight; } + + /** + * Register a bitmap font with data and a texture. + * + * @static + * @param {XMLDocument} xml - The XML document data. + * @param {PIXI.Texture} texture - Texture with all symbols. + * @return {Object} Result font object with font, size, lineHeight and char fields. + */ + static registerFont(xml, texture) + { + const data = {}; + const info = xml.getElementsByTagName('info')[0]; + const common = xml.getElementsByTagName('common')[0]; + + data.font = info.getAttribute('face'); + data.size = parseInt(info.getAttribute('size'), 10); + data.lineHeight = parseInt(common.getAttribute('lineHeight'), 10); + data.chars = {}; + + // parse letters + const letters = xml.getElementsByTagName('char'); + + for (let i = 0; i < letters.length; i++) + { + const letter = letters[i]; + const charCode = parseInt(letter.getAttribute('id'), 10); + + const textureRect = new core.Rectangle( + parseInt(letter.getAttribute('x'), 10) + texture.frame.x, + parseInt(letter.getAttribute('y'), 10) + texture.frame.y, + parseInt(letter.getAttribute('width'), 10), + parseInt(letter.getAttribute('height'), 10) + ); + + data.chars[charCode] = { + xOffset: parseInt(letter.getAttribute('xoffset'), 10), + yOffset: parseInt(letter.getAttribute('yoffset'), 10), + xAdvance: parseInt(letter.getAttribute('xadvance'), 10), + kerning: {}, + texture: new core.Texture(texture.baseTexture, textureRect), + + }; + } + + // parse kernings + const kernings = xml.getElementsByTagName('kerning'); + + for (let i = 0; i < kernings.length; i++) + { + const kerning = kernings[i]; + const first = parseInt(kerning.getAttribute('first'), 10); + const second = parseInt(kerning.getAttribute('second'), 10); + const amount = parseInt(kerning.getAttribute('amount'), 10); + + if (data.chars[second]) + { + data.chars[second].kerning[first] = amount; + } + } + + // I'm leaving this as a temporary fix so we can test the bitmap fonts in v3 + // but it's very likely to change + BitmapText.fonts[data.font] = data; + + return data; + } } BitmapText.fonts = {}; diff --git a/src/index.js b/src/index.js index 59356cf..c553427 100644 --- a/src/index.js +++ b/src/index.js @@ -16,6 +16,10 @@ import * as particles from './particles'; import * as prepare from './prepare'; +// handle mixins now, after all code has been added, including deprecation +import { utils } from './core'; +utils.mixins.performMixins(); + export { accessibility, extract, diff --git a/src/interaction/InteractionManager.js b/src/interaction/InteractionManager.js index 7d51134..8baf13a 100644 --- a/src/interaction/InteractionManager.js +++ b/src/interaction/InteractionManager.js @@ -1,18 +1,20 @@ 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'; -// Mix interactiveTarget into core.DisplayObject.prototype -Object.assign( +// Mix interactiveTarget into core.DisplayObject.prototype, after deprecation has been handled +core.utils.mixins.delayMixin( core.DisplayObject.prototype, 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 +70,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 +101,6 @@ this.eventData = new InteractionEvent(); /** - * Tiny little interactiveData pool ! - * - * @member {PIXI.interaction.InteractionData[]} - */ - this.interactiveDataPool = []; - - /** * The DOM element to bind to. * * @private @@ -153,66 +155,12 @@ */ this.supportsPointerEvents = !!window.PointerEvent; - /** - * Are touch events being 'normalized' and converted into pointer events if pointer events are not supported - * For example, on a touch screen mobile device, a touchstart would also be emitted as a pointerdown - * - * @private - * @readonly - * @member {boolean} - */ - this.normalizeTouchEvents = !this.supportsPointerEvents && this.supportsTouchEvents; - - /** - * Are mouse events being 'normalized' and converted into pointer events if pointer events are not supported - * For example, on a desktop pc, a mousedown would also be emitted as a pointerdown - * - * @private - * @readonly - * @member {boolean} - */ - this.normalizeMouseEvents = !this.supportsPointerEvents && !MobileDevice.any; - // this will make it so that you don't have to call bind all the time /** * @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 +168,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,41 +199,32 @@ this.onPointerOver = this.onPointerOver.bind(this); /** - * @private - * @member {Function} + * Dictionary of how different cursor modes are handled. Strings are handled as CSS cursor + * values, objects are handled as dictionaries of CSS values for interactionDOMElement, + * and functions are called instead of changing the CSS. + * Default CSS cursor values are provided for 'default' and 'pointer' modes. + * @member {Object.)>} */ - this.onTouchStart = this.onTouchStart.bind(this); - this.processTouchStart = this.processTouchStart.bind(this); + this.cursorStyles = { + default: 'inherit', + pointer: 'pointer', + }; /** - * @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. - * - * @member {string} - * @default 'inherit' - */ - this.defaultCursorStyle = 'inherit'; - - /** - * The css style of the cursor that is being used. + * The mode of the cursor that is being used. + * The value of this is a key from the cursorStyles dictionary. * * @member {string} */ - this.currentCursorStyle = 'inherit'; + this.currentCursorMode = null; + + /** + * Internal cached let. + * + * @private + * @member {string} + */ + this.cursor = null; /** * Internal cached let. @@ -303,6 +249,7 @@ * object. * * @event mousedown + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -311,6 +258,7 @@ * on the display object. * * @event rightdown + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -319,6 +267,7 @@ * object. * * @event mouseup + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -327,6 +276,7 @@ * over the display object. * * @event rightup + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -335,6 +285,7 @@ * the display object. * * @event click + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -343,6 +294,7 @@ * and released on the display object. * * @event rightclick + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -352,6 +304,7 @@ * [mousedown]{@link PIXI.interaction.InteractionManager#event:mousedown}. * * @event mouseupoutside + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -361,6 +314,7 @@ * [rightdown]{@link PIXI.interaction.InteractionManager#event:rightdown}. * * @event rightupoutside + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -368,6 +322,7 @@ * Fired when a pointer device (usually a mouse) is moved while over the display object * * @event mousemove + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -375,6 +330,7 @@ * Fired when a pointer device (usually a mouse) is moved onto the display object * * @event mouseover + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -382,6 +338,7 @@ * Fired when a pointer device (usually a mouse) is moved off the display object * * @event mouseout + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -389,6 +346,7 @@ * Fired when a pointer device button is pressed on the display object. * * @event pointerdown + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -396,6 +354,14 @@ * Fired when a pointer device button is released over the display object. * * @event pointerup + * @type {PIXI.interaction.InteractionData} + * @memberof PIXI.interaction.InteractionManager# + */ + + /** + * Fired when the operating system cancels a pointer event + * + * @event pointercancel * @memberof PIXI.interaction.InteractionManager# */ @@ -403,6 +369,7 @@ * Fired when a pointer device button is pressed and released on the display object. * * @event pointertap + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -411,6 +378,7 @@ * registered a [pointerdown]{@link PIXI.interaction.InteractionManager#event:pointerdown}. * * @event pointerupoutside + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -418,6 +386,7 @@ * Fired when a pointer device is moved while over the display object * * @event pointermove + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -425,6 +394,7 @@ * Fired when a pointer device is moved onto the display object * * @event pointerover + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -432,6 +402,7 @@ * Fired when a pointer device is moved off the display object * * @event pointerout + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -439,6 +410,7 @@ * Fired when a touch point is placed on the display object. * * @event touchstart + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -446,6 +418,14 @@ * Fired when a touch point is removed from the display object. * * @event touchend + * @type {PIXI.interaction.InteractionData} + * @memberof PIXI.interaction.InteractionManager# + */ + + /** + * Fired when the operating system cancels a touch + * + * @event touchcancel * @memberof PIXI.interaction.InteractionManager# */ @@ -453,6 +433,7 @@ * Fired when a touch point is placed and removed from the display object. * * @event tap + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -461,6 +442,7 @@ * registered a [touchstart]{@link PIXI.interaction.InteractionManager#event:touchstart}. * * @event touchendoutside + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -468,6 +450,7 @@ * Fired when a touch point is moved along the display object. * * @event touchmove + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ } @@ -524,45 +507,30 @@ { window.document.addEventListener('pointermove', this.onPointerMove, true); this.interactionDOMElement.addEventListener('pointerdown', this.onPointerDown, true); - this.interactionDOMElement.addEventListener('pointerout', this.onPointerOut, true); + // pointerout is fired in addition to pointerup (for touch events) and pointercancel + // we already handle those, so for the purposes of what we do in onPointerOut, we only + // care about the pointerleave event + this.interactionDOMElement.addEventListener('pointerleave', 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; @@ -596,45 +564,26 @@ { window.document.removeEventListener('pointermove', this.onPointerMove, true); this.interactionDOMElement.removeEventListener('pointerdown', this.onPointerDown, true); - this.interactionDOMElement.removeEventListener('pointerout', this.onPointerOut, true); + this.interactionDOMElement.removeEventListener('pointerleave', 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) + 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.onPointerDown, true); + this.interactionDOMElement.removeEventListener('touchcancel', this.onPointerCancel, 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); - - 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 = null; @@ -664,7 +613,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; @@ -672,25 +621,80 @@ return; } - this.cursor = this.defaultCursorStyle; + this.cursor = null; // 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(); - - this.processInteractive(this.mouse.global, this.renderer._lastObjectRendered, this.processMouseOverOut, true); - - if (this.currentCursorStyle !== this.cursor) + for (const k in this.activeInteractionData) { - this.currentCursorStyle = this.cursor; - this.interactionDOMElement.style.cursor = this.cursor; + // eslint-disable-next-line no-prototype-builtins + if (this.activeInteractionData.hasOwnProperty(k)) + { + const interactionData = this.activeInteractionData[k]; + + if (interactionData.originalEvent && interactionData.pointerType !== 'touch') + { + const interactionEvent = this.configureInteractionEventForDOMEvent( + this.eventData, + interactionData.originalEvent, + interactionData + ); + + this.processInteractive( + interactionEvent, + this.renderer._lastObjectRendered, + this.processPointerOverOut, + true + ); + } + } } + this.setCursorMode(this.cursor); + // TODO } /** + * Sets the current cursor mode, handling any callbacks or CSS style changes. + * + * @param {string} mode - cursor mode, a key from the cursorStyles dictionary + */ + setCursorMode(mode) + { + mode = mode || 'default'; + // if the mode didn't actually change, bail early + if (this.currentCursorMode === mode) + { + return; + } + this.currentCursorMode = mode; + const style = this.cursorStyles[mode]; + + // only do things if there is a cursor style for it + if (style) + { + switch (typeof style) + { + case 'string': + // string styles are handled as cursor CSS + this.interactionDOMElement.style.cursor = style; + break; + case 'function': + // functions are just called, and passed the cursor mode + style(mode); + break; + case 'object': + // if it is an object, assume that it is a dictionary of CSS styles, + // apply it to the interactionDOMElement + Object.assign(this.interactionDOMElement.style, style); + break; + } + } + } + + /** * Dispatches an event on the display object that was interacted with * * @param {PIXI.Container|PIXI.Sprite|PIXI.extras.TilingSprite} displayObject - the display object in question @@ -748,22 +752,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 @@ -787,9 +795,8 @@ { interactiveParent = false; } - - // it has a mask! Then lets hit test that before continuing.. - if (hitTest && displayObject._mask) + // it has a mask! Then lets hit test that before continuing + else if (hitTest && displayObject._mask) { if (!displayObject._mask.containsPoint(point)) { @@ -797,14 +804,7 @@ } } - // it has a filterArea! Same as mask but easier, its a rectangle - if (hitTest && displayObject.filterArea) - { - if (!displayObject.filterArea.contains(point.x, point.y)) - { - hitTest = false; - } - } + let keepHitTestingAfterChildren = hitTest; // ** FREE TIP **! If an object is not interactive or has no buttons in it // (such as a game scene!) set interactiveChildren to false for that displayObject. @@ -818,7 +818,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 @@ -838,15 +838,20 @@ // This means we no longer need to hit test anything else. We still need to run // through all objects, but we don't need to perform any hit tests. - // { - hitTest = false; - // } + keepHitTestingAfterChildren = false; + + if (child.interactive) + { + hitTest = false; + } // we can break now as we have hit an object. } } } + hitTest = keepHitTestingAfterChildren; + // no point running this if the item is not interactive or does not have an interactive parent. if (interactive) { @@ -868,14 +873,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 +886,175 @@ } /** - * 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].isNormalized) { - 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); + if (!displayObject.trackedPointers[id]) + { + displayObject.trackedPointers[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.trackedPointers[id].rightDown = true; + } + else + { + displayObject.trackedPointers[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.trackedPointers[id] !== undefined) + { + delete displayObject.trackedPointers[id]; + this.dispatchEvent(displayObject, 'pointercancel', interactionEvent); + + if (e.type === 'touchcancel' || e.pointerType === 'touch') + { + this.dispatchEvent(displayObject, 'touchcancel', interactionEvent); + } } } @@ -1159,42 +1066,94 @@ */ 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) { - if (hit) - { - this.dispatchEvent(displayObject, 'pointerup', this.eventData); + const e = interactionEvent.data.originalEvent; - if (displayObject._pointerDown) + const id = interactionEvent.data.identifier; + + const trackingData = displayObject.trackedPointers[id]; + + const isTouch = (e.type === 'touchend' || e.pointerType === 'touch'); + + const isMouse = (e.type.indexOf('mouse') === 0 || e.pointerType === 'mouse'); + + // 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) { - displayObject._pointerDown = false; - this.dispatchEvent(displayObject, 'pointertap', this.eventData); + 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); + } + // update the down state of the tracking data + if (trackingData) + { + if (isRightButton) + { + trackingData.rightDown = hit; + } + else + { + trackingData.leftDown = hit; + } } } - else if (displayObject._pointerDown) + + // Pointers and Touches, and Mouse + if (hit) { - displayObject._pointerDown = false; - this.dispatchEvent(displayObject, 'pointerupoutside', this.eventData); + this.dispatchEvent(displayObject, 'pointerup', interactionEvent); + if (isTouch) this.dispatchEvent(displayObject, 'touchend', interactionEvent); + + if (trackingData) + { + this.dispatchEvent(displayObject, 'pointertap', interactionEvent); + if (isTouch) + { + this.dispatchEvent(displayObject, 'tap', interactionEvent); + // touches are no longer over (if they ever were) when we get the touchend + // so we should ensure that we don't keep pretending that they are + trackingData.over = false; + } + } + } + else if (trackingData) + { + this.dispatchEvent(displayObject, 'pointerupoutside', interactionEvent); + if (isTouch) this.dispatchEvent(displayObject, 'touchendoutside', interactionEvent); + } + // Only remove the tracking data if there is no over/down state still associated with it + if (trackingData && trackingData.none) + { + delete displayObject.trackedPointers[id]; } } @@ -1202,39 +1161,78 @@ * 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 = null; + } - 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') + { + this.setCursorMode(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 (isMouse) { - 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 +1240,94 @@ * 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.setCursorMode(null); + } - 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'); + + let trackingData = displayObject.trackedPointers[id]; + + // if we just moused over the display object, then we need to track that state + if (hit && !trackingData) + { + trackingData = displayObject.trackedPointers[id] = new InteractionTrackingData(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); + } + } + + // only change the cursor if it has not already been changed (by something deeper in the + // display tree) + if (isMouse && this.cursor === null) + { + this.cursor = displayObject.cursor; } } - 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); + } + // if there is no mouse down information for the pointer, then it is safe to delete + if (trackingData.none) + { + delete displayObject.trackedPointers[id]; + } } } @@ -1287,253 +1335,154 @@ * 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 && pointerEvent.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; + + // mark the touch as normalized, just so that we know we did it + touch.isNormalized = true; + + normalizedEvents.push(touch); + } } - else if (this.normalizeMouseEvents) + // apparently PointerEvent subclasses MouseEvent, so yay + else if (event instanceof MouseEvent && (!this.supportsPointerEvents || !(event instanceof window.PointerEvent))) { if (typeof event.isPrimary === 'undefined') event.isPrimary = true; if (typeof event.width === 'undefined') event.width = 1; @@ -1541,10 +1490,21 @@ 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; + + // mark the mouse event as normalized, just so that we know we did it + event.isNormalized = true; + + normalizedEvents.push(event); } + else + { + normalizedEvents.push(event); + } + + return normalizedEvents; } /** @@ -1563,30 +1523,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 +1542,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..6a1f69e --- /dev/null +++ b/src/interaction/InteractionTrackingData.js @@ -0,0 +1,147 @@ +/** + * 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 inactive (not over or down)? + * + * @member {number} + * @memberof PIXI.interaction.InteractionTrackingData# + */ + get none() + { + return this._flags === this.constructor.FLAGS.NONE; + } + + /** + * 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..d252fba 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 * ); */ @@ -38,68 +38,56 @@ hitArea: null, /** - * If enabled, the mouse cursor will change when hovered over the displayObject if it is interactive + * If enabled, the mouse cursor use the pointer behavior when hovered over the displayObject if it is interactive + * Setting this changes the 'cursor' property to `'pointer'`. * - * @inner {boolean} + * @member {boolean} + * @memberof PIXI.interaction.interactiveTarget# */ - buttonMode: false, + get buttonMode() + { + return this.cursor === 'pointer'; + }, + set buttonMode(value) + { + if (value) + { + this.cursor = 'pointer'; + } + else if (this.cursor === 'pointer') + { + this.cursor = null; + } + }, /** - * If buttonMode is enabled, this defines what CSS cursor property is used when the mouse cursor - * is hovered over the displayObject + * This defines what cursor mode is used when the mouse cursor + * is hovered over the displayObject. * * @see https://developer.mozilla.org/en/docs/Web/CSS/cursor * * @inner {string} */ - defaultCursor: 'pointer', - - // some internal checks.. - /** - * Internal check to detect if the mouse cursor is hovered over the displayObject - * - * @inner {boolean} - * @private - */ - _over: false, + cursor: null, /** - * Internal check to detect if the left mouse button is pressed on the displayObject + * Internal set of all active pointers, by identifier * - * @inner {boolean} + * @member {Map} + * @memberof PIXI.interaction.interactiveTarget# * @private */ - _isLeftDown: false, + get trackedPointers() + { + if (this._trackedPointers === undefined) this._trackedPointers = {}; + + return this._trackedPointers; + }, /** - * Internal check to detect if the right mouse button is pressed on the displayObject + * Map of all tracked pointers, by identifier. Use trackedPointers to access. * - * @inner {boolean} - * @private + * @private {Map} */ - _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, + _trackedPointers: undefined, }; diff --git a/src/loaders/bitmapFontParser.js b/src/loaders/bitmapFontParser.js index cc2618c..f4cf996 100644 --- a/src/loaders/bitmapFontParser.js +++ b/src/loaders/bitmapFontParser.js @@ -1,63 +1,19 @@ import * as path from 'path'; -import { Rectangle, Texture, utils } from '../core'; +import { utils } from '../core'; import { Resource } from 'resource-loader'; import { BitmapText } from '../extras'; +/** + * Register a BitmapText font from loader resource. + * + * @function parseBitmapFontData + * @memberof PIXI.loaders + * @param {PIXI.loaders.Resource} resource - Loader resource. + * @param {PIXI.Texture} texture - Reference to texture. + */ export function parse(resource, texture) { - const data = {}; - const info = resource.data.getElementsByTagName('info')[0]; - const common = resource.data.getElementsByTagName('common')[0]; - - data.font = info.getAttribute('face'); - data.size = parseInt(info.getAttribute('size'), 10); - data.lineHeight = parseInt(common.getAttribute('lineHeight'), 10); - data.chars = {}; - - // parse letters - const letters = resource.data.getElementsByTagName('char'); - - for (let i = 0; i < letters.length; i++) - { - const charCode = parseInt(letters[i].getAttribute('id'), 10); - - const textureRect = new Rectangle( - parseInt(letters[i].getAttribute('x'), 10) + texture.frame.x, - parseInt(letters[i].getAttribute('y'), 10) + texture.frame.y, - parseInt(letters[i].getAttribute('width'), 10), - parseInt(letters[i].getAttribute('height'), 10) - ); - - data.chars[charCode] = { - xOffset: parseInt(letters[i].getAttribute('xoffset'), 10), - yOffset: parseInt(letters[i].getAttribute('yoffset'), 10), - xAdvance: parseInt(letters[i].getAttribute('xadvance'), 10), - kerning: {}, - texture: new Texture(texture.baseTexture, textureRect), - - }; - } - - // parse kernings - const kernings = resource.data.getElementsByTagName('kerning'); - - for (let i = 0; i < kernings.length; i++) - { - const first = parseInt(kernings[i].getAttribute('first'), 10); - const second = parseInt(kernings[i].getAttribute('second'), 10); - const amount = parseInt(kernings[i].getAttribute('amount'), 10); - - if (data.chars[second]) - { - data.chars[second].kerning[first] = amount; - } - } - - resource.bitmapFont = data; - - // I'm leaving this as a temporary fix so we can test the bitmap fonts in v3 - // but it's very likely to change - BitmapText.fonts[data.font] = data; + resource.bitmapFont = BitmapText.registerFont(resource.data, texture); } export default function () diff --git a/src/loaders/index.js b/src/loaders/index.js index 8942436..5189793 100644 --- a/src/loaders/index.js +++ b/src/loaders/index.js @@ -5,4 +5,11 @@ export { default as bitmapFontParser, parse as parseBitmapFontData } from './bitmapFontParser'; export { default as spritesheetParser } from './spritesheetParser'; export { default as textureParser } from './textureParser'; + +/** + * Reference to **resource-loader**'s Resource class. + * See https://github.com/englercj/resource-loader + * @class Resource + * @memberof PIXI.loaders + */ export { Resource } from 'resource-loader'; diff --git a/src/loaders/loader.js b/src/loaders/loader.js index 0a670cb..767a411 100644 --- a/src/loaders/loader.js +++ b/src/loaders/loader.js @@ -7,20 +7,47 @@ /** * - * The new loader, extends Resource Loader by Chad Engler : https://github.com/englercj/resource-loader + * The new loader, extends Resource Loader by Chad Engler: https://github.com/englercj/resource-loader * * ```js - * let loader = PIXI.loader; // pixi exposes a premade instance for you to use. + * const loader = PIXI.loader; // pixi exposes a premade instance for you to use. * //or - * let loader = new PIXI.loaders.Loader(); // you can also create your own if you want + * const loader = new PIXI.loaders.Loader(); // you can also create your own if you want * - * loader.add('bunny', 'data/bunny.png'); - * loader.add('spaceship', 'assets/spritesheet.json'); + * const sprites = {}; + * + * // Chainable `add` to enqueue a resource + * loader.add('bunny', 'data/bunny.png') + * .add('spaceship', 'assets/spritesheet.json'); * loader.add('scoreFont', 'assets/score.fnt'); * - * loader.once('complete',onAssetsLoaded); + * // Chainable `pre` to add a middleware that runs for each resource, *before* loading that resource. + * // This is useful to implement custom caching modules (using filesystem, indexeddb, memory, etc). + * loader.pre(cachingMiddleware); * - * loader.load(); + * // Chainable `use` to add a middleware that runs for each resource, *after* loading that resource. + * // This is useful to implement custom parsing modules (like spritesheet parsers, spine parser, etc). + * loader.use(parsingMiddleware); + * + * // The `load` method loads the queue of resources, and calls the passed in callback called once all + * // resources have loaded. + * loader.load((loader, resources) => { + * // resources is an object where the key is the name of the resource loaded and the value is the resource object. + * // They have a couple default properties: + * // - `url`: The URL that the resource was loaded from + * // - `error`: The error that happened when trying to load (if any) + * // - `data`: The raw data that was loaded + * // also may contain other properties based on the middleware that runs. + * sprites.bunny = new PIXI.TilingSprite(resources.bunny.texture); + * sprites.spaceship = new PIXI.TilingSprite(resources.spaceship.texture); + * sprites.scoreFont = new PIXI.TilingSprite(resources.scoreFont.texture); + * }); + * + * // throughout the process multiple signals can be dispatched. + * loader.onProgress.add(() => {}); // called once per loaded/errored file + * loader.onError.add(() => {}); // called once per errored file + * loader.onLoad.add(() => {}); // called once per loaded file + * loader.onComplete.add(() => {}); // called once when the queued resources all load. * ``` * * @see https://github.com/englercj/resource-loader diff --git a/src/loaders/spritesheetParser.js b/src/loaders/spritesheetParser.js index 7570332..e57dfb2 100644 --- a/src/loaders/spritesheetParser.js +++ b/src/loaders/spritesheetParser.js @@ -1,8 +1,6 @@ import { Resource } from 'resource-loader'; import path from 'path'; -import * as core from '../core'; - -const BATCH_SIZE = 1000; +import { Spritesheet } from '../core'; export default function () { @@ -43,134 +41,18 @@ // load the image for this sheet this.add(imageResourceName, resourcePath, loadOptions, function onImageLoad(res) { - resource.textures = {}; + const spritesheet = new Spritesheet( + res.texture.baseTexture, + resource.data, + resource.url + ); - const frames = resource.data.frames; - const frameKeys = Object.keys(frames); - const baseTexture = res.texture.baseTexture; - const scale = resource.data.meta.scale; - - // Use a defaultValue of `null` to check if a url-based resolution is set - let resolution = core.utils.getResolutionOfUrl(resource.url, null); - - // No resolution found via URL - if (resolution === null) + spritesheet.parse(() => { - // Use the scale value or default to 1 - resolution = scale !== undefined ? scale : 1; - } - - // For non-1 resolutions, update baseTexture - if (resolution !== 1) - { - baseTexture.resolution = resolution; - baseTexture.update(); - } - - let batchIndex = 0; - - function processFrames(initialFrameIndex, maxFrames) - { - let frameIndex = initialFrameIndex; - - while (frameIndex - initialFrameIndex < maxFrames && frameIndex < frameKeys.length) - { - const i = frameKeys[frameIndex]; - const rect = frames[i].frame; - - if (rect) - { - let frame = null; - let trim = null; - const orig = new core.Rectangle( - 0, - 0, - frames[i].sourceSize.w / resolution, - frames[i].sourceSize.h / resolution - ); - - if (frames[i].rotated) - { - frame = new core.Rectangle( - rect.x / resolution, - rect.y / resolution, - rect.h / resolution, - rect.w / resolution - ); - } - else - { - frame = new core.Rectangle( - rect.x / resolution, - rect.y / resolution, - rect.w / resolution, - rect.h / resolution - ); - } - - // Check to see if the sprite is trimmed - if (frames[i].trimmed) - { - trim = new core.Rectangle( - frames[i].spriteSourceSize.x / resolution, - frames[i].spriteSourceSize.y / resolution, - rect.w / resolution, - rect.h / resolution - ); - } - - resource.textures[i] = new core.Texture( - baseTexture, - frame, - orig, - trim, - frames[i].rotated ? 2 : 0 - ); - - // lets also add the frame to pixi's global cache for fromFrame and fromImage functions - core.utils.TextureCache[i] = resource.textures[i]; - } - - frameIndex++; - } - } - - function shouldProcessNextBatch() - { - return batchIndex * BATCH_SIZE < frameKeys.length; - } - - function processNextBatch(done) - { - processFrames(batchIndex * BATCH_SIZE, BATCH_SIZE); - batchIndex++; - setTimeout(done, 0); - } - - function iteration() - { - processNextBatch(() => - { - if (shouldProcessNextBatch()) - { - iteration(); - } - else - { - next(); - } - }); - } - - if (frameKeys.length <= BATCH_SIZE) - { - processFrames(0, BATCH_SIZE); + resource.spritesheet = spritesheet; + resource.textures = spritesheet.textures; next(); - } - else - { - iteration(); - } + }); }); }; } diff --git a/src/loaders/textureParser.js b/src/loaders/textureParser.js index 5398a7f..480edc1 100644 --- a/src/loaders/textureParser.js +++ b/src/loaders/textureParser.js @@ -1,5 +1,5 @@ -import * as core from '../core'; import { Resource } from 'resource-loader'; +import Texture from '../core/textures/Texture'; export default function () { @@ -8,23 +8,12 @@ // create a new texture if the data is an Image object if (resource.data && resource.type === Resource.TYPE.IMAGE) { - const baseTexture = new core.BaseTexture(resource.data, null, core.utils.getResolutionOfUrl(resource.url)); - - baseTexture.imageUrl = resource.url; - resource.texture = new core.Texture(baseTexture); - - // lets also add the frame to pixi's global cache for fromFrame and fromImage fucntions - core.utils.BaseTextureCache[resource.name] = baseTexture; - core.utils.TextureCache[resource.name] = resource.texture; - - // also add references by url if they are different. - if (resource.name !== resource.url) - { - core.utils.BaseTextureCache[resource.url] = baseTexture; - core.utils.TextureCache[resource.url] = resource.texture; - } + resource.texture = Texture.fromLoader( + resource.data, + resource.url, + resource.name + ); } - next(); }; } diff --git a/test/core/Container.js b/test/core/Container.js index 8a10b40..d11be33 100644 --- a/test/core/Container.js +++ b/test/core/Container.js @@ -1,5 +1,33 @@ 'use strict'; +function testAddChild(fn) +{ + fn(function (container, obj) + { + container.addChild(obj); + }); + fn(function (container, obj) + { + container.addChildAt(obj); + }); +} + +function testRemoveChild(fn) +{ + fn(function (container, obj) + { + container.removeChild(obj); + }); + fn(function (container, obj) + { + container.removeChildAt(container.children.indexOf(obj)); + }); + fn(function (container, obj) + { + container.removeChildren(container.children.indexOf(obj), container.children.indexOf(obj) + 1); + }); +} + describe('PIXI.Container', function () { describe('parent', function () @@ -70,6 +98,49 @@ expect(spy).to.have.been.called; expect(spy).to.have.been.calledWith(0); }); + + it('should flag child transform and container bounds for recalculation', testAddChild(function (mockAddChild) + { + const container = new PIXI.Container(); + const child = new PIXI.Container(); + + container.getBounds(); + child.getBounds(); + + const boundsID = container._boundsID; + const childParentID = child.transform._parentID; + + mockAddChild(container, child); + + expect(boundsID).to.not.be.equals(container._boundsID); + expect(childParentID).to.not.be.equals(child.transform._parentID); + })); + + it('should recalculate added child correctly', testAddChild(function (mockAddChild) + { + const parent = new PIXI.Container(); + const container = new PIXI.Container(); + const graphics = new PIXI.Graphics(); + + parent.addChild(container); + + graphics.drawRect(0, 0, 10, 10); + container.position.set(100, 200); + container.updateTransform(); + + graphics.getBounds(); + // Oops, that can happen sometimes! + graphics.transform._parentID = container.transform._worldID + 1; + + mockAddChild(container, graphics); + + const bounds = graphics.getBounds(); + + expect(bounds.x).to.be.equal(100); + expect(bounds.y).to.be.equal(200); + expect(bounds.width).to.be.equal(10); + expect(bounds.height).to.be.equal(10); + })); }); describe('removeChildAt', function () @@ -202,6 +273,44 @@ expect(spy).to.have.been.called; expect(spy).to.have.been.calledWith(0); }); + + it('should flag transform for recalculation', testRemoveChild(function (mockRemoveChild) + { + const container = new PIXI.Container(); + const child = new PIXI.Container(); + + container.addChild(child); + container.getBounds(); + + const childParentID = child.transform._parentID; + const boundsID = container._boundsID; + + mockRemoveChild(container, child); + + expect(childParentID).to.not.be.equals(child.transform._parentID); + expect(boundsID).to.not.be.equals(container._boundsID); + })); + + it('should recalculate removed child correctly', testRemoveChild(function (mockRemoveChild) + { + const parent = new PIXI.Container(); + const container = new PIXI.Container(); + const graphics = new PIXI.Graphics(); + + parent.addChild(container); + + graphics.drawRect(0, 0, 10, 10); + container.position.set(100, 200); + container.addChild(graphics); + graphics.getBounds(); + + mockRemoveChild(container, graphics); + + const bounds = graphics.getBounds(); + + expect(bounds.x).to.be.equal(0); + expect(bounds.y).to.be.equal(0); + })); }); describe('getChildIndex', function () diff --git a/test/core/Graphics.js b/test/core/Graphics.js index 98c7584..11e47a0 100644 --- a/test/core/Graphics.js +++ b/test/core/Graphics.js @@ -1,5 +1,8 @@ 'use strict'; +const MockPointer = require('../interaction/MockPointer'); +const withGL = require('../withGL'); + describe('PIXI.Graphics', function () { describe('constructor', function () @@ -218,4 +221,153 @@ expect(spy).to.have.been.calledOnce; }); }); + + describe('mask', function () + { + it('should trigger interaction callback when no mask present', function () + { + const stage = new PIXI.Container(); + const pointer = new MockPointer(stage); + const graphics = new PIXI.Graphics(); + const mask = new PIXI.Graphics(); + const spy = sinon.spy(); + + graphics.interactive = true; + graphics.beginFill(0xFF0000); + graphics.drawRect(0, 0, 50, 50); + graphics.on('click', spy); + stage.addChild(graphics); + mask.beginFill(); + mask.drawRect(0, 0, 50, 50); + graphics.mask = mask; + + pointer.click(10, 10); + + expect(spy).to.have.been.calledOnce; + }); + it('should trigger interaction callback when mask uses beginFill', function () + { + const stage = new PIXI.Container(); + const pointer = new MockPointer(stage); + const graphics = new PIXI.Graphics(); + const mask = new PIXI.Graphics(); + const spy = sinon.spy(); + + graphics.interactive = true; + graphics.beginFill(0xFF0000); + graphics.drawRect(0, 0, 50, 50); + graphics.on('click', spy); + stage.addChild(graphics); + mask.beginFill(); + mask.drawRect(0, 0, 50, 50); + graphics.mask = mask; + + pointer.click(10, 10); + + expect(spy).to.have.been.calledOnce; + }); + + it('should not trigger interaction callback when mask doesn\'t use beginFill', function () + { + const stage = new PIXI.Container(); + const pointer = new MockPointer(stage); + const graphics = new PIXI.Graphics(); + const mask = new PIXI.Graphics(); + const spy = sinon.spy(); + + graphics.interactive = true; + graphics.beginFill(0xFF0000); + graphics.drawRect(0, 0, 50, 50); + graphics.on('click', spy); + stage.addChild(graphics); + mask.drawRect(0, 0, 50, 50); + graphics.mask = mask; + + pointer.click(10, 10); + + expect(spy).to.have.not.been.called; + }); + + it('should trigger interaction callback when mask doesn\'t use beginFill but hitArea is defined', function () + { + const stage = new PIXI.Container(); + const pointer = new MockPointer(stage); + const graphics = new PIXI.Graphics(); + const mask = new PIXI.Graphics(); + const spy = sinon.spy(); + + graphics.interactive = true; + graphics.beginFill(0xFF0000); + graphics.hitArea = new PIXI.Rectangle(0, 0, 50, 50); + graphics.drawRect(0, 0, 50, 50); + graphics.on('click', spy); + stage.addChild(graphics); + mask.drawRect(0, 0, 50, 50); + graphics.mask = mask; + + pointer.click(10, 10); + + expect(spy).to.have.been.calledOnce; + }); + + it('should trigger interaction callback when mask is a sprite', function () + { + const stage = new PIXI.Container(); + const pointer = new MockPointer(stage); + const graphics = new PIXI.Graphics(); + const mask = new PIXI.Graphics(); + const spy = sinon.spy(); + + graphics.interactive = true; + graphics.beginFill(0xFF0000); + graphics.drawRect(0, 0, 50, 50); + graphics.on('click', spy); + stage.addChild(graphics); + mask.drawRect(0, 0, 50, 50); + graphics.mask = new PIXI.Sprite(mask.generateCanvasTexture()); + + pointer.click(10, 10); + + expect(spy).to.have.been.calledOnce; + }); + + it('should calculate tint, alpha and blendMode of fastRect correctly', withGL(function () + { + const renderer = new PIXI.WebGLRenderer(200, 200, {}); + + try + { + const graphics = new PIXI.Graphics(); + + graphics.beginFill(0x102030, 0.6); + graphics.drawRect(2, 3, 100, 100); + graphics.endFill(); + graphics.tint = 0x101010; + graphics.blendMode = 2; + graphics.alpha = 0.3; + + renderer.render(graphics); + + expect(graphics.isFastRect()).to.be.true; + + const sprite = graphics._spriteRect; + + expect(sprite).to.not.be.equals(null); + expect(sprite.worldAlpha).to.equals(0.18); + expect(sprite.blendMode).to.equals(2); + expect(sprite.tint).to.equals(0x010203); + + const bounds = sprite.getBounds(); + + expect(bounds.x).to.equals(2); + expect(bounds.y).to.equals(3); + expect(bounds.width).to.equals(100); + expect(bounds.height).to.equals(100); + } + finally + { + renderer.destroy(); + } + })); + }); }); diff --git a/test/core/Spritesheet.js b/test/core/Spritesheet.js new file mode 100644 index 0000000..283adf5 --- /dev/null +++ b/test/core/Spritesheet.js @@ -0,0 +1,89 @@ +'use strict'; + +const path = require('path'); + +describe('PIXI.Spritesheet', function () +{ + before(function () + { + this.resources = path.join(__dirname, 'resources'); + this.validate = function (spritesheet, done) + { + spritesheet.parse(function (textures) + { + const id = 'goldmine_10_5.png'; + + expect(Object.keys(textures).length).to.equal(1); + expect(Object.keys(spritesheet.textures).length).to.equal(1); + expect(textures[id]).to.be.an.instanceof(PIXI.Texture); + expect(textures[id].width).to.equal(spritesheet.data.frames[id].frame.w / spritesheet.resolution); + expect(textures[id].height).to.equal(spritesheet.data.frames[id].frame.h / spritesheet.resolution); + spritesheet.destroy(true); + expect(spritesheet.textures).to.be.null; + expect(spritesheet.baseTexture).to.be.null; + done(); + }); + }; + }); + + it('should exist on PIXI', function () + { + expect(PIXI.Spritesheet).to.be.a.function; + expect(PIXI.Spritesheet.BATCH_SIZE).to.be.a.number; + }); + + it('should create an instance', function () + { + const baseTexture = new PIXI.BaseTexture(); + const data = { + frames: {}, + meta: {}, + }; + const spritesheet = new PIXI.Spritesheet(baseTexture, data); + + expect(spritesheet.data).to.equal(data); + expect(spritesheet.baseTexture).to.equal(baseTexture); + expect(spritesheet.resolution).to.equal(1); + + spritesheet.destroy(true); + }); + + it('should create instance with scale resolution', function (done) + { + const data = require(path.resolve(this.resources, 'building1.json')); // eslint-disable-line global-require + const image = new Image(); + + image.src = path.join(this.resources, data.meta.image); + image.onload = () => + { + const baseTexture = new PIXI.BaseTexture(image, null, 1); + const spritesheet = new PIXI.Spritesheet(baseTexture, data); + + expect(data).to.be.an.object; + expect(data.meta.image).to.equal('building1.png'); + expect(spritesheet.resolution).to.equal(0.5); + + this.validate(spritesheet, done); + }; + }); + + it('should create instance with filename resolution', function (done) + { + const uri = path.resolve(this.resources, 'building1@2x.json'); + const data = require(uri); // eslint-disable-line global-require + const image = new Image(); + + image.src = path.join(this.resources, data.meta.image); + image.onload = () => + { + const baseTexture = new PIXI.BaseTexture(image, null, 1); + const spritesheet = new PIXI.Spritesheet(baseTexture, data, uri); + + expect(data).to.be.an.object; + expect(data.meta.image).to.equal('building1@2x.png'); + expect(spritesheet.resolution).to.equal(2); + + this.validate(spritesheet, done); + }; + }); +}); diff --git a/test/core/Texture.js b/test/core/Texture.js new file mode 100644 index 0000000..8c4ef98 --- /dev/null +++ b/test/core/Texture.js @@ -0,0 +1,19 @@ +'use strict'; + +describe('PIXI.Texture', function () +{ + it('should register Texture from Loader', function () + { + const URL = 'foo.png'; + const NAME = 'bar'; + const image = new Image(); + + const texture = PIXI.Texture.fromLoader(image, URL, NAME); + + expect(texture.baseTexture.imageUrl).to.equal('foo.png'); + expect(PIXI.utils.TextureCache[NAME]).to.equal(texture); + expect(PIXI.utils.BaseTextureCache[NAME]).to.equal(texture.baseTexture); + expect(PIXI.utils.TextureCache[URL]).to.equal(texture); + expect(PIXI.utils.BaseTextureCache[URL]).to.equal(texture.baseTexture); + }); +}); diff --git a/test/core/WebGLRenderer.js b/test/core/WebGLRenderer.js index 9bc3bc2..3d6cf98 100644 --- a/test/core/WebGLRenderer.js +++ b/test/core/WebGLRenderer.js @@ -1,21 +1,21 @@ 'use strict'; -function isWebGLSupported(fn) -{ - return PIXI.utils.isWebGLSupported() ? fn : function () {}; // eslint-disable-line no-empty-function -} +const withGL = require('../withGL'); -describe('PIXI.WebGLRenderer', isWebGLSupported(function () +describe('PIXI.WebGLRenderer', function () { - it('setting option legacy should disable VAOs and set minimum SPRITE_MAX_TEXTURES to 1', function () + it('setting option legacy should disable VAOs and SPRITE_MAX_TEXTURES', withGL(function () { const renderer = new PIXI.WebGLRenderer(1, 1, { legacy: true }); - - expect(PIXI.glCore.VertexArrayObject.FORCE_NATIVE).to.equal(true); - expect(renderer.plugins.sprite.MAX_TEXTURES).to.equal(1); - - renderer.destroy(); - }); -})); - + try + { + expect(PIXI.glCore.VertexArrayObject.FORCE_NATIVE).to.equal(true); + expect(renderer.plugins.sprite.MAX_TEXTURES).to.equal(1); + } + finally + { + renderer.destroy(); + } + })); +}); diff --git a/test/core/filters.js b/test/core/filters.js new file mode 100644 index 0000000..df459f6 --- /dev/null +++ b/test/core/filters.js @@ -0,0 +1,21 @@ +'use strict'; + +describe('PIXI.filters', function () +{ + it('should correctly form uniformData', function () + { + const sprite = new PIXI.Sprite(PIXI.Texture.EMPTY); + const displ = new PIXI.filters.DisplacementFilter(sprite); + + expect(!!displ.uniformData.scale).to.be.true; + expect(!!displ.uniformData.filterMatrix).to.be.true; + expect(!!displ.uniformData.mapSampler).to.be.true; + // it does have filterClamp, but it is handled by FilterManager + expect(!!displ.uniformData.filterClamp).to.be.false; + + const fxaa = new PIXI.filters.FXAAFilter(); + + // it does have filterArea, but it is handled by FilterManager + expect(!!fxaa.uniformData.filterArea).to.be.false; + }); +}); diff --git a/test/core/index.js b/test/core/index.js index 63a8f21..8530131 100755 --- a/test/core/index.js +++ b/test/core/index.js @@ -7,6 +7,7 @@ require('./DisplayObject'); require('./getLocalBounds'); require('./Sprite'); +require('./Spritesheet'); require('./TilingSprite'); require('./TextStyle'); require('./Text'); @@ -25,3 +26,5 @@ require('./SpriteRenderer'); require('./WebGLRenderer'); require('./Ellipse'); +require('./Texture'); +require('./filters'); diff --git a/package.json b/package.json index 6a366ba..71ade15 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pixi.js", - "version": "4.3.4", + "version": "4.3.5", "description": "Pixi.js is a fast lightweight 2D library that works across all devices.", "author": "Mat Groves", "contributors": [ @@ -61,7 +61,7 @@ "ismobilejs": "^0.4.0", "object-assign": "^4.0.1", "pixi-gl-core": "^1.0.3", - "resource-loader": "^2.0.4" + "resource-loader": "^2.0.6" }, "devDependencies": { "babel-cli": "^6.18.0", @@ -70,9 +70,9 @@ "babel-preset-es2015": "^6.14.0", "babelify": "^7.3.0", "del": "^2.2.0", - "electron-prebuilt": "^1.3.2", + "electron": "^1.4.15", "eslint": "^3.5.0", - "floss": "^1.2.0", + "floss": "^2.0.1", "gh-pages": "^0.11.0", "jaguarjs-jsdoc": "^1.0.1", "js-md5": "^0.4.1", diff --git a/src/accessibility/AccessibilityManager.js b/src/accessibility/AccessibilityManager.js index df2716a..f16bcb1 100644 --- a/src/accessibility/AccessibilityManager.js +++ b/src/accessibility/AccessibilityManager.js @@ -3,7 +3,7 @@ import accessibleTarget from './accessibleTarget'; // add some extra variables to the container.. -Object.assign( +core.utils.mixins.delayMixin( core.DisplayObject.prototype, accessibleTarget ); diff --git a/src/core/Application.js b/src/core/Application.js index ea0f058..5340578 100644 --- a/src/core/Application.js +++ b/src/core/Application.js @@ -1,6 +1,6 @@ import { autoDetectRenderer } from './autoDetectRenderer'; import Container from './display/Container'; -import Ticker from './ticker/Ticker'; +import { shared, Ticker } from './ticker'; /** * Convenience class to create a new PIXI application. @@ -35,9 +35,9 @@ * @param {boolean} [noWebGL=false] - prevents selection of WebGL renderer, even if such is present * @param {boolean} [options.legacy=false] - If true Pixi will aim to ensure compatibility * with older / less advanced devices. If you experience unexplained flickering try setting this to true. - + * @param {boolean} [useSharedTicker=false] - `true` to use PIXI.ticker.shared, `false` to create new ticker. */ - constructor(width, height, options, noWebGL) + constructor(width, height, options, noWebGL, useSharedTicker = false) { /** * WebGL renderer if available, otherwise CanvasRenderer @@ -46,23 +46,46 @@ this.renderer = autoDetectRenderer(width, height, options, noWebGL); /** - * The root display container that's renderered. + * The root display container that's rendered. * @member {PIXI.Container} */ this.stage = new Container(); /** + * Internal reference to the ticker + * @member {PIXI.ticker.Ticker} + * @private + */ + this._ticker = null; + + /** * Ticker for doing render updates. * @member {PIXI.ticker.Ticker} + * @default PIXI.ticker.shared */ - this.ticker = new Ticker(); - - this.ticker.add(this.render, this); + this.ticker = useSharedTicker ? shared : new Ticker(); // Start the rendering this.start(); } + set ticker(ticker) // eslint-disable-line require-jsdoc + { + if (this._ticker) + { + this._ticker.remove(this.render, this); + } + this._ticker = ticker; + if (ticker) + { + ticker.add(this.render, this); + } + } + get ticker() // eslint-disable-line require-jsdoc + { + return this._ticker; + } + /** * Render the current stage. */ @@ -76,7 +99,7 @@ */ stop() { - this.ticker.stop(); + this._ticker.stop(); } /** @@ -84,7 +107,7 @@ */ start() { - this.ticker.start(); + this._ticker.start(); } /** @@ -98,13 +121,22 @@ } /** + * Reference to the renderer's screen rectangle. Its safe to use as filterArea or hitArea for whole screen + * @member {PIXI.Rectangle} + * @readonly + */ + get screen() + { + return this.renderer.screen; + } + + /** * Destroy and don't use after this. * @param {Boolean} [removeView=false] Automatically remove canvas from DOM. */ destroy(removeView) { this.stop(); - this.ticker.remove(this.render, this); this.ticker = null; this.stage.destroy(); diff --git a/src/core/Shader.js b/src/core/Shader.js new file mode 100644 index 0000000..78b17e6 --- /dev/null +++ b/src/core/Shader.js @@ -0,0 +1,46 @@ +import { GLShader } from 'pixi-gl-core'; +import settings from './settings'; + +function checkPrecision(src, def) +{ + if (src instanceof Array) + { + if (src[0].substring(0, 9) !== 'precision') + { + const copy = src.slice(0); + + copy.unshift(`precision ${def} float;`); + + return copy; + } + } + else if (src.substring(0, 9) !== 'precision') + { + return `precision ${def} float;\n${src}`; + } + + return src; +} + +/** + * Wrapper class, webGL Shader for Pixi. + * Adds precision string if vertexSrc or fragmentSrc have no mention of it. + * + * @class + * @extends GLShader + * @memberof PIXI + */ +export default class Shader extends GLShader +{ + /** + * + * @param {WebGLRenderingContext} gl - The current WebGL rendering context + * @param {string|string[]} vertexSrc - The vertex shader source as an array of strings. + * @param {string|string[]} fragmentSrc - The fragment shader source as an array of strings. + */ + constructor(gl, vertexSrc, fragmentSrc) + { + super(gl, checkPrecision(vertexSrc, settings.PRECISION_VERTEX), + checkPrecision(fragmentSrc, settings.PRECISION_FRAGMENT)); + } +} diff --git a/src/core/const.js b/src/core/const.js index ef6f662..3de0efe 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -188,7 +188,7 @@ * The gc modes that are supported by pixi. * * The {@link PIXI.settings.GC_MODE} Garbage Collection mode for pixi textures is AUTO - * If set to GC_MODE, the renderer will occasianally check textures usage. If they are not + * If set to GC_MODE, the renderer will occasionally check textures usage. If they are not * used for a specified period of time they will be removed from the GPU. They will of course * be uploaded again when they are required. This is a silent behind the scenes process that * should ensure that the GPU does not get filled up. diff --git a/src/core/display/Container.js b/src/core/display/Container.js index ed8cd5f..17fde8d 100644 --- a/src/core/display/Container.js +++ b/src/core/display/Container.js @@ -73,11 +73,12 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.push(child); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -108,11 +109,12 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.splice(index, 0, child); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -225,10 +227,11 @@ if (index === -1) return null; child.parent = null; + // ensure child transform will be recalculated + child.transform._parentID = -1; removeItems(this.children, index, 1); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -249,11 +252,12 @@ { const child = this.getChildAt(index); + // ensure child transform will be recalculated.. child.parent = null; + child.transform._parentID = -1; removeItems(this.children, index, 1); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -284,13 +288,12 @@ for (let i = 0; i < removed.length; ++i) { removed[i].parent = null; + if (removed[i].transform) + { + removed[i].transform._parentID = -1; + } } - // ensure a transform will be recalculated.. - if (this.transform) - { - this.transform._parentID = -1; - } this._boundsID++; this.onChildrenChange(beginIndex); diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index f37f2f5..be4c4fb 100644 --- a/src/core/graphics/Graphics.js +++ b/src/core/graphics/Graphics.js @@ -28,8 +28,9 @@ { /** * + * @param {boolean} [nativeLines=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP */ - constructor() + constructor(nativeLines = false) { super(); @@ -50,6 +51,13 @@ this.lineWidth = 0; /** + * If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * + * @member {boolean} + */ + this.nativeLines = nativeLines; + + /** * The color of any lines drawn. * * @member {string} @@ -757,28 +765,14 @@ if (!this._spriteRect) { - if (!Graphics._SPRITE_TEXTURE) - { - Graphics._SPRITE_TEXTURE = RenderTexture.create(10, 10); - - const canvas = document.createElement('canvas'); - - canvas.width = 10; - canvas.height = 10; - - const context = canvas.getContext('2d'); - - context.fillStyle = 'white'; - context.fillRect(0, 0, 10, 10); - - Graphics._SPRITE_TEXTURE = Texture.fromCanvas(canvas); - } - - this._spriteRect = new Sprite(Graphics._SPRITE_TEXTURE); + this._spriteRect = new Sprite(new Texture(Texture.WHITE)); } + + const sprite = this._spriteRect; + if (this.tint === 0xffffff) { - this._spriteRect.tint = this.graphicsData[0].fillColor; + sprite.tint = this.graphicsData[0].fillColor; } else { @@ -792,20 +786,21 @@ t1[1] *= t2[1]; t1[2] *= t2[2]; - this._spriteRect.tint = rgb2hex(t1); + sprite.tint = rgb2hex(t1); } - this._spriteRect.alpha = this.graphicsData[0].fillAlpha; - this._spriteRect.worldAlpha = this.worldAlpha * this._spriteRect.alpha; + sprite.alpha = this.graphicsData[0].fillAlpha; + sprite.worldAlpha = this.worldAlpha * sprite.alpha; + sprite.blendMode = this.blendMode; - Graphics._SPRITE_TEXTURE._frame.width = rect.width; - Graphics._SPRITE_TEXTURE._frame.height = rect.height; + sprite.texture._frame.width = rect.width; + sprite.texture._frame.height = rect.height; - this._spriteRect.transform.worldTransform = this.transform.worldTransform; + sprite.transform.worldTransform = this.transform.worldTransform; - this._spriteRect.anchor.set(-rect.x / rect.width, -rect.y / rect.height); - this._spriteRect._onAnchorUpdate(); + sprite.anchor.set(-rect.x / rect.width, -rect.y / rect.height); + sprite._onAnchorUpdate(); - this._spriteRect._renderWebGL(renderer); + sprite._renderWebGL(renderer); } /** @@ -1031,6 +1026,7 @@ this.fillColor, this.fillAlpha, this.filling, + this.nativeLines, shape ); diff --git a/src/core/graphics/GraphicsData.js b/src/core/graphics/GraphicsData.js index f5a87ae..b6f03c0 100644 --- a/src/core/graphics/GraphicsData.js +++ b/src/core/graphics/GraphicsData.js @@ -14,14 +14,19 @@ * @param {number} fillColor - the color of the fill * @param {number} fillAlpha - the alpha of the fill * @param {boolean} fill - whether or not the shape is filled with a colour + * @param {boolean} nativeLines - the method for drawing lines * @param {PIXI.Circle|PIXI.Rectangle|PIXI.Ellipse|PIXI.Polygon} shape - The shape object to draw. */ - constructor(lineWidth, lineColor, lineAlpha, fillColor, fillAlpha, fill, shape) + constructor(lineWidth, lineColor, lineAlpha, fillColor, fillAlpha, fill, nativeLines, shape) { /** * @member {number} the width of the line to draw */ this.lineWidth = lineWidth; + /** + * @member {boolean} if true the liens will be draw using LINES instead of TRIANGLE_STRIP + */ + this.nativeLines = nativeLines; /** * @member {number} the color of the line to draw @@ -85,6 +90,7 @@ this.fillColor, this.fillAlpha, this.fill, + this.nativeLines, this.shape ); } diff --git a/src/core/graphics/webgl/GraphicsRenderer.js b/src/core/graphics/webgl/GraphicsRenderer.js index 14083fe..67f5e92 100644 --- a/src/core/graphics/webgl/GraphicsRenderer.js +++ b/src/core/graphics/webgl/GraphicsRenderer.js @@ -102,7 +102,15 @@ shaderTemp.uniforms.alpha = graphics.worldAlpha; renderer.bindVao(webGLData.vao); - webGLData.vao.draw(gl.TRIANGLE_STRIP, webGLData.indices.length); + + if (graphics.nativeLines) + { + gl.drawArrays(gl.LINES, 0, webGLData.points.length / 6); + } + else + { + webGLData.vao.draw(gl.TRIANGLE_STRIP, webGLData.indices.length); + } } } diff --git a/src/core/graphics/webgl/utils/buildLine.js b/src/core/graphics/webgl/utils/buildLine.js index ecf43f7..4d6862e 100644 --- a/src/core/graphics/webgl/utils/buildLine.js +++ b/src/core/graphics/webgl/utils/buildLine.js @@ -11,7 +11,29 @@ * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties * @param {object} webGLData - an object containing all the webGL-specific information to create this shape */ -export default function buildLine(graphicsData, webGLData) +export default function (graphicsData, webGLData) +{ + if (graphicsData.nativeLines) + { + buildNativeLine(graphicsData, webGLData); + } + else + { + buildLine(graphicsData, webGLData); + } +} + +/** + * Builds a line to draw using the poligon method. + * + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @private + * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties + * @param {object} webGLData - an object containing all the webGL-specific information to create this shape + */ +function buildLine(graphicsData, webGLData) { // TODO OPTIMISE! let points = graphicsData.points; @@ -224,3 +246,46 @@ indices.push(indexStart - 1); } + +/** + * Builds a line to draw using the gl.drawArrays(gl.LINES) method + * + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @private + * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties + * @param {object} webGLData - an object containing all the webGL-specific information to create this shape + */ +function buildNativeLine(graphicsData, webGLData) +{ + let i = 0; + const points = graphicsData.points; + + if (points.length === 0) return; + + const verts = webGLData.points; + const length = points.length / 2; + + // sort color + const color = hex2rgb(graphicsData.lineColor); + const alpha = graphicsData.lineAlpha; + const r = color[0] * alpha; + const g = color[1] * alpha; + const b = color[2] * alpha; + + for (i = 1; i < length; i++) + { + const p1x = points[(i - 1) * 2]; + const p1y = points[((i - 1) * 2) + 1]; + + const p2x = points[i * 2]; + const p2y = points[(i * 2) + 1]; + + verts.push(p1x, p1y); + verts.push(r, g, b, alpha); + + verts.push(p2x, p2y); + verts.push(r, g, b, alpha); + } +} diff --git a/src/core/graphics/webgl/utils/buildRoundedRectangle.js b/src/core/graphics/webgl/utils/buildRoundedRectangle.js index 9abc807..a6c8461 100644 --- a/src/core/graphics/webgl/utils/buildRoundedRectangle.js +++ b/src/core/graphics/webgl/utils/buildRoundedRectangle.js @@ -77,6 +77,26 @@ } /** + * Calculate a single point for a quadratic bezier curve. + * Utility function used by quadraticBezierCurve. + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @private + * @param {number} n1 - first number + * @param {number} n2 - second number + * @param {number} perc - percentage + * @return {number} the result + * + */ +function getPt(n1, n2, perc) +{ + const diff = n2 - n1; + + return n1 + (diff * perc); +} + +/** * Calculate the points for a quadratic bezier curve. (helper function..) * Based on: https://stackoverflow.com/questions/785097/how-do-i-implement-a-bezier-curve-in-c * @@ -105,13 +125,6 @@ let x = 0; let y = 0; - function getPt(n1, n2, perc) - { - const diff = n2 - n1; - - return n1 + (diff * perc); - } - for (let i = 0, j = 0; i <= n; ++i) { j = i / n; diff --git a/src/core/index.js b/src/core/index.js index f7a817c..4f414e8 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -30,6 +30,7 @@ export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; export { default as CanvasGraphicsRenderer } from './graphics/canvas/CanvasGraphicsRenderer'; +export { default as Spritesheet } from './textures/Spritesheet'; export { default as Texture } from './textures/Texture'; export { default as BaseTexture } from './textures/BaseTexture'; export { default as RenderTexture } from './textures/RenderTexture'; diff --git a/src/core/math/Matrix.js b/src/core/math/Matrix.js index 0b77477..3679860 100644 --- a/src/core/math/Matrix.js +++ b/src/core/math/Matrix.js @@ -13,45 +13,50 @@ export default class Matrix { /** - * + * @param {number} [a=1] - x scale + * @param {number} [b=0] - y skew + * @param {number} [c=0] - x skew + * @param {number} [d=1] - y scale + * @param {number} [tx=0] - x translation + * @param {number} [ty=0] - y translation */ - constructor() + constructor(a = 1, b = 0, c = 0, d = 1, tx = 0, ty = 0) { /** * @member {number} * @default 1 */ - this.a = 1; + this.a = a; /** * @member {number} * @default 0 */ - this.b = 0; + this.b = b; /** * @member {number} * @default 0 */ - this.c = 0; + this.c = c; /** * @member {number} * @default 1 */ - this.d = 1; + this.d = d; /** * @member {number} * @default 0 */ - this.tx = 0; + this.tx = tx; /** * @member {number} * @default 0 */ - this.ty = 0; + this.ty = ty; this.array = null; } diff --git a/src/core/renderers/SystemRenderer.js b/src/core/renderers/SystemRenderer.js index fa116a6..a24c775 100644 --- a/src/core/renderers/SystemRenderer.js +++ b/src/core/renderers/SystemRenderer.js @@ -1,5 +1,5 @@ import { sayHello, hex2string, hex2rgb } from '../utils'; -import { Matrix } from '../math'; +import { Matrix, Rectangle } from '../math'; import { RENDERER_TYPE } from '../const'; import settings from '../settings'; import Container from '../display/Container'; @@ -21,8 +21,8 @@ { /** * @param {string} system - The name of the system this renderer is for. - * @param {number} [width=800] - the width of the canvas view - * @param {number} [height=600] - the height of the canvas view + * @param {number} [screenWidth=800] - the width of the screen + * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false @@ -37,7 +37,7 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(system, width, height, options) + constructor(system, screenWidth, screenHeight, options) { super(); @@ -69,20 +69,13 @@ this.type = RENDERER_TYPE.UNKNOWN; /** - * The width of the canvas view + * Measurements of the screen. (0, 0, screenWidth, screenHeight) * - * @member {number} - * @default 800 - */ - this.width = width || 800; - - /** - * The height of the canvas view + * Its safe to use as filterArea or hitArea for whole stage * - * @member {number} - * @default 600 + * @member {PIXI.Rectangle} */ - this.height = height || 600; + this.screen = new Rectangle(0, 0, screenWidth || 800, screenHeight || 600); /** * The canvas element that everything is drawn to @@ -107,7 +100,7 @@ this.transparent = options.transparent; /** - * Whether the render view should be resized automatically + * Whether css dimensions of canvas view should be resized to screen dimensions automatically * * @member {boolean} */ @@ -192,23 +185,48 @@ } /** - * Resizes the canvas view to the specified width and height + * Same as view.width, actual number of pixels in the canvas by horizontal * - * @param {number} width - the new width of the canvas view - * @param {number} height - the new height of the canvas view + * @member {number} + * @readonly + * @default 800 */ - resize(width, height) + get width() { - this.width = width * this.resolution; - this.height = height * this.resolution; + return this.view.width; + } - this.view.width = this.width; - this.view.height = this.height; + /** + * Same as view.height, actual number of pixels in the canvas by vertical + * + * @member {number} + * @readonly + * @default 600 + */ + get height() + { + return this.view.height; + } + + /** + * Resizes the screen and canvas to the specified width and height + * Canvas dimensions are multiplied by resolution + * + * @param {number} screenWidth - the new width of the screen + * @param {number} screenHeight - the new height of the screen + */ + resize(screenWidth, screenHeight) + { + this.screen.width = screenWidth; + this.screen.height = screenHeight; + + this.view.width = screenWidth * this.resolution; + this.view.height = screenHeight * this.resolution; if (this.autoResize) { - this.view.style.width = `${this.width / this.resolution}px`; - this.view.style.height = `${this.height / this.resolution}px`; + this.view.style.width = `${screenWidth}px`; + this.view.style.height = `${screenHeight}px`; } } @@ -249,11 +267,10 @@ this.type = RENDERER_TYPE.UNKNOWN; - this.width = 0; - this.height = 0; - this.view = null; + this.screen = null; + this.resolution = 0; this.transparent = false; diff --git a/src/core/renderers/canvas/CanvasRenderer.js b/src/core/renderers/canvas/CanvasRenderer.js index 1bd2d5e..e84e49c 100644 --- a/src/core/renderers/canvas/CanvasRenderer.js +++ b/src/core/renderers/canvas/CanvasRenderer.js @@ -18,8 +18,8 @@ export default class CanvasRenderer extends SystemRenderer { /** - * @param {number} [width=800] - the width of the canvas view - * @param {number} [height=600] - the height of the canvas view + * @param {number} [screenWidth=800] - the width of the screen + * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false @@ -34,9 +34,9 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(width, height, options = {}) + constructor(screenWidth, screenHeight, options = {}) { - super('Canvas', width, height, options); + super('Canvas', screenWidth, screenHeight, options); this.type = RENDERER_TYPE.CANVAS; @@ -96,7 +96,7 @@ this.context = null; this.renderingToScreen = false; - this.resize(width, height); + this.resize(screenWidth, screenHeight); } /** @@ -283,12 +283,12 @@ * * @extends PIXI.SystemRenderer#resize * - * @param {number} width - The new width of the canvas view - * @param {number} height - The new height of the canvas view + * @param {number} screenWidth - the new width of the screen + * @param {number} screenHeight - the new height of the screen */ - resize(width, height) + resize(screenWidth, screenHeight) { - super.resize(width, height); + super.resize(screenWidth, screenHeight); // reset the scale mode.. oddly this seems to be reset when the canvas is resized. // surely a browser bug?? Let pixi fix that for you.. diff --git a/src/core/renderers/webgl/TextureManager.js b/src/core/renderers/webgl/TextureManager.js index b685c1a..8227289 100644 --- a/src/core/renderers/webgl/TextureManager.js +++ b/src/core/renderers/webgl/TextureManager.js @@ -61,7 +61,7 @@ * Updates and/or Creates a WebGL texture for the renderer's context. * * @param {PIXI.BaseTexture|PIXI.Texture} texture - the texture to update - * @param {Number} location - the location the texture will be bound to. + * @param {number} location - the location the texture will be bound to. * @return {GLTexture} The gl texture. */ updateTexture(texture, location) diff --git a/src/core/renderers/webgl/WebGLRenderer.js b/src/core/renderers/webgl/WebGLRenderer.js index e4cd8a0..830b382 100644 --- a/src/core/renderers/webgl/WebGLRenderer.js +++ b/src/core/renderers/webgl/WebGLRenderer.js @@ -31,8 +31,8 @@ { /** * - * @param {number} [width=0] - the width of the canvas view - * @param {number} [height=0] - the height of the canvas view + * @param {number} [screenWidth=800] - the width of the screen + * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false @@ -53,9 +53,9 @@ * @param {boolean} [options.legacy=false] - If true Pixi will aim to ensure compatibility * with older / less advanced devices. If you experiance unexplained flickering try setting this to true. */ - constructor(width, height, options = {}) + constructor(screenWidth, screenHeight, options = {}) { - super('WebGL', width, height, options); + super('WebGL', screenWidth, screenHeight, options); this.legacy = !!options.legacy; @@ -245,7 +245,7 @@ glCore._testingContext = gl; // setup the width/height properties and gl viewport - this.resize(this.width, this.height); + this.resize(this.screen.width, this.screen.height); } /** @@ -343,16 +343,16 @@ /** * Resizes the webGL view to the specified width and height. * - * @param {number} width - the new width of the webGL view - * @param {number} height - the new height of the webGL view + * @param {number} screenWidth - the new width of the screen + * @param {number} screenHeight - the new height of the screen */ - resize(width, height) + resize(screenWidth, screenHeight) { // if(width * this.resolution === this.width && height * this.resolution === this.height)return; - SystemRenderer.prototype.resize.call(this, width, height); + SystemRenderer.prototype.resize.call(this, screenWidth, screenHeight); - this.rootRenderTarget.resize(width, height); + this.rootRenderTarget.resize(screenWidth, screenHeight); if (this._activeRenderTarget === this.rootRenderTarget) { @@ -396,6 +396,26 @@ } /** + * Erases the render texture and fills the drawing area with a colour + * + * @param {PIXI.RenderTexture} renderTexture - The render texture to clear + * @param {number} [clearColor] - The colour + * @return {PIXI.WebGLRenderer} Returns itself. + */ + clearRenderTexture(renderTexture, clearColor) + { + const baseTexture = renderTexture.baseTexture; + const renderTarget = baseTexture._glRenderTargets[this.CONTEXT_UID]; + + if (renderTarget) + { + renderTarget.clear(clearColor); + } + + return this; + } + + /** * Binds a render texture for rendering * * @param {PIXI.RenderTexture} renderTexture - The render texture to render @@ -474,9 +494,10 @@ * Changes the current GLShader to the one given in parameter * * @param {PIXI.glCore.Shader} shader - the new glShader + * @param {boolean} [autoProject=true] - Whether automatically set the projection matrix * @return {PIXI.WebGLRenderer} Returns itself. */ - _bindGLShader(shader) + _bindGLShader(shader, autoProject) { // TODO cache if (this._activeShader !== shader) @@ -484,8 +505,14 @@ this._activeShader = shader; shader.bind(); - // automatically set the projection matrix - shader.uniforms.projectionMatrix = this._activeRenderTarget.projectionMatrix.toArray(true); + // `autoProject` normally would be a default parameter set to true + // but because of how Babel transpiles default parameters + // it hinders the performance of this method. + if (autoProject !== false) + { + // automatically set the projection matrix + shader.uniforms.projectionMatrix = this._activeRenderTarget.projectionMatrix.toArray(true); + } } return this; diff --git a/src/core/renderers/webgl/WebGLState.js b/src/core/renderers/webgl/WebGLState.js index ba6ecc5..6c846d5 100755 --- a/src/core/renderers/webgl/WebGLState.js +++ b/src/core/renderers/webgl/WebGLState.js @@ -82,18 +82,20 @@ push() { // next state.. - let state = this.stack[++this.stackIndex]; + let state = this.stack[this.stackIndex]; if (!state) { state = this.stack[this.stackIndex] = new Uint8Array(16); } + ++this.stackIndex; + // copy state.. // set active state so we can force overrides of gl state for (let i = 0; i < this.activeState.length; i++) { - this.activeState[i] = state[i]; + state[i] = this.activeState[i]; } } diff --git a/src/core/renderers/webgl/filters/Filter.js b/src/core/renderers/webgl/filters/Filter.js index 2c3eaad..4eb6128 100644 --- a/src/core/renderers/webgl/filters/Filter.js +++ b/src/core/renderers/webgl/filters/Filter.js @@ -9,7 +9,7 @@ * @memberof PIXI * @extends PIXI.Shader */ -class Filter extends Shader +export default class Filter extends Shader { /** * @param {string} [vertexSrc] - The source of the vertex shader. @@ -56,15 +56,16 @@ * @param {PIXI.RenderTarget} input - The input render target. * @param {PIXI.RenderTarget} output - The target to output to. * @param {boolean} clear - Should the output be cleared before rendering to it + * @param {object} [currentState] - It's current state of filter. + * There are some useful properties in the currentState : + * target, filters, sourceFrame, destinationFrame, renderTarget, resolution */ - apply(filterManager, input, output, clear) + apply(filterManager, input, output, clear, currentState) // eslint-disable-line no-unused-vars { // do as you please! - filterManager.applyFilter(this, input, output, clear); + filterManager.applyFilter(this, input, output, clear, currentState); // or just do a regular render.. } } - -export default Filter; diff --git a/src/core/renderers/webgl/filters/extractUniformsFromSrc.js b/src/core/renderers/webgl/filters/extractUniformsFromSrc.js index d78a0b1..4893214 100644 --- a/src/core/renderers/webgl/filters/extractUniformsFromSrc.js +++ b/src/core/renderers/webgl/filters/extractUniformsFromSrc.js @@ -12,7 +12,7 @@ function extractUniformsFromString(string) { - const maskRegex = new RegExp('^(projectionMatrix|uSampler|filterArea)$'); + const maskRegex = new RegExp('^(projectionMatrix|uSampler|filterArea|filterClamp)$'); const uniforms = {}; let nameSplit; diff --git a/src/core/renderers/webgl/managers/FilterManager.js b/src/core/renderers/webgl/managers/FilterManager.js index 43fc869..6c7422d 100644 --- a/src/core/renderers/webgl/managers/FilterManager.js +++ b/src/core/renderers/webgl/managers/FilterManager.js @@ -2,6 +2,7 @@ import RenderTarget from '../utils/RenderTarget'; import Quad from '../utils/Quad'; import { Rectangle } from '../../../math'; +import Shader from '../../../Shader'; import * as filterTransforms from '../filters/filterTransforms'; import bitTwiddle from 'bit-twiddle'; @@ -22,8 +23,6 @@ this.filters = []; this.target = null; this.resolution = 1; - - this.firstRun = true; } } @@ -155,7 +154,7 @@ if (filters.length === 1) { - filters[0].apply(this, currentState.renderTarget, lastState.renderTarget, false); + filters[0].apply(this, currentState.renderTarget, lastState.renderTarget, false, currentState); this.freePotRenderTarget(currentState.renderTarget); } else @@ -177,7 +176,7 @@ for (i = 0; i < filters.length - 1; ++i) { - filters[i].apply(this, flip, flop, true); + filters[i].apply(this, flip, flop, true, currentState); const t = flip; @@ -185,7 +184,7 @@ flop = t; } - filters[i].apply(this, flip, lastState.renderTarget, false); + filters[i].apply(this, flip, lastState.renderTarget, false, currentState); this.freePotRenderTarget(flip); this.freePotRenderTarget(flop); @@ -212,16 +211,28 @@ const renderer = this.renderer; const gl = renderer.gl; - renderer.bindShader(filter, true); + let shader = filter.glShaders[renderer.CONTEXT_UID]; - // TODO theres a better way! - // quad can be a mesh and we then should be able to pull this off without accessing the shader directly - const shader = renderer.shaderManager.getGLShader(); - - // TODO change quad to mesh.. - if (!this.firstRun) + // cacheing.. + if (!shader) { - this.firstRun = true; + if (filter.glShaderKey) + { + shader = this.shaderCache[filter.glShaderKey]; + + if (!shader) + { + shader = new Shader(this.gl, filter.vertexSrc, filter.fragmentSrc); + + filter.glShaders[renderer.CONTEXT_UID] = this.shaderCache[filter.glShaderKey] = shader; + } + } + else + { + shader = filter.glShaders[renderer.CONTEXT_UID] = new Shader(this.gl, filter.vertexSrc, filter.fragmentSrc); + } + + // TODO - this only needs to be done once? renderer.bindVao(null); this.quad.initVao(shader); @@ -244,58 +255,146 @@ renderer.maskManager.pushScissorMask(null, renderer.maskManager.scissorData); } - // renderer._bindGLShader(shader); + renderer.bindShader(shader); - let currentState; - const uniforms = filter.uniforms; + // free unit 0 for us, doesn't matter what was there + // don't try to restore it, because syncUniforms can upload it to another slot + // and it'll be a problem + const tex = this.renderer.emptyTextures[0]; - if (uniforms.filterArea) - { - currentState = this.filterData.stack[this.filterData.index]; - - const filterArea = uniforms.filterArea; - - filterArea[0] = currentState.renderTarget.size.width; - filterArea[1] = currentState.renderTarget.size.height; - filterArea[2] = currentState.sourceFrame.x; - filterArea[3] = currentState.sourceFrame.y; - - filter.uniforms.filterArea = filterArea; - } - - // use this to clamp displaced texture coords so they belong to filterArea - // see displacementFilter fragment shader for an example - if (uniforms.filterClamp) - { - currentState = this.filterData.stack[this.filterData.index]; - - const filterClamp = uniforms.filterClamp; - - filterClamp[0] = 0; - filterClamp[1] = 0; - filterClamp[2] = (currentState.sourceFrame.width - 1) / currentState.renderTarget.size.width; - filterClamp[3] = (currentState.sourceFrame.height - 1) / currentState.renderTarget.size.height; - - filter.uniforms.filterClamp = filterClamp; - } - - renderer.shaderManager.setUniforms(filter.uniforms); + this.renderer.boundTextures[0] = tex; + // this syncs the pixi filters uniforms with glsl uniforms + this.syncUniforms(shader, filter); renderer.state.setBlendMode(filter.blendMode); - // temporary bypass cache.. - const tex = this.renderer.boundTextures[0]; - gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, input.texture.texture); this.quad.vao.draw(this.renderer.gl.TRIANGLES, 6, 0); - // restore cache. gl.bindTexture(gl.TEXTURE_2D, tex._glTextures[this.renderer.CONTEXT_UID].texture); } /** + * Uploads the uniforms of the filter. + * + * @param {GLShader} shader - The underlying gl shader. + * @param {PIXI.Filter} filter - The filter we are synchronizing. + */ + syncUniforms(shader, filter) + { + const uniformData = filter.uniformData; + const uniforms = filter.uniforms; + + // 0 is reserved for the pixi texture so we start at 1! + let textureCount = 1; + let currentState; + + // filterArea and filterClamp that are handled by FilterManager directly + // they must not appear in uniformData + + if (shader.uniforms.filterArea) + { + currentState = this.filterData.stack[this.filterData.index]; + + const filterArea = shader.uniforms.filterArea; + + filterArea[0] = currentState.renderTarget.size.width; + filterArea[1] = currentState.renderTarget.size.height; + filterArea[2] = currentState.sourceFrame.x; + filterArea[3] = currentState.sourceFrame.y; + + shader.uniforms.filterArea = filterArea; + } + + // use this to clamp displaced texture coords so they belong to filterArea + // see displacementFilter fragment shader for an example + if (shader.uniforms.filterClamp) + { + currentState = currentState || this.filterData.stack[this.filterData.index]; + + const filterClamp = shader.uniforms.filterClamp; + + filterClamp[0] = 0; + filterClamp[1] = 0; + filterClamp[2] = (currentState.sourceFrame.width - 1) / currentState.renderTarget.size.width; + filterClamp[3] = (currentState.sourceFrame.height - 1) / currentState.renderTarget.size.height; + + shader.uniforms.filterClamp = filterClamp; + } + + // TODO Cacheing layer.. + for (const i in uniformData) + { + if (uniformData[i].type === 'sampler2D' && uniforms[i] !== 0) + { + if (uniforms[i].baseTexture) + { + shader.uniforms[i] = this.renderer.bindTexture(uniforms[i].baseTexture, textureCount); + } + else + { + shader.uniforms[i] = textureCount; + + // TODO + // this is helpful as renderTargets can also be set. + // Although thinking about it, we could probably + // make the filter texture cache return a RenderTexture + // rather than a renderTarget + const gl = this.renderer.gl; + + this.renderer.boundTextures[textureCount] = this.renderer.emptyTextures[textureCount]; + gl.activeTexture(gl.TEXTURE0 + textureCount); + + uniforms[i].texture.bind(); + } + + textureCount++; + } + else if (uniformData[i].type === 'mat3') + { + // check if its pixi matrix.. + if (uniforms[i].a !== undefined) + { + shader.uniforms[i] = uniforms[i].toArray(true); + } + else + { + shader.uniforms[i] = uniforms[i]; + } + } + else if (uniformData[i].type === 'vec2') + { + // check if its a point.. + if (uniforms[i].x !== undefined) + { + const val = shader.uniforms[i] || new Float32Array(2); + + val[0] = uniforms[i].x; + val[1] = uniforms[i].y; + shader.uniforms[i] = val; + } + else + { + shader.uniforms[i] = uniforms[i]; + } + } + else if (uniformData[i].type === 'float') + { + if (shader.uniforms.data[i].value !== uniformData[i]) + { + shader.uniforms[i] = uniforms[i]; + } + } + else + { + shader.uniforms[i] = uniforms[i]; + } + } + } + + /** * Gets a render target from the pool, or creates a new one. * * @param {boolean} clear - Should we clear the render texture when we get it? @@ -442,7 +541,6 @@ renderTarget.resolution = resolution; renderTarget.defaultFrame.width = renderTarget.size.width = minWidth / resolution; renderTarget.defaultFrame.height = renderTarget.size.height = minHeight / resolution; - renderTarget.attachStencilBuffer(); return renderTarget; } diff --git a/src/core/settings.js b/src/core/settings.js index bb08cb0..f4dd087 100644 --- a/src/core/settings.js +++ b/src/core/settings.js @@ -77,11 +77,11 @@ * * @static * @memberof PIXI.settings - * @type {RegExp|string} + * @type {RegExp} * @example `@2x` - * @default /@(.+)x/ + * @default /@([0-9\.]+)x/ */ - RETINA_PREFIX: /@(.+)x/, + RETINA_PREFIX: /@([0-9\.]+)x/, /** * The default render options if none are supplied to {@link PIXI.WebGLRenderer} @@ -175,14 +175,24 @@ SCALE_MODE: 0, /** - * Default specify float precision in shaders. + * Default specify float precision in vertex shader. + * + * @static + * @memberof PIXI.settings + * @type {PIXI.PRECISION} + * @default PIXI.PRECISION.HIGH + */ + PRECISION_VERTEX: 'highp', + + /** + * Default specify float precision in fragment shader. * * @static * @memberof PIXI.settings * @type {PIXI.PRECISION} * @default PIXI.PRECISION.MEDIUM */ - PRECISION: 'mediump', + PRECISION_FRAGMENT: 'mediump', /** * Can we upload the same buffer in a single frame? diff --git a/src/core/sprites/Sprite.js b/src/core/sprites/Sprite.js index 28b0b18..121bf58 100644 --- a/src/core/sprites/Sprite.js +++ b/src/core/sprites/Sprite.js @@ -212,11 +212,11 @@ } else { - w0 = orig.width * (1 - anchor._x); - w1 = orig.width * -anchor._x; + w1 = -anchor._x * orig.width; + w0 = w1 + orig.width; - h0 = orig.height * (1 - anchor._y); - h1 = orig.height * -anchor._y; + h1 = -anchor._y * orig.height; + h0 = h1 + orig.height; } // xy @@ -269,11 +269,11 @@ const tx = wt.tx; const ty = wt.ty; - const w0 = (orig.width) * (1 - anchor._x); - const w1 = (orig.width) * -anchor._x; + const w1 = -anchor._x * orig.width; + const w0 = w1 + orig.width; - const h0 = orig.height * (1 - anchor._y); - const h1 = orig.height * -anchor._y; + const h1 = -anchor._y * orig.height; + const h0 = h1 + orig.height; // xy vertexData[0] = (a * w1) + (c * h1) + tx; @@ -440,7 +440,7 @@ * * @static * @param {number|string|PIXI.BaseTexture|HTMLCanvasElement|HTMLVideoElement} source Source to create texture from - * @return {PIXI.Sprite} The newly created texture + * @return {PIXI.Sprite} The newly created sprite */ static from(source) { @@ -538,8 +538,8 @@ } /** - * The tint applied to the sprite. This is a hex value. A value of - * 0xFFFFFF will remove any tint effect. + * The tint applied to the sprite. This is a hex value. + * A value of 0xFFFFFF will remove any tint effect. * * @member {number} * @default 0xFFFFFF diff --git a/src/core/sprites/canvas/CanvasTinter.js b/src/core/sprites/canvas/CanvasTinter.js index 1cf316b..27e5e0c 100644 --- a/src/core/sprites/canvas/CanvasTinter.js +++ b/src/core/sprites/canvas/CanvasTinter.js @@ -75,8 +75,8 @@ crop.width *= resolution; crop.height *= resolution; - canvas.width = crop.width; - canvas.height = crop.height; + canvas.width = Math.ceil(crop.width); + canvas.height = Math.ceil(crop.height); context.fillStyle = `#${(`00000${(color | 0).toString(16)}`).substr(-6)}`; @@ -130,8 +130,8 @@ crop.width *= resolution; crop.height *= resolution; - canvas.width = crop.width; - canvas.height = crop.height; + canvas.width = Math.ceil(crop.width); + canvas.height = Math.ceil(crop.height); context.globalCompositeOperation = 'copy'; context.fillStyle = `#${(`00000${(color | 0).toString(16)}`).substr(-6)}`; @@ -172,8 +172,8 @@ crop.width *= resolution; crop.height *= resolution; - canvas.width = crop.width; - canvas.height = crop.height; + canvas.width = Math.ceil(crop.width); + canvas.height = Math.ceil(crop.height); context.globalCompositeOperation = 'copy'; context.drawImage( diff --git a/src/core/sprites/webgl/SpriteRenderer.js b/src/core/sprites/webgl/SpriteRenderer.js index 2affcd5..7c352e2 100644 --- a/src/core/sprites/webgl/SpriteRenderer.js +++ b/src/core/sprites/webgl/SpriteRenderer.js @@ -30,7 +30,7 @@ /** * Number of values sent in the vertex buffer. - * positionX, positionY, colorR, colorG, colorB = 5 + * aVertexPosition(2), aTextureCoord(1), aColor(1), aTextureId(1) = 5 * * @member {number} */ @@ -75,7 +75,6 @@ this.shader = null; this.currentIndex = 0; - TICK = 0; this.groups = []; for (let k = 0; k < this.size; k++) diff --git a/src/core/textures/BaseTexture.js b/src/core/textures/BaseTexture.js index 853de7e..aeccf39 100644 --- a/src/core/textures/BaseTexture.js +++ b/src/core/textures/BaseTexture.js @@ -232,16 +232,24 @@ this.realWidth = this.source.naturalWidth || this.source.videoWidth || this.source.width; this.realHeight = this.source.naturalHeight || this.source.videoHeight || this.source.height; - this.width = this.realWidth / this.resolution; - this.height = this.realHeight / this.resolution; - - this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); + this._updateDimensions(); } this.emit('update', this); } /** + * Update dimensions from real values + */ + _updateDimensions() + { + this.width = this.realWidth / this.resolution; + this.height = this.realHeight / this.resolution; + + this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); + } + + /** * Load a source. * * If the source is not-immediately-available, such as an image that needs to be @@ -525,11 +533,7 @@ this.realWidth = Math.round(svgWidth * this.sourceScale); this.realHeight = Math.round(svgHeight * this.sourceScale); - this.width = this.realWidth / this.resolution; - this.height = this.realHeight / this.resolution; - - // Check pow2 after scale - this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); + this._updateDimensions(); // Create a canvas element const canvas = document.createElement('canvas'); @@ -690,4 +694,52 @@ return baseTexture; } + + /** + * Helper function that creates a base texture based on the source you provide. + * The source can be - image url, image element, canvas element. + * + * @static + * @param {string|HTMLImageElement|HTMLCanvasElement} source - The source to create base texture from. + * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values + * @param {number} [sourceScale=(auto)] - Scale for the original image, used with Svg images. + * @return {PIXI.BaseTexture} The new base texture. + */ + static from(source, scaleMode, sourceScale) + { + if (typeof source === 'string') + { + return BaseTexture.fromImage(source, undefined, scaleMode, sourceScale); + } + else if (source instanceof HTMLImageElement) + { + const imageUrl = source.src; + let baseTexture = BaseTextureCache[imageUrl]; + + if (!baseTexture) + { + baseTexture = new BaseTexture(source, scaleMode); + baseTexture.imageUrl = imageUrl; + + if (sourceScale) + { + baseTexture.sourceScale = sourceScale; + } + + // if there is an @2x at the end of the url we are going to assume its a highres image + baseTexture.resolution = getResolutionOfUrl(imageUrl); + + BaseTextureCache[imageUrl] = baseTexture; + } + + return baseTexture; + } + else if (source instanceof HTMLCanvasElement) + { + return BaseTexture.fromCanvas(source, scaleMode); + } + + // lets assume its a base texture! + return source; + } } diff --git a/src/core/textures/Spritesheet.js b/src/core/textures/Spritesheet.js new file mode 100644 index 0000000..d469c62 --- /dev/null +++ b/src/core/textures/Spritesheet.js @@ -0,0 +1,274 @@ +import { Rectangle, Texture } from '../'; +import { getResolutionOfUrl, TextureCache } from '../utils'; + +/** + * Utility class for maintaining reference to a collection + * of Textures on a single Spritesheet. + * + * @class + * @memberof PIXI + */ +export default class Spritesheet +{ + /** + * The maximum number of Textures to build per process. + * + * @type {number} + * @default 1000 + */ + static get BATCH_SIZE() + { + return 1000; + } + + /** + * @param {PIXI.BaseTexture} baseTexture Reference to the source BaseTexture object. + * @param {Object} data - Spritesheet image data. + * @param {string} [resolutionFilename] - The filename to consider when determining + * the resolution of the spritesheet. If not provided, the imageUrl will + * be used on the BaseTexture. + */ + constructor(baseTexture, data, resolutionFilename = null) + { + /** + * Reference to ths source texture + * @type {PIXI.BaseTexture} + */ + this.baseTexture = baseTexture; + + /** + * Map of spritesheet textures. + * @type {Object} + */ + this.textures = {}; + + /** + * Reference to the original JSON data. + * @type {Object} + */ + this.data = data; + + /** + * The resolution of the spritesheet. + * @type {number} + */ + this.resolution = this._updateResolution( + resolutionFilename || this.baseTexture.imageUrl + ); + + /** + * Map of spritesheet frames. + * @type {Object} + * @private + */ + this._frames = this.data.frames; + + /** + * Collection of frame names. + * @type {string[]} + * @private + */ + this._frameKeys = Object.keys(this._frames); + + /** + * Current batch index being processed. + * @type {number} + * @private + */ + this._batchIndex = 0; + + /** + * Callback when parse is completed. + * @type {Function} + * @private + */ + this._callback = null; + } + + /** + * Generate the resolution from the filename or fallback + * to the meta.scale field of the JSON data. + * + * @private + * @param {string} resolutionFilename - The filename to use for resolving + * the default resolution. + * @return {number} Resolution to use for spritesheet. + */ + _updateResolution(resolutionFilename) + { + const scale = this.data.meta.scale; + + // Use a defaultValue of `null` to check if a url-based resolution is set + let resolution = getResolutionOfUrl(resolutionFilename, null); + + // No resolution found via URL + if (resolution === null) + { + // Use the scale value or default to 1 + resolution = scale !== undefined ? parseFloat(scale) : 1; + } + + // For non-1 resolutions, update baseTexture + if (resolution !== 1) + { + this.baseTexture.resolution = resolution; + this.baseTexture.update(); + } + + return resolution; + } + + /** + * Parser spritesheet from loaded data. This is done asynchronously + * to prevent creating too many Texture within a single process. + * + * @param {Function} callback - Callback when complete returns + * a map of the Textures for this spritesheet. + */ + parse(callback) + { + this._batchIndex = 0; + this._callback = callback; + + if (this._frameKeys.length <= Spritesheet.BATCH_SIZE) + { + this._processFrames(0); + this._parseComplete(); + } + else + { + this._nextBatch(); + } + } + + /** + * Process a batch of frames + * + * @private + * @param {number} initialFrameIndex - The index of frame to start. + */ + _processFrames(initialFrameIndex) + { + let frameIndex = initialFrameIndex; + const maxFrames = Spritesheet.BATCH_SIZE; + + while (frameIndex - initialFrameIndex < maxFrames && frameIndex < this._frameKeys.length) + { + const i = this._frameKeys[frameIndex]; + const rect = this._frames[i].frame; + + if (rect) + { + let frame = null; + let trim = null; + const orig = new Rectangle( + 0, + 0, + this._frames[i].sourceSize.w / this.resolution, + this._frames[i].sourceSize.h / this.resolution + ); + + if (this._frames[i].rotated) + { + frame = new Rectangle( + rect.x / this.resolution, + rect.y / this.resolution, + rect.h / this.resolution, + rect.w / this.resolution + ); + } + else + { + frame = new Rectangle( + rect.x / this.resolution, + rect.y / this.resolution, + rect.w / this.resolution, + rect.h / this.resolution + ); + } + + // Check to see if the sprite is trimmed + if (this._frames[i].trimmed) + { + trim = new Rectangle( + this._frames[i].spriteSourceSize.x / this.resolution, + this._frames[i].spriteSourceSize.y / this.resolution, + rect.w / this.resolution, + rect.h / this.resolution + ); + } + + this.textures[i] = new Texture( + this.baseTexture, + frame, + orig, + trim, + this._frames[i].rotated ? 2 : 0 + ); + + // lets also add the frame to pixi's global cache for fromFrame and fromImage functions + TextureCache[i] = this.textures[i]; + } + + frameIndex++; + } + } + + /** + * The parse has completed. + * + * @private + */ + _parseComplete() + { + const callback = this._callback; + + this._callback = null; + this._batchIndex = 0; + callback.call(this, this.textures); + } + + /** + * Begin the next batch of textures. + * + * @private + */ + _nextBatch() + { + this._processFrames(this._batchIndex * Spritesheet.BATCH_SIZE); + this._batchIndex++; + setTimeout(() => + { + if (this._batchIndex * Spritesheet.BATCH_SIZE < this._frameKeys.length) + { + this._nextBatch(); + } + else + { + this._parseComplete(); + } + }, 0); + } + + /** + * Destroy Spritesheet and don't use after this. + * + * @param {boolean} [destroyBase=false] Whether to destroy the base texture as well + */ + destroy(destroyBase = false) + { + for (const i in this.textures) + { + this.textures[i].destroy(); + } + this._frames = null; + this._frameKeys = null; + this.data = null; + this.textures = null; + if (destroyBase) + { + this.baseTexture.destroy(); + } + this.baseTexture = null; + } +} diff --git a/src/core/textures/Texture.js b/src/core/textures/Texture.js index 6403d4f..afe4254 100644 --- a/src/core/textures/Texture.js +++ b/src/core/textures/Texture.js @@ -3,7 +3,7 @@ import TextureUvs from './TextureUvs'; import EventEmitter from 'eventemitter3'; import { Rectangle } from '../math'; -import { TextureCache, BaseTextureCache } from '../utils'; +import { TextureCache, BaseTextureCache, getResolutionOfUrl } from '../utils'; /** * A texture stores the information that represents an image or part of an image. It cannot be added @@ -365,7 +365,8 @@ * The source can be - frame id, image url, video url, canvas element, video element, base texture * * @static - * @param {number|string|PIXI.BaseTexture|HTMLCanvasElement|HTMLVideoElement} source - Source to create texture from + * @param {number|string|HTMLImageElement|HTMLCanvasElement|HTMLVideoElement|PIXI.BaseTexture} + * source - Source to create texture from * @return {PIXI.Texture} The newly created texture */ static from(source) @@ -393,7 +394,7 @@ } else if (source instanceof HTMLImageElement) { - return new Texture(new BaseTexture(source)); + return new Texture(BaseTexture.from(source)); } else if (source instanceof HTMLCanvasElement) { @@ -413,6 +414,43 @@ } /** + * Create a texture from a source and add to the cache. + * + * @static + * @param {HTMLImageElement|HTMLCanvasElement} source - The input source. + * @param {String} imageUrl - File name of texture, for cache and resolving resolution. + * @param {String} [name] - Human readible name for the texture cache. If no name is + * specified, only `imageUrl` will be used as the cache ID. + * @return {PIXI.Texture} Output texture + */ + static fromLoader(source, imageUrl, name) + { + const baseTexture = new BaseTexture(source, null, getResolutionOfUrl(imageUrl)); + const texture = new Texture(baseTexture); + + baseTexture.imageUrl = imageUrl; + + // No name, use imageUrl instead + if (!name) + { + name = imageUrl; + } + + // lets also add the frame to pixi's global cache for fromFrame and fromImage fucntions + BaseTextureCache[name] = baseTexture; + TextureCache[name] = texture; + + // also add references by url if they are different. + if (name !== imageUrl) + { + BaseTextureCache[imageUrl] = baseTexture; + TextureCache[imageUrl] = texture; + } + + return texture; + } + + /** * Adds a texture to the global TextureCache. This cache is shared across the whole PIXI object. * * @static @@ -522,6 +560,29 @@ } } +function createWhiteTexture() +{ + const canvas = document.createElement('canvas'); + + canvas.width = 10; + canvas.height = 10; + + const context = canvas.getContext('2d'); + + context.fillStyle = 'white'; + context.fillRect(0, 0, 10, 10); + + return new Texture(new BaseTexture(canvas)); +} + +function removeAllHandlers(tex) +{ + tex.destroy = function _emptyDestroy() { /* empty */ }; + tex.on = function _emptyOn() { /* empty */ }; + tex.once = function _emptyOnce() { /* empty */ }; + tex.emit = function _emptyEmit() { /* empty */ }; +} + /** * An empty texture, used often to not have to create multiple empty textures. * Can not be destroyed. @@ -530,7 +591,14 @@ * @constant */ Texture.EMPTY = new Texture(new BaseTexture()); -Texture.EMPTY.destroy = function _emptyDestroy() { /* empty */ }; -Texture.EMPTY.on = function _emptyOn() { /* empty */ }; -Texture.EMPTY.once = function _emptyOnce() { /* empty */ }; -Texture.EMPTY.emit = function _emptyEmit() { /* empty */ }; +removeAllHandlers(Texture.EMPTY); + +/** + * A white texture of 10x10 size, used for graphics and other things + * Can not be destroyed. + * + * @static + * @constant + */ +Texture.WHITE = createWhiteTexture(); +removeAllHandlers(Texture.WHITE); diff --git a/src/core/ticker/Ticker.js b/src/core/ticker/Ticker.js index d98a1f0..17a1517 100644 --- a/src/core/ticker/Ticker.js +++ b/src/core/ticker/Ticker.js @@ -67,11 +67,12 @@ * is based, this value is neither capped nor scaled. * If the platform supports DOMHighResTimeStamp, * this value will have a precision of 1 µs. + * Defaults to target frame time * * @member {number} - * @default 1 / TARGET_FPMS + * @default 16.66 */ - this.elapsedMS = 1 / settings.TARGET_FPMS; // default to target frame time + this.elapsedMS = 1 / settings.TARGET_FPMS; /** * The last time {@link PIXI.ticker.Ticker#update} was invoked. diff --git a/src/core/utils/index.js b/src/core/utils/index.js index 6ff609f..be44750 100644 --- a/src/core/utils/index.js +++ b/src/core/utils/index.js @@ -2,6 +2,7 @@ import settings from '../settings'; import EventEmitter from 'eventemitter3'; import pluginTarget from './pluginTarget'; +import * as mixins from './mixin'; import * as isMobile from 'ismobilejs'; let nextUid = 0; @@ -33,6 +34,7 @@ * @type {mixin} */ pluginTarget, + mixins, }; /** @@ -93,7 +95,7 @@ */ export function rgb2hex(rgb) { - return (((rgb[0] * 255) << 16) + ((rgb[1] * 255) << 8) + (rgb[2] * 255)); + return (((rgb[0] * 255) << 16) + ((rgb[1] * 255) << 8) + (rgb[2] * 255 | 0)); } /** @@ -368,3 +370,43 @@ * @private */ export const ProgramCache = {}; + +/** + * Destroys all texture in the cache + * + * @memberof PIXI.utils + * @function destroyTextureCache + */ +export function destroyTextureCache() +{ + let key; + + for (key in TextureCache) + { + TextureCache[key].destroy(); + } + for (key in BaseTextureCache) + { + BaseTextureCache[key].destroy(); + } +} + +/** + * Removes all textures from cache, but does not destroy them + * + * @memberof PIXI.utils + * @function clearTextureCache + */ +export function clearTextureCache() +{ + let key; + + for (key in TextureCache) + { + delete TextureCache[key]; + } + for (key in BaseTextureCache) + { + delete BaseTextureCache[key]; + } +} diff --git a/src/core/utils/mixin.js b/src/core/utils/mixin.js new file mode 100644 index 0000000..114de2a --- /dev/null +++ b/src/core/utils/mixin.js @@ -0,0 +1,59 @@ +/** + * Mixes all enumerable properties and methods from a source object to a target object. + * + * @memberof PIXI.utils.mixins + * @function mixin + * @param {object} target The prototype or instance that properties and methods should be added to. + * @param {object} source The source of properties and methods to mix in. + */ +export function mixin(target, source) +{ + if (!target || !source) return; + // in ES8/ES2017, this would be really easy: + // Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); + + // get all the enumerable property keys + const keys = Object.keys(source); + + // loop through properties + for (let i = 0; i < keys.length; ++i) + { + const propertyName = keys[i]; + + // Set the property using the property descriptor - this works for accessors and normal value properties + Object.defineProperty(target, propertyName, Object.getOwnPropertyDescriptor(source, propertyName)); + } +} + +const mixins = []; + +/** + * Queues a mixin to be handled towards the end of the initialization of PIXI, so that deprecation + * can take effect. + * + * @memberof PIXI.utils.mixins + * @function delayMixin + * @private + * @param {object} target The prototype or instance that properties and methods should be added to. + * @param {object} source The source of properties and methods to mix in. + */ +export function delayMixin(target, source) +{ + mixins.push(target, source); +} + +/** + * Handles all mixins queued via delayMixin(). + * + * @memberof PIXI.utils.mixins + * @function performMixins + * @private + */ +export function performMixins() +{ + for (let i = 0; i < mixins.length; i += 2) + { + mixin(mixins[i], mixins[i + 1]); + } + mixins.length = 0; +} diff --git a/src/deprecation.js b/src/deprecation.js index 045a19f..b9c73b3 100644 --- a/src/deprecation.js +++ b/src/deprecation.js @@ -5,6 +5,7 @@ import * as filters from './filters'; import * as prepare from './prepare'; import * as loaders from './loaders'; +import * as interaction from './interaction'; // provide method to give a stack track for warnings // useful for tracking-down where deprecated methods/properties/classes @@ -534,7 +535,7 @@ { parent: 'GC_MODES', target: 'GC_MODE' }, { parent: 'WRAP_MODES', target: 'WRAP_MODE' }, { parent: 'SCALE_MODES', target: 'SCALE_MODE' }, - { parent: 'PRECISION', target: 'PRECISION' }, + { parent: 'PRECISION', target: 'PRECISION_FRAGMENT' }, ]; for (let i = 0; i < defaults.length; i++) @@ -558,6 +559,32 @@ }); } +Object.defineProperties(core.settings, { + + /** + * @static + * @name PRECISION + * @memberof PIXI.settings + * @see PIXI.PRECISION + * @deprecated since version 4.4.0 + */ + PRECISION: { + enumerable: true, + get() + { + warn('PIXI.settings.PRECISION has been deprecated, please use PIXI.settings.PRECISION_FRAGMENT'); + + return core.settings.PRECISION_FRAGMENT; + }, + set(value) + { + warn('PIXI.settings.PRECISION has been deprecated, please use PIXI.settings.PRECISION_FRAGMENT'); + + core.settings.PRECISION_FRAGMENT = value; + }, + }, +}); + Object.defineProperties(extras, { /** @@ -694,7 +721,7 @@ warn('determineFontProperties is now deprecated, please use the static calculateFontProperties method, ' + 'e.g : Text.calculateFontProperties(fontStyle);'); - return Text.calculateFontProperties(fontStyle); + return core.Text.calculateFontProperties(fontStyle); }; Object.defineProperties(core.TextStyle.prototype, { @@ -1003,3 +1030,69 @@ }, }, }); + +/** + * @name PIXI.interaction.interactiveTarget#defaultCursor + * @static + * @type {number} + * @see PIXI.interaction.interactiveTarget#cursor + * @deprecated since 4.3.0 + */ +Object.defineProperty(interaction.interactiveTarget, 'defaultCursor', { + set(value) + { + warn('Property defaultCursor has been replaced with \'cursor\'. '); + this.cursor = value; + }, + get() + { + warn('Property defaultCursor has been replaced with \'cursor\'. '); + + return this.cursor; + }, + enumerable: true, +}); + +/** + * @name PIXI.interaction.InteractionManager#defaultCursorStyle + * @static + * @type {string} + * @see PIXI.interaction.InteractionManager#cursorStyles + * @deprecated since 4.3.0 + */ +Object.defineProperty(interaction.InteractionManager, 'defaultCursorStyle', { + set(value) + { + warn('Property defaultCursorStyle has been replaced with \'cursorStyles.default\'. '); + this.cursorStyles.default = value; + }, + get() + { + warn('Property defaultCursorStyle has been replaced with \'cursorStyles.default\'. '); + + return this.cursorStyles.default; + }, +}); + +/** + * @name PIXI.interaction.InteractionManager#currentCursorStyle + * @static + * @type {string} + * @see PIXI.interaction.InteractionManager#cursorStyles + * @deprecated since 4.3.0 + */ +Object.defineProperty(interaction.InteractionManager, 'currentCursorStyle', { + set(value) + { + warn('Property currentCursorStyle has been removed.' + + 'See the currentCursorMode property, which works differently.'); + this.currentCursorMode = value; + }, + get() + { + warn('Property currentCursorStyle has been removed.' + + 'See the currentCursorMode property, which works differently.'); + + return this.currentCursorMode; + }, +}); diff --git a/src/extras/BitmapText.js b/src/extras/BitmapText.js index fb6c343..0a0e150 100644 --- a/src/extras/BitmapText.js +++ b/src/extras/BitmapText.js @@ -455,6 +455,73 @@ return this._textHeight; } + + /** + * Register a bitmap font with data and a texture. + * + * @static + * @param {XMLDocument} xml - The XML document data. + * @param {PIXI.Texture} texture - Texture with all symbols. + * @return {Object} Result font object with font, size, lineHeight and char fields. + */ + static registerFont(xml, texture) + { + const data = {}; + const info = xml.getElementsByTagName('info')[0]; + const common = xml.getElementsByTagName('common')[0]; + + data.font = info.getAttribute('face'); + data.size = parseInt(info.getAttribute('size'), 10); + data.lineHeight = parseInt(common.getAttribute('lineHeight'), 10); + data.chars = {}; + + // parse letters + const letters = xml.getElementsByTagName('char'); + + for (let i = 0; i < letters.length; i++) + { + const letter = letters[i]; + const charCode = parseInt(letter.getAttribute('id'), 10); + + const textureRect = new core.Rectangle( + parseInt(letter.getAttribute('x'), 10) + texture.frame.x, + parseInt(letter.getAttribute('y'), 10) + texture.frame.y, + parseInt(letter.getAttribute('width'), 10), + parseInt(letter.getAttribute('height'), 10) + ); + + data.chars[charCode] = { + xOffset: parseInt(letter.getAttribute('xoffset'), 10), + yOffset: parseInt(letter.getAttribute('yoffset'), 10), + xAdvance: parseInt(letter.getAttribute('xadvance'), 10), + kerning: {}, + texture: new core.Texture(texture.baseTexture, textureRect), + + }; + } + + // parse kernings + const kernings = xml.getElementsByTagName('kerning'); + + for (let i = 0; i < kernings.length; i++) + { + const kerning = kernings[i]; + const first = parseInt(kerning.getAttribute('first'), 10); + const second = parseInt(kerning.getAttribute('second'), 10); + const amount = parseInt(kerning.getAttribute('amount'), 10); + + if (data.chars[second]) + { + data.chars[second].kerning[first] = amount; + } + } + + // I'm leaving this as a temporary fix so we can test the bitmap fonts in v3 + // but it's very likely to change + BitmapText.fonts[data.font] = data; + + return data; + } } BitmapText.fonts = {}; diff --git a/src/index.js b/src/index.js index 59356cf..c553427 100644 --- a/src/index.js +++ b/src/index.js @@ -16,6 +16,10 @@ import * as particles from './particles'; import * as prepare from './prepare'; +// handle mixins now, after all code has been added, including deprecation +import { utils } from './core'; +utils.mixins.performMixins(); + export { accessibility, extract, diff --git a/src/interaction/InteractionManager.js b/src/interaction/InteractionManager.js index 7d51134..8baf13a 100644 --- a/src/interaction/InteractionManager.js +++ b/src/interaction/InteractionManager.js @@ -1,18 +1,20 @@ 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'; -// Mix interactiveTarget into core.DisplayObject.prototype -Object.assign( +// Mix interactiveTarget into core.DisplayObject.prototype, after deprecation has been handled +core.utils.mixins.delayMixin( core.DisplayObject.prototype, 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 +70,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 +101,6 @@ this.eventData = new InteractionEvent(); /** - * Tiny little interactiveData pool ! - * - * @member {PIXI.interaction.InteractionData[]} - */ - this.interactiveDataPool = []; - - /** * The DOM element to bind to. * * @private @@ -153,66 +155,12 @@ */ this.supportsPointerEvents = !!window.PointerEvent; - /** - * Are touch events being 'normalized' and converted into pointer events if pointer events are not supported - * For example, on a touch screen mobile device, a touchstart would also be emitted as a pointerdown - * - * @private - * @readonly - * @member {boolean} - */ - this.normalizeTouchEvents = !this.supportsPointerEvents && this.supportsTouchEvents; - - /** - * Are mouse events being 'normalized' and converted into pointer events if pointer events are not supported - * For example, on a desktop pc, a mousedown would also be emitted as a pointerdown - * - * @private - * @readonly - * @member {boolean} - */ - this.normalizeMouseEvents = !this.supportsPointerEvents && !MobileDevice.any; - // this will make it so that you don't have to call bind all the time /** * @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 +168,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,41 +199,32 @@ this.onPointerOver = this.onPointerOver.bind(this); /** - * @private - * @member {Function} + * Dictionary of how different cursor modes are handled. Strings are handled as CSS cursor + * values, objects are handled as dictionaries of CSS values for interactionDOMElement, + * and functions are called instead of changing the CSS. + * Default CSS cursor values are provided for 'default' and 'pointer' modes. + * @member {Object.)>} */ - this.onTouchStart = this.onTouchStart.bind(this); - this.processTouchStart = this.processTouchStart.bind(this); + this.cursorStyles = { + default: 'inherit', + pointer: 'pointer', + }; /** - * @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. - * - * @member {string} - * @default 'inherit' - */ - this.defaultCursorStyle = 'inherit'; - - /** - * The css style of the cursor that is being used. + * The mode of the cursor that is being used. + * The value of this is a key from the cursorStyles dictionary. * * @member {string} */ - this.currentCursorStyle = 'inherit'; + this.currentCursorMode = null; + + /** + * Internal cached let. + * + * @private + * @member {string} + */ + this.cursor = null; /** * Internal cached let. @@ -303,6 +249,7 @@ * object. * * @event mousedown + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -311,6 +258,7 @@ * on the display object. * * @event rightdown + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -319,6 +267,7 @@ * object. * * @event mouseup + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -327,6 +276,7 @@ * over the display object. * * @event rightup + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -335,6 +285,7 @@ * the display object. * * @event click + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -343,6 +294,7 @@ * and released on the display object. * * @event rightclick + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -352,6 +304,7 @@ * [mousedown]{@link PIXI.interaction.InteractionManager#event:mousedown}. * * @event mouseupoutside + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -361,6 +314,7 @@ * [rightdown]{@link PIXI.interaction.InteractionManager#event:rightdown}. * * @event rightupoutside + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -368,6 +322,7 @@ * Fired when a pointer device (usually a mouse) is moved while over the display object * * @event mousemove + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -375,6 +330,7 @@ * Fired when a pointer device (usually a mouse) is moved onto the display object * * @event mouseover + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -382,6 +338,7 @@ * Fired when a pointer device (usually a mouse) is moved off the display object * * @event mouseout + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -389,6 +346,7 @@ * Fired when a pointer device button is pressed on the display object. * * @event pointerdown + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -396,6 +354,14 @@ * Fired when a pointer device button is released over the display object. * * @event pointerup + * @type {PIXI.interaction.InteractionData} + * @memberof PIXI.interaction.InteractionManager# + */ + + /** + * Fired when the operating system cancels a pointer event + * + * @event pointercancel * @memberof PIXI.interaction.InteractionManager# */ @@ -403,6 +369,7 @@ * Fired when a pointer device button is pressed and released on the display object. * * @event pointertap + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -411,6 +378,7 @@ * registered a [pointerdown]{@link PIXI.interaction.InteractionManager#event:pointerdown}. * * @event pointerupoutside + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -418,6 +386,7 @@ * Fired when a pointer device is moved while over the display object * * @event pointermove + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -425,6 +394,7 @@ * Fired when a pointer device is moved onto the display object * * @event pointerover + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -432,6 +402,7 @@ * Fired when a pointer device is moved off the display object * * @event pointerout + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -439,6 +410,7 @@ * Fired when a touch point is placed on the display object. * * @event touchstart + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -446,6 +418,14 @@ * Fired when a touch point is removed from the display object. * * @event touchend + * @type {PIXI.interaction.InteractionData} + * @memberof PIXI.interaction.InteractionManager# + */ + + /** + * Fired when the operating system cancels a touch + * + * @event touchcancel * @memberof PIXI.interaction.InteractionManager# */ @@ -453,6 +433,7 @@ * Fired when a touch point is placed and removed from the display object. * * @event tap + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -461,6 +442,7 @@ * registered a [touchstart]{@link PIXI.interaction.InteractionManager#event:touchstart}. * * @event touchendoutside + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -468,6 +450,7 @@ * Fired when a touch point is moved along the display object. * * @event touchmove + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ } @@ -524,45 +507,30 @@ { window.document.addEventListener('pointermove', this.onPointerMove, true); this.interactionDOMElement.addEventListener('pointerdown', this.onPointerDown, true); - this.interactionDOMElement.addEventListener('pointerout', this.onPointerOut, true); + // pointerout is fired in addition to pointerup (for touch events) and pointercancel + // we already handle those, so for the purposes of what we do in onPointerOut, we only + // care about the pointerleave event + this.interactionDOMElement.addEventListener('pointerleave', 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; @@ -596,45 +564,26 @@ { window.document.removeEventListener('pointermove', this.onPointerMove, true); this.interactionDOMElement.removeEventListener('pointerdown', this.onPointerDown, true); - this.interactionDOMElement.removeEventListener('pointerout', this.onPointerOut, true); + this.interactionDOMElement.removeEventListener('pointerleave', 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) + 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.onPointerDown, true); + this.interactionDOMElement.removeEventListener('touchcancel', this.onPointerCancel, 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); - - 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 = null; @@ -664,7 +613,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; @@ -672,25 +621,80 @@ return; } - this.cursor = this.defaultCursorStyle; + this.cursor = null; // 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(); - - this.processInteractive(this.mouse.global, this.renderer._lastObjectRendered, this.processMouseOverOut, true); - - if (this.currentCursorStyle !== this.cursor) + for (const k in this.activeInteractionData) { - this.currentCursorStyle = this.cursor; - this.interactionDOMElement.style.cursor = this.cursor; + // eslint-disable-next-line no-prototype-builtins + if (this.activeInteractionData.hasOwnProperty(k)) + { + const interactionData = this.activeInteractionData[k]; + + if (interactionData.originalEvent && interactionData.pointerType !== 'touch') + { + const interactionEvent = this.configureInteractionEventForDOMEvent( + this.eventData, + interactionData.originalEvent, + interactionData + ); + + this.processInteractive( + interactionEvent, + this.renderer._lastObjectRendered, + this.processPointerOverOut, + true + ); + } + } } + this.setCursorMode(this.cursor); + // TODO } /** + * Sets the current cursor mode, handling any callbacks or CSS style changes. + * + * @param {string} mode - cursor mode, a key from the cursorStyles dictionary + */ + setCursorMode(mode) + { + mode = mode || 'default'; + // if the mode didn't actually change, bail early + if (this.currentCursorMode === mode) + { + return; + } + this.currentCursorMode = mode; + const style = this.cursorStyles[mode]; + + // only do things if there is a cursor style for it + if (style) + { + switch (typeof style) + { + case 'string': + // string styles are handled as cursor CSS + this.interactionDOMElement.style.cursor = style; + break; + case 'function': + // functions are just called, and passed the cursor mode + style(mode); + break; + case 'object': + // if it is an object, assume that it is a dictionary of CSS styles, + // apply it to the interactionDOMElement + Object.assign(this.interactionDOMElement.style, style); + break; + } + } + } + + /** * Dispatches an event on the display object that was interacted with * * @param {PIXI.Container|PIXI.Sprite|PIXI.extras.TilingSprite} displayObject - the display object in question @@ -748,22 +752,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 @@ -787,9 +795,8 @@ { interactiveParent = false; } - - // it has a mask! Then lets hit test that before continuing.. - if (hitTest && displayObject._mask) + // it has a mask! Then lets hit test that before continuing + else if (hitTest && displayObject._mask) { if (!displayObject._mask.containsPoint(point)) { @@ -797,14 +804,7 @@ } } - // it has a filterArea! Same as mask but easier, its a rectangle - if (hitTest && displayObject.filterArea) - { - if (!displayObject.filterArea.contains(point.x, point.y)) - { - hitTest = false; - } - } + let keepHitTestingAfterChildren = hitTest; // ** FREE TIP **! If an object is not interactive or has no buttons in it // (such as a game scene!) set interactiveChildren to false for that displayObject. @@ -818,7 +818,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 @@ -838,15 +838,20 @@ // This means we no longer need to hit test anything else. We still need to run // through all objects, but we don't need to perform any hit tests. - // { - hitTest = false; - // } + keepHitTestingAfterChildren = false; + + if (child.interactive) + { + hitTest = false; + } // we can break now as we have hit an object. } } } + hitTest = keepHitTestingAfterChildren; + // no point running this if the item is not interactive or does not have an interactive parent. if (interactive) { @@ -868,14 +873,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 +886,175 @@ } /** - * 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].isNormalized) { - 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); + if (!displayObject.trackedPointers[id]) + { + displayObject.trackedPointers[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.trackedPointers[id].rightDown = true; + } + else + { + displayObject.trackedPointers[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.trackedPointers[id] !== undefined) + { + delete displayObject.trackedPointers[id]; + this.dispatchEvent(displayObject, 'pointercancel', interactionEvent); + + if (e.type === 'touchcancel' || e.pointerType === 'touch') + { + this.dispatchEvent(displayObject, 'touchcancel', interactionEvent); + } } } @@ -1159,42 +1066,94 @@ */ 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) { - if (hit) - { - this.dispatchEvent(displayObject, 'pointerup', this.eventData); + const e = interactionEvent.data.originalEvent; - if (displayObject._pointerDown) + const id = interactionEvent.data.identifier; + + const trackingData = displayObject.trackedPointers[id]; + + const isTouch = (e.type === 'touchend' || e.pointerType === 'touch'); + + const isMouse = (e.type.indexOf('mouse') === 0 || e.pointerType === 'mouse'); + + // 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) { - displayObject._pointerDown = false; - this.dispatchEvent(displayObject, 'pointertap', this.eventData); + 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); + } + // update the down state of the tracking data + if (trackingData) + { + if (isRightButton) + { + trackingData.rightDown = hit; + } + else + { + trackingData.leftDown = hit; + } } } - else if (displayObject._pointerDown) + + // Pointers and Touches, and Mouse + if (hit) { - displayObject._pointerDown = false; - this.dispatchEvent(displayObject, 'pointerupoutside', this.eventData); + this.dispatchEvent(displayObject, 'pointerup', interactionEvent); + if (isTouch) this.dispatchEvent(displayObject, 'touchend', interactionEvent); + + if (trackingData) + { + this.dispatchEvent(displayObject, 'pointertap', interactionEvent); + if (isTouch) + { + this.dispatchEvent(displayObject, 'tap', interactionEvent); + // touches are no longer over (if they ever were) when we get the touchend + // so we should ensure that we don't keep pretending that they are + trackingData.over = false; + } + } + } + else if (trackingData) + { + this.dispatchEvent(displayObject, 'pointerupoutside', interactionEvent); + if (isTouch) this.dispatchEvent(displayObject, 'touchendoutside', interactionEvent); + } + // Only remove the tracking data if there is no over/down state still associated with it + if (trackingData && trackingData.none) + { + delete displayObject.trackedPointers[id]; } } @@ -1202,39 +1161,78 @@ * 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 = null; + } - 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') + { + this.setCursorMode(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 (isMouse) { - 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 +1240,94 @@ * 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.setCursorMode(null); + } - 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'); + + let trackingData = displayObject.trackedPointers[id]; + + // if we just moused over the display object, then we need to track that state + if (hit && !trackingData) + { + trackingData = displayObject.trackedPointers[id] = new InteractionTrackingData(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); + } + } + + // only change the cursor if it has not already been changed (by something deeper in the + // display tree) + if (isMouse && this.cursor === null) + { + this.cursor = displayObject.cursor; } } - 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); + } + // if there is no mouse down information for the pointer, then it is safe to delete + if (trackingData.none) + { + delete displayObject.trackedPointers[id]; + } } } @@ -1287,253 +1335,154 @@ * 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 && pointerEvent.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; + + // mark the touch as normalized, just so that we know we did it + touch.isNormalized = true; + + normalizedEvents.push(touch); + } } - else if (this.normalizeMouseEvents) + // apparently PointerEvent subclasses MouseEvent, so yay + else if (event instanceof MouseEvent && (!this.supportsPointerEvents || !(event instanceof window.PointerEvent))) { if (typeof event.isPrimary === 'undefined') event.isPrimary = true; if (typeof event.width === 'undefined') event.width = 1; @@ -1541,10 +1490,21 @@ 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; + + // mark the mouse event as normalized, just so that we know we did it + event.isNormalized = true; + + normalizedEvents.push(event); } + else + { + normalizedEvents.push(event); + } + + return normalizedEvents; } /** @@ -1563,30 +1523,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 +1542,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..6a1f69e --- /dev/null +++ b/src/interaction/InteractionTrackingData.js @@ -0,0 +1,147 @@ +/** + * 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 inactive (not over or down)? + * + * @member {number} + * @memberof PIXI.interaction.InteractionTrackingData# + */ + get none() + { + return this._flags === this.constructor.FLAGS.NONE; + } + + /** + * 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..d252fba 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 * ); */ @@ -38,68 +38,56 @@ hitArea: null, /** - * If enabled, the mouse cursor will change when hovered over the displayObject if it is interactive + * If enabled, the mouse cursor use the pointer behavior when hovered over the displayObject if it is interactive + * Setting this changes the 'cursor' property to `'pointer'`. * - * @inner {boolean} + * @member {boolean} + * @memberof PIXI.interaction.interactiveTarget# */ - buttonMode: false, + get buttonMode() + { + return this.cursor === 'pointer'; + }, + set buttonMode(value) + { + if (value) + { + this.cursor = 'pointer'; + } + else if (this.cursor === 'pointer') + { + this.cursor = null; + } + }, /** - * If buttonMode is enabled, this defines what CSS cursor property is used when the mouse cursor - * is hovered over the displayObject + * This defines what cursor mode is used when the mouse cursor + * is hovered over the displayObject. * * @see https://developer.mozilla.org/en/docs/Web/CSS/cursor * * @inner {string} */ - defaultCursor: 'pointer', - - // some internal checks.. - /** - * Internal check to detect if the mouse cursor is hovered over the displayObject - * - * @inner {boolean} - * @private - */ - _over: false, + cursor: null, /** - * Internal check to detect if the left mouse button is pressed on the displayObject + * Internal set of all active pointers, by identifier * - * @inner {boolean} + * @member {Map} + * @memberof PIXI.interaction.interactiveTarget# * @private */ - _isLeftDown: false, + get trackedPointers() + { + if (this._trackedPointers === undefined) this._trackedPointers = {}; + + return this._trackedPointers; + }, /** - * Internal check to detect if the right mouse button is pressed on the displayObject + * Map of all tracked pointers, by identifier. Use trackedPointers to access. * - * @inner {boolean} - * @private + * @private {Map} */ - _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, + _trackedPointers: undefined, }; diff --git a/src/loaders/bitmapFontParser.js b/src/loaders/bitmapFontParser.js index cc2618c..f4cf996 100644 --- a/src/loaders/bitmapFontParser.js +++ b/src/loaders/bitmapFontParser.js @@ -1,63 +1,19 @@ import * as path from 'path'; -import { Rectangle, Texture, utils } from '../core'; +import { utils } from '../core'; import { Resource } from 'resource-loader'; import { BitmapText } from '../extras'; +/** + * Register a BitmapText font from loader resource. + * + * @function parseBitmapFontData + * @memberof PIXI.loaders + * @param {PIXI.loaders.Resource} resource - Loader resource. + * @param {PIXI.Texture} texture - Reference to texture. + */ export function parse(resource, texture) { - const data = {}; - const info = resource.data.getElementsByTagName('info')[0]; - const common = resource.data.getElementsByTagName('common')[0]; - - data.font = info.getAttribute('face'); - data.size = parseInt(info.getAttribute('size'), 10); - data.lineHeight = parseInt(common.getAttribute('lineHeight'), 10); - data.chars = {}; - - // parse letters - const letters = resource.data.getElementsByTagName('char'); - - for (let i = 0; i < letters.length; i++) - { - const charCode = parseInt(letters[i].getAttribute('id'), 10); - - const textureRect = new Rectangle( - parseInt(letters[i].getAttribute('x'), 10) + texture.frame.x, - parseInt(letters[i].getAttribute('y'), 10) + texture.frame.y, - parseInt(letters[i].getAttribute('width'), 10), - parseInt(letters[i].getAttribute('height'), 10) - ); - - data.chars[charCode] = { - xOffset: parseInt(letters[i].getAttribute('xoffset'), 10), - yOffset: parseInt(letters[i].getAttribute('yoffset'), 10), - xAdvance: parseInt(letters[i].getAttribute('xadvance'), 10), - kerning: {}, - texture: new Texture(texture.baseTexture, textureRect), - - }; - } - - // parse kernings - const kernings = resource.data.getElementsByTagName('kerning'); - - for (let i = 0; i < kernings.length; i++) - { - const first = parseInt(kernings[i].getAttribute('first'), 10); - const second = parseInt(kernings[i].getAttribute('second'), 10); - const amount = parseInt(kernings[i].getAttribute('amount'), 10); - - if (data.chars[second]) - { - data.chars[second].kerning[first] = amount; - } - } - - resource.bitmapFont = data; - - // I'm leaving this as a temporary fix so we can test the bitmap fonts in v3 - // but it's very likely to change - BitmapText.fonts[data.font] = data; + resource.bitmapFont = BitmapText.registerFont(resource.data, texture); } export default function () diff --git a/src/loaders/index.js b/src/loaders/index.js index 8942436..5189793 100644 --- a/src/loaders/index.js +++ b/src/loaders/index.js @@ -5,4 +5,11 @@ export { default as bitmapFontParser, parse as parseBitmapFontData } from './bitmapFontParser'; export { default as spritesheetParser } from './spritesheetParser'; export { default as textureParser } from './textureParser'; + +/** + * Reference to **resource-loader**'s Resource class. + * See https://github.com/englercj/resource-loader + * @class Resource + * @memberof PIXI.loaders + */ export { Resource } from 'resource-loader'; diff --git a/src/loaders/loader.js b/src/loaders/loader.js index 0a670cb..767a411 100644 --- a/src/loaders/loader.js +++ b/src/loaders/loader.js @@ -7,20 +7,47 @@ /** * - * The new loader, extends Resource Loader by Chad Engler : https://github.com/englercj/resource-loader + * The new loader, extends Resource Loader by Chad Engler: https://github.com/englercj/resource-loader * * ```js - * let loader = PIXI.loader; // pixi exposes a premade instance for you to use. + * const loader = PIXI.loader; // pixi exposes a premade instance for you to use. * //or - * let loader = new PIXI.loaders.Loader(); // you can also create your own if you want + * const loader = new PIXI.loaders.Loader(); // you can also create your own if you want * - * loader.add('bunny', 'data/bunny.png'); - * loader.add('spaceship', 'assets/spritesheet.json'); + * const sprites = {}; + * + * // Chainable `add` to enqueue a resource + * loader.add('bunny', 'data/bunny.png') + * .add('spaceship', 'assets/spritesheet.json'); * loader.add('scoreFont', 'assets/score.fnt'); * - * loader.once('complete',onAssetsLoaded); + * // Chainable `pre` to add a middleware that runs for each resource, *before* loading that resource. + * // This is useful to implement custom caching modules (using filesystem, indexeddb, memory, etc). + * loader.pre(cachingMiddleware); * - * loader.load(); + * // Chainable `use` to add a middleware that runs for each resource, *after* loading that resource. + * // This is useful to implement custom parsing modules (like spritesheet parsers, spine parser, etc). + * loader.use(parsingMiddleware); + * + * // The `load` method loads the queue of resources, and calls the passed in callback called once all + * // resources have loaded. + * loader.load((loader, resources) => { + * // resources is an object where the key is the name of the resource loaded and the value is the resource object. + * // They have a couple default properties: + * // - `url`: The URL that the resource was loaded from + * // - `error`: The error that happened when trying to load (if any) + * // - `data`: The raw data that was loaded + * // also may contain other properties based on the middleware that runs. + * sprites.bunny = new PIXI.TilingSprite(resources.bunny.texture); + * sprites.spaceship = new PIXI.TilingSprite(resources.spaceship.texture); + * sprites.scoreFont = new PIXI.TilingSprite(resources.scoreFont.texture); + * }); + * + * // throughout the process multiple signals can be dispatched. + * loader.onProgress.add(() => {}); // called once per loaded/errored file + * loader.onError.add(() => {}); // called once per errored file + * loader.onLoad.add(() => {}); // called once per loaded file + * loader.onComplete.add(() => {}); // called once when the queued resources all load. * ``` * * @see https://github.com/englercj/resource-loader diff --git a/src/loaders/spritesheetParser.js b/src/loaders/spritesheetParser.js index 7570332..e57dfb2 100644 --- a/src/loaders/spritesheetParser.js +++ b/src/loaders/spritesheetParser.js @@ -1,8 +1,6 @@ import { Resource } from 'resource-loader'; import path from 'path'; -import * as core from '../core'; - -const BATCH_SIZE = 1000; +import { Spritesheet } from '../core'; export default function () { @@ -43,134 +41,18 @@ // load the image for this sheet this.add(imageResourceName, resourcePath, loadOptions, function onImageLoad(res) { - resource.textures = {}; + const spritesheet = new Spritesheet( + res.texture.baseTexture, + resource.data, + resource.url + ); - const frames = resource.data.frames; - const frameKeys = Object.keys(frames); - const baseTexture = res.texture.baseTexture; - const scale = resource.data.meta.scale; - - // Use a defaultValue of `null` to check if a url-based resolution is set - let resolution = core.utils.getResolutionOfUrl(resource.url, null); - - // No resolution found via URL - if (resolution === null) + spritesheet.parse(() => { - // Use the scale value or default to 1 - resolution = scale !== undefined ? scale : 1; - } - - // For non-1 resolutions, update baseTexture - if (resolution !== 1) - { - baseTexture.resolution = resolution; - baseTexture.update(); - } - - let batchIndex = 0; - - function processFrames(initialFrameIndex, maxFrames) - { - let frameIndex = initialFrameIndex; - - while (frameIndex - initialFrameIndex < maxFrames && frameIndex < frameKeys.length) - { - const i = frameKeys[frameIndex]; - const rect = frames[i].frame; - - if (rect) - { - let frame = null; - let trim = null; - const orig = new core.Rectangle( - 0, - 0, - frames[i].sourceSize.w / resolution, - frames[i].sourceSize.h / resolution - ); - - if (frames[i].rotated) - { - frame = new core.Rectangle( - rect.x / resolution, - rect.y / resolution, - rect.h / resolution, - rect.w / resolution - ); - } - else - { - frame = new core.Rectangle( - rect.x / resolution, - rect.y / resolution, - rect.w / resolution, - rect.h / resolution - ); - } - - // Check to see if the sprite is trimmed - if (frames[i].trimmed) - { - trim = new core.Rectangle( - frames[i].spriteSourceSize.x / resolution, - frames[i].spriteSourceSize.y / resolution, - rect.w / resolution, - rect.h / resolution - ); - } - - resource.textures[i] = new core.Texture( - baseTexture, - frame, - orig, - trim, - frames[i].rotated ? 2 : 0 - ); - - // lets also add the frame to pixi's global cache for fromFrame and fromImage functions - core.utils.TextureCache[i] = resource.textures[i]; - } - - frameIndex++; - } - } - - function shouldProcessNextBatch() - { - return batchIndex * BATCH_SIZE < frameKeys.length; - } - - function processNextBatch(done) - { - processFrames(batchIndex * BATCH_SIZE, BATCH_SIZE); - batchIndex++; - setTimeout(done, 0); - } - - function iteration() - { - processNextBatch(() => - { - if (shouldProcessNextBatch()) - { - iteration(); - } - else - { - next(); - } - }); - } - - if (frameKeys.length <= BATCH_SIZE) - { - processFrames(0, BATCH_SIZE); + resource.spritesheet = spritesheet; + resource.textures = spritesheet.textures; next(); - } - else - { - iteration(); - } + }); }); }; } diff --git a/src/loaders/textureParser.js b/src/loaders/textureParser.js index 5398a7f..480edc1 100644 --- a/src/loaders/textureParser.js +++ b/src/loaders/textureParser.js @@ -1,5 +1,5 @@ -import * as core from '../core'; import { Resource } from 'resource-loader'; +import Texture from '../core/textures/Texture'; export default function () { @@ -8,23 +8,12 @@ // create a new texture if the data is an Image object if (resource.data && resource.type === Resource.TYPE.IMAGE) { - const baseTexture = new core.BaseTexture(resource.data, null, core.utils.getResolutionOfUrl(resource.url)); - - baseTexture.imageUrl = resource.url; - resource.texture = new core.Texture(baseTexture); - - // lets also add the frame to pixi's global cache for fromFrame and fromImage fucntions - core.utils.BaseTextureCache[resource.name] = baseTexture; - core.utils.TextureCache[resource.name] = resource.texture; - - // also add references by url if they are different. - if (resource.name !== resource.url) - { - core.utils.BaseTextureCache[resource.url] = baseTexture; - core.utils.TextureCache[resource.url] = resource.texture; - } + resource.texture = Texture.fromLoader( + resource.data, + resource.url, + resource.name + ); } - next(); }; } diff --git a/test/core/Container.js b/test/core/Container.js index 8a10b40..d11be33 100644 --- a/test/core/Container.js +++ b/test/core/Container.js @@ -1,5 +1,33 @@ 'use strict'; +function testAddChild(fn) +{ + fn(function (container, obj) + { + container.addChild(obj); + }); + fn(function (container, obj) + { + container.addChildAt(obj); + }); +} + +function testRemoveChild(fn) +{ + fn(function (container, obj) + { + container.removeChild(obj); + }); + fn(function (container, obj) + { + container.removeChildAt(container.children.indexOf(obj)); + }); + fn(function (container, obj) + { + container.removeChildren(container.children.indexOf(obj), container.children.indexOf(obj) + 1); + }); +} + describe('PIXI.Container', function () { describe('parent', function () @@ -70,6 +98,49 @@ expect(spy).to.have.been.called; expect(spy).to.have.been.calledWith(0); }); + + it('should flag child transform and container bounds for recalculation', testAddChild(function (mockAddChild) + { + const container = new PIXI.Container(); + const child = new PIXI.Container(); + + container.getBounds(); + child.getBounds(); + + const boundsID = container._boundsID; + const childParentID = child.transform._parentID; + + mockAddChild(container, child); + + expect(boundsID).to.not.be.equals(container._boundsID); + expect(childParentID).to.not.be.equals(child.transform._parentID); + })); + + it('should recalculate added child correctly', testAddChild(function (mockAddChild) + { + const parent = new PIXI.Container(); + const container = new PIXI.Container(); + const graphics = new PIXI.Graphics(); + + parent.addChild(container); + + graphics.drawRect(0, 0, 10, 10); + container.position.set(100, 200); + container.updateTransform(); + + graphics.getBounds(); + // Oops, that can happen sometimes! + graphics.transform._parentID = container.transform._worldID + 1; + + mockAddChild(container, graphics); + + const bounds = graphics.getBounds(); + + expect(bounds.x).to.be.equal(100); + expect(bounds.y).to.be.equal(200); + expect(bounds.width).to.be.equal(10); + expect(bounds.height).to.be.equal(10); + })); }); describe('removeChildAt', function () @@ -202,6 +273,44 @@ expect(spy).to.have.been.called; expect(spy).to.have.been.calledWith(0); }); + + it('should flag transform for recalculation', testRemoveChild(function (mockRemoveChild) + { + const container = new PIXI.Container(); + const child = new PIXI.Container(); + + container.addChild(child); + container.getBounds(); + + const childParentID = child.transform._parentID; + const boundsID = container._boundsID; + + mockRemoveChild(container, child); + + expect(childParentID).to.not.be.equals(child.transform._parentID); + expect(boundsID).to.not.be.equals(container._boundsID); + })); + + it('should recalculate removed child correctly', testRemoveChild(function (mockRemoveChild) + { + const parent = new PIXI.Container(); + const container = new PIXI.Container(); + const graphics = new PIXI.Graphics(); + + parent.addChild(container); + + graphics.drawRect(0, 0, 10, 10); + container.position.set(100, 200); + container.addChild(graphics); + graphics.getBounds(); + + mockRemoveChild(container, graphics); + + const bounds = graphics.getBounds(); + + expect(bounds.x).to.be.equal(0); + expect(bounds.y).to.be.equal(0); + })); }); describe('getChildIndex', function () diff --git a/test/core/Graphics.js b/test/core/Graphics.js index 98c7584..11e47a0 100644 --- a/test/core/Graphics.js +++ b/test/core/Graphics.js @@ -1,5 +1,8 @@ 'use strict'; +const MockPointer = require('../interaction/MockPointer'); +const withGL = require('../withGL'); + describe('PIXI.Graphics', function () { describe('constructor', function () @@ -218,4 +221,153 @@ expect(spy).to.have.been.calledOnce; }); }); + + describe('mask', function () + { + it('should trigger interaction callback when no mask present', function () + { + const stage = new PIXI.Container(); + const pointer = new MockPointer(stage); + const graphics = new PIXI.Graphics(); + const mask = new PIXI.Graphics(); + const spy = sinon.spy(); + + graphics.interactive = true; + graphics.beginFill(0xFF0000); + graphics.drawRect(0, 0, 50, 50); + graphics.on('click', spy); + stage.addChild(graphics); + mask.beginFill(); + mask.drawRect(0, 0, 50, 50); + graphics.mask = mask; + + pointer.click(10, 10); + + expect(spy).to.have.been.calledOnce; + }); + it('should trigger interaction callback when mask uses beginFill', function () + { + const stage = new PIXI.Container(); + const pointer = new MockPointer(stage); + const graphics = new PIXI.Graphics(); + const mask = new PIXI.Graphics(); + const spy = sinon.spy(); + + graphics.interactive = true; + graphics.beginFill(0xFF0000); + graphics.drawRect(0, 0, 50, 50); + graphics.on('click', spy); + stage.addChild(graphics); + mask.beginFill(); + mask.drawRect(0, 0, 50, 50); + graphics.mask = mask; + + pointer.click(10, 10); + + expect(spy).to.have.been.calledOnce; + }); + + it('should not trigger interaction callback when mask doesn\'t use beginFill', function () + { + const stage = new PIXI.Container(); + const pointer = new MockPointer(stage); + const graphics = new PIXI.Graphics(); + const mask = new PIXI.Graphics(); + const spy = sinon.spy(); + + graphics.interactive = true; + graphics.beginFill(0xFF0000); + graphics.drawRect(0, 0, 50, 50); + graphics.on('click', spy); + stage.addChild(graphics); + mask.drawRect(0, 0, 50, 50); + graphics.mask = mask; + + pointer.click(10, 10); + + expect(spy).to.have.not.been.called; + }); + + it('should trigger interaction callback when mask doesn\'t use beginFill but hitArea is defined', function () + { + const stage = new PIXI.Container(); + const pointer = new MockPointer(stage); + const graphics = new PIXI.Graphics(); + const mask = new PIXI.Graphics(); + const spy = sinon.spy(); + + graphics.interactive = true; + graphics.beginFill(0xFF0000); + graphics.hitArea = new PIXI.Rectangle(0, 0, 50, 50); + graphics.drawRect(0, 0, 50, 50); + graphics.on('click', spy); + stage.addChild(graphics); + mask.drawRect(0, 0, 50, 50); + graphics.mask = mask; + + pointer.click(10, 10); + + expect(spy).to.have.been.calledOnce; + }); + + it('should trigger interaction callback when mask is a sprite', function () + { + const stage = new PIXI.Container(); + const pointer = new MockPointer(stage); + const graphics = new PIXI.Graphics(); + const mask = new PIXI.Graphics(); + const spy = sinon.spy(); + + graphics.interactive = true; + graphics.beginFill(0xFF0000); + graphics.drawRect(0, 0, 50, 50); + graphics.on('click', spy); + stage.addChild(graphics); + mask.drawRect(0, 0, 50, 50); + graphics.mask = new PIXI.Sprite(mask.generateCanvasTexture()); + + pointer.click(10, 10); + + expect(spy).to.have.been.calledOnce; + }); + + it('should calculate tint, alpha and blendMode of fastRect correctly', withGL(function () + { + const renderer = new PIXI.WebGLRenderer(200, 200, {}); + + try + { + const graphics = new PIXI.Graphics(); + + graphics.beginFill(0x102030, 0.6); + graphics.drawRect(2, 3, 100, 100); + graphics.endFill(); + graphics.tint = 0x101010; + graphics.blendMode = 2; + graphics.alpha = 0.3; + + renderer.render(graphics); + + expect(graphics.isFastRect()).to.be.true; + + const sprite = graphics._spriteRect; + + expect(sprite).to.not.be.equals(null); + expect(sprite.worldAlpha).to.equals(0.18); + expect(sprite.blendMode).to.equals(2); + expect(sprite.tint).to.equals(0x010203); + + const bounds = sprite.getBounds(); + + expect(bounds.x).to.equals(2); + expect(bounds.y).to.equals(3); + expect(bounds.width).to.equals(100); + expect(bounds.height).to.equals(100); + } + finally + { + renderer.destroy(); + } + })); + }); }); diff --git a/test/core/Spritesheet.js b/test/core/Spritesheet.js new file mode 100644 index 0000000..283adf5 --- /dev/null +++ b/test/core/Spritesheet.js @@ -0,0 +1,89 @@ +'use strict'; + +const path = require('path'); + +describe('PIXI.Spritesheet', function () +{ + before(function () + { + this.resources = path.join(__dirname, 'resources'); + this.validate = function (spritesheet, done) + { + spritesheet.parse(function (textures) + { + const id = 'goldmine_10_5.png'; + + expect(Object.keys(textures).length).to.equal(1); + expect(Object.keys(spritesheet.textures).length).to.equal(1); + expect(textures[id]).to.be.an.instanceof(PIXI.Texture); + expect(textures[id].width).to.equal(spritesheet.data.frames[id].frame.w / spritesheet.resolution); + expect(textures[id].height).to.equal(spritesheet.data.frames[id].frame.h / spritesheet.resolution); + spritesheet.destroy(true); + expect(spritesheet.textures).to.be.null; + expect(spritesheet.baseTexture).to.be.null; + done(); + }); + }; + }); + + it('should exist on PIXI', function () + { + expect(PIXI.Spritesheet).to.be.a.function; + expect(PIXI.Spritesheet.BATCH_SIZE).to.be.a.number; + }); + + it('should create an instance', function () + { + const baseTexture = new PIXI.BaseTexture(); + const data = { + frames: {}, + meta: {}, + }; + const spritesheet = new PIXI.Spritesheet(baseTexture, data); + + expect(spritesheet.data).to.equal(data); + expect(spritesheet.baseTexture).to.equal(baseTexture); + expect(spritesheet.resolution).to.equal(1); + + spritesheet.destroy(true); + }); + + it('should create instance with scale resolution', function (done) + { + const data = require(path.resolve(this.resources, 'building1.json')); // eslint-disable-line global-require + const image = new Image(); + + image.src = path.join(this.resources, data.meta.image); + image.onload = () => + { + const baseTexture = new PIXI.BaseTexture(image, null, 1); + const spritesheet = new PIXI.Spritesheet(baseTexture, data); + + expect(data).to.be.an.object; + expect(data.meta.image).to.equal('building1.png'); + expect(spritesheet.resolution).to.equal(0.5); + + this.validate(spritesheet, done); + }; + }); + + it('should create instance with filename resolution', function (done) + { + const uri = path.resolve(this.resources, 'building1@2x.json'); + const data = require(uri); // eslint-disable-line global-require + const image = new Image(); + + image.src = path.join(this.resources, data.meta.image); + image.onload = () => + { + const baseTexture = new PIXI.BaseTexture(image, null, 1); + const spritesheet = new PIXI.Spritesheet(baseTexture, data, uri); + + expect(data).to.be.an.object; + expect(data.meta.image).to.equal('building1@2x.png'); + expect(spritesheet.resolution).to.equal(2); + + this.validate(spritesheet, done); + }; + }); +}); diff --git a/test/core/Texture.js b/test/core/Texture.js new file mode 100644 index 0000000..8c4ef98 --- /dev/null +++ b/test/core/Texture.js @@ -0,0 +1,19 @@ +'use strict'; + +describe('PIXI.Texture', function () +{ + it('should register Texture from Loader', function () + { + const URL = 'foo.png'; + const NAME = 'bar'; + const image = new Image(); + + const texture = PIXI.Texture.fromLoader(image, URL, NAME); + + expect(texture.baseTexture.imageUrl).to.equal('foo.png'); + expect(PIXI.utils.TextureCache[NAME]).to.equal(texture); + expect(PIXI.utils.BaseTextureCache[NAME]).to.equal(texture.baseTexture); + expect(PIXI.utils.TextureCache[URL]).to.equal(texture); + expect(PIXI.utils.BaseTextureCache[URL]).to.equal(texture.baseTexture); + }); +}); diff --git a/test/core/WebGLRenderer.js b/test/core/WebGLRenderer.js index 9bc3bc2..3d6cf98 100644 --- a/test/core/WebGLRenderer.js +++ b/test/core/WebGLRenderer.js @@ -1,21 +1,21 @@ 'use strict'; -function isWebGLSupported(fn) -{ - return PIXI.utils.isWebGLSupported() ? fn : function () {}; // eslint-disable-line no-empty-function -} +const withGL = require('../withGL'); -describe('PIXI.WebGLRenderer', isWebGLSupported(function () +describe('PIXI.WebGLRenderer', function () { - it('setting option legacy should disable VAOs and set minimum SPRITE_MAX_TEXTURES to 1', function () + it('setting option legacy should disable VAOs and SPRITE_MAX_TEXTURES', withGL(function () { const renderer = new PIXI.WebGLRenderer(1, 1, { legacy: true }); - - expect(PIXI.glCore.VertexArrayObject.FORCE_NATIVE).to.equal(true); - expect(renderer.plugins.sprite.MAX_TEXTURES).to.equal(1); - - renderer.destroy(); - }); -})); - + try + { + expect(PIXI.glCore.VertexArrayObject.FORCE_NATIVE).to.equal(true); + expect(renderer.plugins.sprite.MAX_TEXTURES).to.equal(1); + } + finally + { + renderer.destroy(); + } + })); +}); diff --git a/test/core/filters.js b/test/core/filters.js new file mode 100644 index 0000000..df459f6 --- /dev/null +++ b/test/core/filters.js @@ -0,0 +1,21 @@ +'use strict'; + +describe('PIXI.filters', function () +{ + it('should correctly form uniformData', function () + { + const sprite = new PIXI.Sprite(PIXI.Texture.EMPTY); + const displ = new PIXI.filters.DisplacementFilter(sprite); + + expect(!!displ.uniformData.scale).to.be.true; + expect(!!displ.uniformData.filterMatrix).to.be.true; + expect(!!displ.uniformData.mapSampler).to.be.true; + // it does have filterClamp, but it is handled by FilterManager + expect(!!displ.uniformData.filterClamp).to.be.false; + + const fxaa = new PIXI.filters.FXAAFilter(); + + // it does have filterArea, but it is handled by FilterManager + expect(!!fxaa.uniformData.filterArea).to.be.false; + }); +}); diff --git a/test/core/index.js b/test/core/index.js index 63a8f21..8530131 100755 --- a/test/core/index.js +++ b/test/core/index.js @@ -7,6 +7,7 @@ require('./DisplayObject'); require('./getLocalBounds'); require('./Sprite'); +require('./Spritesheet'); require('./TilingSprite'); require('./TextStyle'); require('./Text'); @@ -25,3 +26,5 @@ require('./SpriteRenderer'); require('./WebGLRenderer'); require('./Ellipse'); +require('./Texture'); +require('./filters'); diff --git a/test/core/resources/building1.json b/test/core/resources/building1.json new file mode 100755 index 0000000..03fa5c1 --- /dev/null +++ b/test/core/resources/building1.json @@ -0,0 +1,21 @@ +{"frames": { + +"goldmine_10_5.png": +{ + "frame": {"x":1,"y":1,"w":95,"h":115}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":95,"h":115}, + "sourceSize": {"w":95,"h":115}, + "pivot": {"x":0.5,"y":0.5} +}}, +"meta": { + "app": "http://www.codeandweb.com/texturepacker", + "version": "1.0", + "image": "building1.png", + "format": "RGBA8888", + "size": {"w":128,"h":128}, + "scale": "0.5", + "smartupdate": "$TexturePacker:SmartUpdate:d7a5e54c8f8a3fecd508baf190c44807:99a0e3a4dc0f441e4aad77614191ab38:6046b8eb706ddefaa771c33ceb7cb6d5$" +} +} diff --git a/package.json b/package.json index 6a366ba..71ade15 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pixi.js", - "version": "4.3.4", + "version": "4.3.5", "description": "Pixi.js is a fast lightweight 2D library that works across all devices.", "author": "Mat Groves", "contributors": [ @@ -61,7 +61,7 @@ "ismobilejs": "^0.4.0", "object-assign": "^4.0.1", "pixi-gl-core": "^1.0.3", - "resource-loader": "^2.0.4" + "resource-loader": "^2.0.6" }, "devDependencies": { "babel-cli": "^6.18.0", @@ -70,9 +70,9 @@ "babel-preset-es2015": "^6.14.0", "babelify": "^7.3.0", "del": "^2.2.0", - "electron-prebuilt": "^1.3.2", + "electron": "^1.4.15", "eslint": "^3.5.0", - "floss": "^1.2.0", + "floss": "^2.0.1", "gh-pages": "^0.11.0", "jaguarjs-jsdoc": "^1.0.1", "js-md5": "^0.4.1", diff --git a/src/accessibility/AccessibilityManager.js b/src/accessibility/AccessibilityManager.js index df2716a..f16bcb1 100644 --- a/src/accessibility/AccessibilityManager.js +++ b/src/accessibility/AccessibilityManager.js @@ -3,7 +3,7 @@ import accessibleTarget from './accessibleTarget'; // add some extra variables to the container.. -Object.assign( +core.utils.mixins.delayMixin( core.DisplayObject.prototype, accessibleTarget ); diff --git a/src/core/Application.js b/src/core/Application.js index ea0f058..5340578 100644 --- a/src/core/Application.js +++ b/src/core/Application.js @@ -1,6 +1,6 @@ import { autoDetectRenderer } from './autoDetectRenderer'; import Container from './display/Container'; -import Ticker from './ticker/Ticker'; +import { shared, Ticker } from './ticker'; /** * Convenience class to create a new PIXI application. @@ -35,9 +35,9 @@ * @param {boolean} [noWebGL=false] - prevents selection of WebGL renderer, even if such is present * @param {boolean} [options.legacy=false] - If true Pixi will aim to ensure compatibility * with older / less advanced devices. If you experience unexplained flickering try setting this to true. - + * @param {boolean} [useSharedTicker=false] - `true` to use PIXI.ticker.shared, `false` to create new ticker. */ - constructor(width, height, options, noWebGL) + constructor(width, height, options, noWebGL, useSharedTicker = false) { /** * WebGL renderer if available, otherwise CanvasRenderer @@ -46,23 +46,46 @@ this.renderer = autoDetectRenderer(width, height, options, noWebGL); /** - * The root display container that's renderered. + * The root display container that's rendered. * @member {PIXI.Container} */ this.stage = new Container(); /** + * Internal reference to the ticker + * @member {PIXI.ticker.Ticker} + * @private + */ + this._ticker = null; + + /** * Ticker for doing render updates. * @member {PIXI.ticker.Ticker} + * @default PIXI.ticker.shared */ - this.ticker = new Ticker(); - - this.ticker.add(this.render, this); + this.ticker = useSharedTicker ? shared : new Ticker(); // Start the rendering this.start(); } + set ticker(ticker) // eslint-disable-line require-jsdoc + { + if (this._ticker) + { + this._ticker.remove(this.render, this); + } + this._ticker = ticker; + if (ticker) + { + ticker.add(this.render, this); + } + } + get ticker() // eslint-disable-line require-jsdoc + { + return this._ticker; + } + /** * Render the current stage. */ @@ -76,7 +99,7 @@ */ stop() { - this.ticker.stop(); + this._ticker.stop(); } /** @@ -84,7 +107,7 @@ */ start() { - this.ticker.start(); + this._ticker.start(); } /** @@ -98,13 +121,22 @@ } /** + * Reference to the renderer's screen rectangle. Its safe to use as filterArea or hitArea for whole screen + * @member {PIXI.Rectangle} + * @readonly + */ + get screen() + { + return this.renderer.screen; + } + + /** * Destroy and don't use after this. * @param {Boolean} [removeView=false] Automatically remove canvas from DOM. */ destroy(removeView) { this.stop(); - this.ticker.remove(this.render, this); this.ticker = null; this.stage.destroy(); diff --git a/src/core/Shader.js b/src/core/Shader.js new file mode 100644 index 0000000..78b17e6 --- /dev/null +++ b/src/core/Shader.js @@ -0,0 +1,46 @@ +import { GLShader } from 'pixi-gl-core'; +import settings from './settings'; + +function checkPrecision(src, def) +{ + if (src instanceof Array) + { + if (src[0].substring(0, 9) !== 'precision') + { + const copy = src.slice(0); + + copy.unshift(`precision ${def} float;`); + + return copy; + } + } + else if (src.substring(0, 9) !== 'precision') + { + return `precision ${def} float;\n${src}`; + } + + return src; +} + +/** + * Wrapper class, webGL Shader for Pixi. + * Adds precision string if vertexSrc or fragmentSrc have no mention of it. + * + * @class + * @extends GLShader + * @memberof PIXI + */ +export default class Shader extends GLShader +{ + /** + * + * @param {WebGLRenderingContext} gl - The current WebGL rendering context + * @param {string|string[]} vertexSrc - The vertex shader source as an array of strings. + * @param {string|string[]} fragmentSrc - The fragment shader source as an array of strings. + */ + constructor(gl, vertexSrc, fragmentSrc) + { + super(gl, checkPrecision(vertexSrc, settings.PRECISION_VERTEX), + checkPrecision(fragmentSrc, settings.PRECISION_FRAGMENT)); + } +} diff --git a/src/core/const.js b/src/core/const.js index ef6f662..3de0efe 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -188,7 +188,7 @@ * The gc modes that are supported by pixi. * * The {@link PIXI.settings.GC_MODE} Garbage Collection mode for pixi textures is AUTO - * If set to GC_MODE, the renderer will occasianally check textures usage. If they are not + * If set to GC_MODE, the renderer will occasionally check textures usage. If they are not * used for a specified period of time they will be removed from the GPU. They will of course * be uploaded again when they are required. This is a silent behind the scenes process that * should ensure that the GPU does not get filled up. diff --git a/src/core/display/Container.js b/src/core/display/Container.js index ed8cd5f..17fde8d 100644 --- a/src/core/display/Container.js +++ b/src/core/display/Container.js @@ -73,11 +73,12 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.push(child); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -108,11 +109,12 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.splice(index, 0, child); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -225,10 +227,11 @@ if (index === -1) return null; child.parent = null; + // ensure child transform will be recalculated + child.transform._parentID = -1; removeItems(this.children, index, 1); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -249,11 +252,12 @@ { const child = this.getChildAt(index); + // ensure child transform will be recalculated.. child.parent = null; + child.transform._parentID = -1; removeItems(this.children, index, 1); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -284,13 +288,12 @@ for (let i = 0; i < removed.length; ++i) { removed[i].parent = null; + if (removed[i].transform) + { + removed[i].transform._parentID = -1; + } } - // ensure a transform will be recalculated.. - if (this.transform) - { - this.transform._parentID = -1; - } this._boundsID++; this.onChildrenChange(beginIndex); diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index f37f2f5..be4c4fb 100644 --- a/src/core/graphics/Graphics.js +++ b/src/core/graphics/Graphics.js @@ -28,8 +28,9 @@ { /** * + * @param {boolean} [nativeLines=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP */ - constructor() + constructor(nativeLines = false) { super(); @@ -50,6 +51,13 @@ this.lineWidth = 0; /** + * If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * + * @member {boolean} + */ + this.nativeLines = nativeLines; + + /** * The color of any lines drawn. * * @member {string} @@ -757,28 +765,14 @@ if (!this._spriteRect) { - if (!Graphics._SPRITE_TEXTURE) - { - Graphics._SPRITE_TEXTURE = RenderTexture.create(10, 10); - - const canvas = document.createElement('canvas'); - - canvas.width = 10; - canvas.height = 10; - - const context = canvas.getContext('2d'); - - context.fillStyle = 'white'; - context.fillRect(0, 0, 10, 10); - - Graphics._SPRITE_TEXTURE = Texture.fromCanvas(canvas); - } - - this._spriteRect = new Sprite(Graphics._SPRITE_TEXTURE); + this._spriteRect = new Sprite(new Texture(Texture.WHITE)); } + + const sprite = this._spriteRect; + if (this.tint === 0xffffff) { - this._spriteRect.tint = this.graphicsData[0].fillColor; + sprite.tint = this.graphicsData[0].fillColor; } else { @@ -792,20 +786,21 @@ t1[1] *= t2[1]; t1[2] *= t2[2]; - this._spriteRect.tint = rgb2hex(t1); + sprite.tint = rgb2hex(t1); } - this._spriteRect.alpha = this.graphicsData[0].fillAlpha; - this._spriteRect.worldAlpha = this.worldAlpha * this._spriteRect.alpha; + sprite.alpha = this.graphicsData[0].fillAlpha; + sprite.worldAlpha = this.worldAlpha * sprite.alpha; + sprite.blendMode = this.blendMode; - Graphics._SPRITE_TEXTURE._frame.width = rect.width; - Graphics._SPRITE_TEXTURE._frame.height = rect.height; + sprite.texture._frame.width = rect.width; + sprite.texture._frame.height = rect.height; - this._spriteRect.transform.worldTransform = this.transform.worldTransform; + sprite.transform.worldTransform = this.transform.worldTransform; - this._spriteRect.anchor.set(-rect.x / rect.width, -rect.y / rect.height); - this._spriteRect._onAnchorUpdate(); + sprite.anchor.set(-rect.x / rect.width, -rect.y / rect.height); + sprite._onAnchorUpdate(); - this._spriteRect._renderWebGL(renderer); + sprite._renderWebGL(renderer); } /** @@ -1031,6 +1026,7 @@ this.fillColor, this.fillAlpha, this.filling, + this.nativeLines, shape ); diff --git a/src/core/graphics/GraphicsData.js b/src/core/graphics/GraphicsData.js index f5a87ae..b6f03c0 100644 --- a/src/core/graphics/GraphicsData.js +++ b/src/core/graphics/GraphicsData.js @@ -14,14 +14,19 @@ * @param {number} fillColor - the color of the fill * @param {number} fillAlpha - the alpha of the fill * @param {boolean} fill - whether or not the shape is filled with a colour + * @param {boolean} nativeLines - the method for drawing lines * @param {PIXI.Circle|PIXI.Rectangle|PIXI.Ellipse|PIXI.Polygon} shape - The shape object to draw. */ - constructor(lineWidth, lineColor, lineAlpha, fillColor, fillAlpha, fill, shape) + constructor(lineWidth, lineColor, lineAlpha, fillColor, fillAlpha, fill, nativeLines, shape) { /** * @member {number} the width of the line to draw */ this.lineWidth = lineWidth; + /** + * @member {boolean} if true the liens will be draw using LINES instead of TRIANGLE_STRIP + */ + this.nativeLines = nativeLines; /** * @member {number} the color of the line to draw @@ -85,6 +90,7 @@ this.fillColor, this.fillAlpha, this.fill, + this.nativeLines, this.shape ); } diff --git a/src/core/graphics/webgl/GraphicsRenderer.js b/src/core/graphics/webgl/GraphicsRenderer.js index 14083fe..67f5e92 100644 --- a/src/core/graphics/webgl/GraphicsRenderer.js +++ b/src/core/graphics/webgl/GraphicsRenderer.js @@ -102,7 +102,15 @@ shaderTemp.uniforms.alpha = graphics.worldAlpha; renderer.bindVao(webGLData.vao); - webGLData.vao.draw(gl.TRIANGLE_STRIP, webGLData.indices.length); + + if (graphics.nativeLines) + { + gl.drawArrays(gl.LINES, 0, webGLData.points.length / 6); + } + else + { + webGLData.vao.draw(gl.TRIANGLE_STRIP, webGLData.indices.length); + } } } diff --git a/src/core/graphics/webgl/utils/buildLine.js b/src/core/graphics/webgl/utils/buildLine.js index ecf43f7..4d6862e 100644 --- a/src/core/graphics/webgl/utils/buildLine.js +++ b/src/core/graphics/webgl/utils/buildLine.js @@ -11,7 +11,29 @@ * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties * @param {object} webGLData - an object containing all the webGL-specific information to create this shape */ -export default function buildLine(graphicsData, webGLData) +export default function (graphicsData, webGLData) +{ + if (graphicsData.nativeLines) + { + buildNativeLine(graphicsData, webGLData); + } + else + { + buildLine(graphicsData, webGLData); + } +} + +/** + * Builds a line to draw using the poligon method. + * + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @private + * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties + * @param {object} webGLData - an object containing all the webGL-specific information to create this shape + */ +function buildLine(graphicsData, webGLData) { // TODO OPTIMISE! let points = graphicsData.points; @@ -224,3 +246,46 @@ indices.push(indexStart - 1); } + +/** + * Builds a line to draw using the gl.drawArrays(gl.LINES) method + * + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @private + * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties + * @param {object} webGLData - an object containing all the webGL-specific information to create this shape + */ +function buildNativeLine(graphicsData, webGLData) +{ + let i = 0; + const points = graphicsData.points; + + if (points.length === 0) return; + + const verts = webGLData.points; + const length = points.length / 2; + + // sort color + const color = hex2rgb(graphicsData.lineColor); + const alpha = graphicsData.lineAlpha; + const r = color[0] * alpha; + const g = color[1] * alpha; + const b = color[2] * alpha; + + for (i = 1; i < length; i++) + { + const p1x = points[(i - 1) * 2]; + const p1y = points[((i - 1) * 2) + 1]; + + const p2x = points[i * 2]; + const p2y = points[(i * 2) + 1]; + + verts.push(p1x, p1y); + verts.push(r, g, b, alpha); + + verts.push(p2x, p2y); + verts.push(r, g, b, alpha); + } +} diff --git a/src/core/graphics/webgl/utils/buildRoundedRectangle.js b/src/core/graphics/webgl/utils/buildRoundedRectangle.js index 9abc807..a6c8461 100644 --- a/src/core/graphics/webgl/utils/buildRoundedRectangle.js +++ b/src/core/graphics/webgl/utils/buildRoundedRectangle.js @@ -77,6 +77,26 @@ } /** + * Calculate a single point for a quadratic bezier curve. + * Utility function used by quadraticBezierCurve. + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @private + * @param {number} n1 - first number + * @param {number} n2 - second number + * @param {number} perc - percentage + * @return {number} the result + * + */ +function getPt(n1, n2, perc) +{ + const diff = n2 - n1; + + return n1 + (diff * perc); +} + +/** * Calculate the points for a quadratic bezier curve. (helper function..) * Based on: https://stackoverflow.com/questions/785097/how-do-i-implement-a-bezier-curve-in-c * @@ -105,13 +125,6 @@ let x = 0; let y = 0; - function getPt(n1, n2, perc) - { - const diff = n2 - n1; - - return n1 + (diff * perc); - } - for (let i = 0, j = 0; i <= n; ++i) { j = i / n; diff --git a/src/core/index.js b/src/core/index.js index f7a817c..4f414e8 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -30,6 +30,7 @@ export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; export { default as CanvasGraphicsRenderer } from './graphics/canvas/CanvasGraphicsRenderer'; +export { default as Spritesheet } from './textures/Spritesheet'; export { default as Texture } from './textures/Texture'; export { default as BaseTexture } from './textures/BaseTexture'; export { default as RenderTexture } from './textures/RenderTexture'; diff --git a/src/core/math/Matrix.js b/src/core/math/Matrix.js index 0b77477..3679860 100644 --- a/src/core/math/Matrix.js +++ b/src/core/math/Matrix.js @@ -13,45 +13,50 @@ export default class Matrix { /** - * + * @param {number} [a=1] - x scale + * @param {number} [b=0] - y skew + * @param {number} [c=0] - x skew + * @param {number} [d=1] - y scale + * @param {number} [tx=0] - x translation + * @param {number} [ty=0] - y translation */ - constructor() + constructor(a = 1, b = 0, c = 0, d = 1, tx = 0, ty = 0) { /** * @member {number} * @default 1 */ - this.a = 1; + this.a = a; /** * @member {number} * @default 0 */ - this.b = 0; + this.b = b; /** * @member {number} * @default 0 */ - this.c = 0; + this.c = c; /** * @member {number} * @default 1 */ - this.d = 1; + this.d = d; /** * @member {number} * @default 0 */ - this.tx = 0; + this.tx = tx; /** * @member {number} * @default 0 */ - this.ty = 0; + this.ty = ty; this.array = null; } diff --git a/src/core/renderers/SystemRenderer.js b/src/core/renderers/SystemRenderer.js index fa116a6..a24c775 100644 --- a/src/core/renderers/SystemRenderer.js +++ b/src/core/renderers/SystemRenderer.js @@ -1,5 +1,5 @@ import { sayHello, hex2string, hex2rgb } from '../utils'; -import { Matrix } from '../math'; +import { Matrix, Rectangle } from '../math'; import { RENDERER_TYPE } from '../const'; import settings from '../settings'; import Container from '../display/Container'; @@ -21,8 +21,8 @@ { /** * @param {string} system - The name of the system this renderer is for. - * @param {number} [width=800] - the width of the canvas view - * @param {number} [height=600] - the height of the canvas view + * @param {number} [screenWidth=800] - the width of the screen + * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false @@ -37,7 +37,7 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(system, width, height, options) + constructor(system, screenWidth, screenHeight, options) { super(); @@ -69,20 +69,13 @@ this.type = RENDERER_TYPE.UNKNOWN; /** - * The width of the canvas view + * Measurements of the screen. (0, 0, screenWidth, screenHeight) * - * @member {number} - * @default 800 - */ - this.width = width || 800; - - /** - * The height of the canvas view + * Its safe to use as filterArea or hitArea for whole stage * - * @member {number} - * @default 600 + * @member {PIXI.Rectangle} */ - this.height = height || 600; + this.screen = new Rectangle(0, 0, screenWidth || 800, screenHeight || 600); /** * The canvas element that everything is drawn to @@ -107,7 +100,7 @@ this.transparent = options.transparent; /** - * Whether the render view should be resized automatically + * Whether css dimensions of canvas view should be resized to screen dimensions automatically * * @member {boolean} */ @@ -192,23 +185,48 @@ } /** - * Resizes the canvas view to the specified width and height + * Same as view.width, actual number of pixels in the canvas by horizontal * - * @param {number} width - the new width of the canvas view - * @param {number} height - the new height of the canvas view + * @member {number} + * @readonly + * @default 800 */ - resize(width, height) + get width() { - this.width = width * this.resolution; - this.height = height * this.resolution; + return this.view.width; + } - this.view.width = this.width; - this.view.height = this.height; + /** + * Same as view.height, actual number of pixels in the canvas by vertical + * + * @member {number} + * @readonly + * @default 600 + */ + get height() + { + return this.view.height; + } + + /** + * Resizes the screen and canvas to the specified width and height + * Canvas dimensions are multiplied by resolution + * + * @param {number} screenWidth - the new width of the screen + * @param {number} screenHeight - the new height of the screen + */ + resize(screenWidth, screenHeight) + { + this.screen.width = screenWidth; + this.screen.height = screenHeight; + + this.view.width = screenWidth * this.resolution; + this.view.height = screenHeight * this.resolution; if (this.autoResize) { - this.view.style.width = `${this.width / this.resolution}px`; - this.view.style.height = `${this.height / this.resolution}px`; + this.view.style.width = `${screenWidth}px`; + this.view.style.height = `${screenHeight}px`; } } @@ -249,11 +267,10 @@ this.type = RENDERER_TYPE.UNKNOWN; - this.width = 0; - this.height = 0; - this.view = null; + this.screen = null; + this.resolution = 0; this.transparent = false; diff --git a/src/core/renderers/canvas/CanvasRenderer.js b/src/core/renderers/canvas/CanvasRenderer.js index 1bd2d5e..e84e49c 100644 --- a/src/core/renderers/canvas/CanvasRenderer.js +++ b/src/core/renderers/canvas/CanvasRenderer.js @@ -18,8 +18,8 @@ export default class CanvasRenderer extends SystemRenderer { /** - * @param {number} [width=800] - the width of the canvas view - * @param {number} [height=600] - the height of the canvas view + * @param {number} [screenWidth=800] - the width of the screen + * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false @@ -34,9 +34,9 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(width, height, options = {}) + constructor(screenWidth, screenHeight, options = {}) { - super('Canvas', width, height, options); + super('Canvas', screenWidth, screenHeight, options); this.type = RENDERER_TYPE.CANVAS; @@ -96,7 +96,7 @@ this.context = null; this.renderingToScreen = false; - this.resize(width, height); + this.resize(screenWidth, screenHeight); } /** @@ -283,12 +283,12 @@ * * @extends PIXI.SystemRenderer#resize * - * @param {number} width - The new width of the canvas view - * @param {number} height - The new height of the canvas view + * @param {number} screenWidth - the new width of the screen + * @param {number} screenHeight - the new height of the screen */ - resize(width, height) + resize(screenWidth, screenHeight) { - super.resize(width, height); + super.resize(screenWidth, screenHeight); // reset the scale mode.. oddly this seems to be reset when the canvas is resized. // surely a browser bug?? Let pixi fix that for you.. diff --git a/src/core/renderers/webgl/TextureManager.js b/src/core/renderers/webgl/TextureManager.js index b685c1a..8227289 100644 --- a/src/core/renderers/webgl/TextureManager.js +++ b/src/core/renderers/webgl/TextureManager.js @@ -61,7 +61,7 @@ * Updates and/or Creates a WebGL texture for the renderer's context. * * @param {PIXI.BaseTexture|PIXI.Texture} texture - the texture to update - * @param {Number} location - the location the texture will be bound to. + * @param {number} location - the location the texture will be bound to. * @return {GLTexture} The gl texture. */ updateTexture(texture, location) diff --git a/src/core/renderers/webgl/WebGLRenderer.js b/src/core/renderers/webgl/WebGLRenderer.js index e4cd8a0..830b382 100644 --- a/src/core/renderers/webgl/WebGLRenderer.js +++ b/src/core/renderers/webgl/WebGLRenderer.js @@ -31,8 +31,8 @@ { /** * - * @param {number} [width=0] - the width of the canvas view - * @param {number} [height=0] - the height of the canvas view + * @param {number} [screenWidth=800] - the width of the screen + * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false @@ -53,9 +53,9 @@ * @param {boolean} [options.legacy=false] - If true Pixi will aim to ensure compatibility * with older / less advanced devices. If you experiance unexplained flickering try setting this to true. */ - constructor(width, height, options = {}) + constructor(screenWidth, screenHeight, options = {}) { - super('WebGL', width, height, options); + super('WebGL', screenWidth, screenHeight, options); this.legacy = !!options.legacy; @@ -245,7 +245,7 @@ glCore._testingContext = gl; // setup the width/height properties and gl viewport - this.resize(this.width, this.height); + this.resize(this.screen.width, this.screen.height); } /** @@ -343,16 +343,16 @@ /** * Resizes the webGL view to the specified width and height. * - * @param {number} width - the new width of the webGL view - * @param {number} height - the new height of the webGL view + * @param {number} screenWidth - the new width of the screen + * @param {number} screenHeight - the new height of the screen */ - resize(width, height) + resize(screenWidth, screenHeight) { // if(width * this.resolution === this.width && height * this.resolution === this.height)return; - SystemRenderer.prototype.resize.call(this, width, height); + SystemRenderer.prototype.resize.call(this, screenWidth, screenHeight); - this.rootRenderTarget.resize(width, height); + this.rootRenderTarget.resize(screenWidth, screenHeight); if (this._activeRenderTarget === this.rootRenderTarget) { @@ -396,6 +396,26 @@ } /** + * Erases the render texture and fills the drawing area with a colour + * + * @param {PIXI.RenderTexture} renderTexture - The render texture to clear + * @param {number} [clearColor] - The colour + * @return {PIXI.WebGLRenderer} Returns itself. + */ + clearRenderTexture(renderTexture, clearColor) + { + const baseTexture = renderTexture.baseTexture; + const renderTarget = baseTexture._glRenderTargets[this.CONTEXT_UID]; + + if (renderTarget) + { + renderTarget.clear(clearColor); + } + + return this; + } + + /** * Binds a render texture for rendering * * @param {PIXI.RenderTexture} renderTexture - The render texture to render @@ -474,9 +494,10 @@ * Changes the current GLShader to the one given in parameter * * @param {PIXI.glCore.Shader} shader - the new glShader + * @param {boolean} [autoProject=true] - Whether automatically set the projection matrix * @return {PIXI.WebGLRenderer} Returns itself. */ - _bindGLShader(shader) + _bindGLShader(shader, autoProject) { // TODO cache if (this._activeShader !== shader) @@ -484,8 +505,14 @@ this._activeShader = shader; shader.bind(); - // automatically set the projection matrix - shader.uniforms.projectionMatrix = this._activeRenderTarget.projectionMatrix.toArray(true); + // `autoProject` normally would be a default parameter set to true + // but because of how Babel transpiles default parameters + // it hinders the performance of this method. + if (autoProject !== false) + { + // automatically set the projection matrix + shader.uniforms.projectionMatrix = this._activeRenderTarget.projectionMatrix.toArray(true); + } } return this; diff --git a/src/core/renderers/webgl/WebGLState.js b/src/core/renderers/webgl/WebGLState.js index ba6ecc5..6c846d5 100755 --- a/src/core/renderers/webgl/WebGLState.js +++ b/src/core/renderers/webgl/WebGLState.js @@ -82,18 +82,20 @@ push() { // next state.. - let state = this.stack[++this.stackIndex]; + let state = this.stack[this.stackIndex]; if (!state) { state = this.stack[this.stackIndex] = new Uint8Array(16); } + ++this.stackIndex; + // copy state.. // set active state so we can force overrides of gl state for (let i = 0; i < this.activeState.length; i++) { - this.activeState[i] = state[i]; + state[i] = this.activeState[i]; } } diff --git a/src/core/renderers/webgl/filters/Filter.js b/src/core/renderers/webgl/filters/Filter.js index 2c3eaad..4eb6128 100644 --- a/src/core/renderers/webgl/filters/Filter.js +++ b/src/core/renderers/webgl/filters/Filter.js @@ -9,7 +9,7 @@ * @memberof PIXI * @extends PIXI.Shader */ -class Filter extends Shader +export default class Filter extends Shader { /** * @param {string} [vertexSrc] - The source of the vertex shader. @@ -56,15 +56,16 @@ * @param {PIXI.RenderTarget} input - The input render target. * @param {PIXI.RenderTarget} output - The target to output to. * @param {boolean} clear - Should the output be cleared before rendering to it + * @param {object} [currentState] - It's current state of filter. + * There are some useful properties in the currentState : + * target, filters, sourceFrame, destinationFrame, renderTarget, resolution */ - apply(filterManager, input, output, clear) + apply(filterManager, input, output, clear, currentState) // eslint-disable-line no-unused-vars { // do as you please! - filterManager.applyFilter(this, input, output, clear); + filterManager.applyFilter(this, input, output, clear, currentState); // or just do a regular render.. } } - -export default Filter; diff --git a/src/core/renderers/webgl/filters/extractUniformsFromSrc.js b/src/core/renderers/webgl/filters/extractUniformsFromSrc.js index d78a0b1..4893214 100644 --- a/src/core/renderers/webgl/filters/extractUniformsFromSrc.js +++ b/src/core/renderers/webgl/filters/extractUniformsFromSrc.js @@ -12,7 +12,7 @@ function extractUniformsFromString(string) { - const maskRegex = new RegExp('^(projectionMatrix|uSampler|filterArea)$'); + const maskRegex = new RegExp('^(projectionMatrix|uSampler|filterArea|filterClamp)$'); const uniforms = {}; let nameSplit; diff --git a/src/core/renderers/webgl/managers/FilterManager.js b/src/core/renderers/webgl/managers/FilterManager.js index 43fc869..6c7422d 100644 --- a/src/core/renderers/webgl/managers/FilterManager.js +++ b/src/core/renderers/webgl/managers/FilterManager.js @@ -2,6 +2,7 @@ import RenderTarget from '../utils/RenderTarget'; import Quad from '../utils/Quad'; import { Rectangle } from '../../../math'; +import Shader from '../../../Shader'; import * as filterTransforms from '../filters/filterTransforms'; import bitTwiddle from 'bit-twiddle'; @@ -22,8 +23,6 @@ this.filters = []; this.target = null; this.resolution = 1; - - this.firstRun = true; } } @@ -155,7 +154,7 @@ if (filters.length === 1) { - filters[0].apply(this, currentState.renderTarget, lastState.renderTarget, false); + filters[0].apply(this, currentState.renderTarget, lastState.renderTarget, false, currentState); this.freePotRenderTarget(currentState.renderTarget); } else @@ -177,7 +176,7 @@ for (i = 0; i < filters.length - 1; ++i) { - filters[i].apply(this, flip, flop, true); + filters[i].apply(this, flip, flop, true, currentState); const t = flip; @@ -185,7 +184,7 @@ flop = t; } - filters[i].apply(this, flip, lastState.renderTarget, false); + filters[i].apply(this, flip, lastState.renderTarget, false, currentState); this.freePotRenderTarget(flip); this.freePotRenderTarget(flop); @@ -212,16 +211,28 @@ const renderer = this.renderer; const gl = renderer.gl; - renderer.bindShader(filter, true); + let shader = filter.glShaders[renderer.CONTEXT_UID]; - // TODO theres a better way! - // quad can be a mesh and we then should be able to pull this off without accessing the shader directly - const shader = renderer.shaderManager.getGLShader(); - - // TODO change quad to mesh.. - if (!this.firstRun) + // cacheing.. + if (!shader) { - this.firstRun = true; + if (filter.glShaderKey) + { + shader = this.shaderCache[filter.glShaderKey]; + + if (!shader) + { + shader = new Shader(this.gl, filter.vertexSrc, filter.fragmentSrc); + + filter.glShaders[renderer.CONTEXT_UID] = this.shaderCache[filter.glShaderKey] = shader; + } + } + else + { + shader = filter.glShaders[renderer.CONTEXT_UID] = new Shader(this.gl, filter.vertexSrc, filter.fragmentSrc); + } + + // TODO - this only needs to be done once? renderer.bindVao(null); this.quad.initVao(shader); @@ -244,58 +255,146 @@ renderer.maskManager.pushScissorMask(null, renderer.maskManager.scissorData); } - // renderer._bindGLShader(shader); + renderer.bindShader(shader); - let currentState; - const uniforms = filter.uniforms; + // free unit 0 for us, doesn't matter what was there + // don't try to restore it, because syncUniforms can upload it to another slot + // and it'll be a problem + const tex = this.renderer.emptyTextures[0]; - if (uniforms.filterArea) - { - currentState = this.filterData.stack[this.filterData.index]; - - const filterArea = uniforms.filterArea; - - filterArea[0] = currentState.renderTarget.size.width; - filterArea[1] = currentState.renderTarget.size.height; - filterArea[2] = currentState.sourceFrame.x; - filterArea[3] = currentState.sourceFrame.y; - - filter.uniforms.filterArea = filterArea; - } - - // use this to clamp displaced texture coords so they belong to filterArea - // see displacementFilter fragment shader for an example - if (uniforms.filterClamp) - { - currentState = this.filterData.stack[this.filterData.index]; - - const filterClamp = uniforms.filterClamp; - - filterClamp[0] = 0; - filterClamp[1] = 0; - filterClamp[2] = (currentState.sourceFrame.width - 1) / currentState.renderTarget.size.width; - filterClamp[3] = (currentState.sourceFrame.height - 1) / currentState.renderTarget.size.height; - - filter.uniforms.filterClamp = filterClamp; - } - - renderer.shaderManager.setUniforms(filter.uniforms); + this.renderer.boundTextures[0] = tex; + // this syncs the pixi filters uniforms with glsl uniforms + this.syncUniforms(shader, filter); renderer.state.setBlendMode(filter.blendMode); - // temporary bypass cache.. - const tex = this.renderer.boundTextures[0]; - gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, input.texture.texture); this.quad.vao.draw(this.renderer.gl.TRIANGLES, 6, 0); - // restore cache. gl.bindTexture(gl.TEXTURE_2D, tex._glTextures[this.renderer.CONTEXT_UID].texture); } /** + * Uploads the uniforms of the filter. + * + * @param {GLShader} shader - The underlying gl shader. + * @param {PIXI.Filter} filter - The filter we are synchronizing. + */ + syncUniforms(shader, filter) + { + const uniformData = filter.uniformData; + const uniforms = filter.uniforms; + + // 0 is reserved for the pixi texture so we start at 1! + let textureCount = 1; + let currentState; + + // filterArea and filterClamp that are handled by FilterManager directly + // they must not appear in uniformData + + if (shader.uniforms.filterArea) + { + currentState = this.filterData.stack[this.filterData.index]; + + const filterArea = shader.uniforms.filterArea; + + filterArea[0] = currentState.renderTarget.size.width; + filterArea[1] = currentState.renderTarget.size.height; + filterArea[2] = currentState.sourceFrame.x; + filterArea[3] = currentState.sourceFrame.y; + + shader.uniforms.filterArea = filterArea; + } + + // use this to clamp displaced texture coords so they belong to filterArea + // see displacementFilter fragment shader for an example + if (shader.uniforms.filterClamp) + { + currentState = currentState || this.filterData.stack[this.filterData.index]; + + const filterClamp = shader.uniforms.filterClamp; + + filterClamp[0] = 0; + filterClamp[1] = 0; + filterClamp[2] = (currentState.sourceFrame.width - 1) / currentState.renderTarget.size.width; + filterClamp[3] = (currentState.sourceFrame.height - 1) / currentState.renderTarget.size.height; + + shader.uniforms.filterClamp = filterClamp; + } + + // TODO Cacheing layer.. + for (const i in uniformData) + { + if (uniformData[i].type === 'sampler2D' && uniforms[i] !== 0) + { + if (uniforms[i].baseTexture) + { + shader.uniforms[i] = this.renderer.bindTexture(uniforms[i].baseTexture, textureCount); + } + else + { + shader.uniforms[i] = textureCount; + + // TODO + // this is helpful as renderTargets can also be set. + // Although thinking about it, we could probably + // make the filter texture cache return a RenderTexture + // rather than a renderTarget + const gl = this.renderer.gl; + + this.renderer.boundTextures[textureCount] = this.renderer.emptyTextures[textureCount]; + gl.activeTexture(gl.TEXTURE0 + textureCount); + + uniforms[i].texture.bind(); + } + + textureCount++; + } + else if (uniformData[i].type === 'mat3') + { + // check if its pixi matrix.. + if (uniforms[i].a !== undefined) + { + shader.uniforms[i] = uniforms[i].toArray(true); + } + else + { + shader.uniforms[i] = uniforms[i]; + } + } + else if (uniformData[i].type === 'vec2') + { + // check if its a point.. + if (uniforms[i].x !== undefined) + { + const val = shader.uniforms[i] || new Float32Array(2); + + val[0] = uniforms[i].x; + val[1] = uniforms[i].y; + shader.uniforms[i] = val; + } + else + { + shader.uniforms[i] = uniforms[i]; + } + } + else if (uniformData[i].type === 'float') + { + if (shader.uniforms.data[i].value !== uniformData[i]) + { + shader.uniforms[i] = uniforms[i]; + } + } + else + { + shader.uniforms[i] = uniforms[i]; + } + } + } + + /** * Gets a render target from the pool, or creates a new one. * * @param {boolean} clear - Should we clear the render texture when we get it? @@ -442,7 +541,6 @@ renderTarget.resolution = resolution; renderTarget.defaultFrame.width = renderTarget.size.width = minWidth / resolution; renderTarget.defaultFrame.height = renderTarget.size.height = minHeight / resolution; - renderTarget.attachStencilBuffer(); return renderTarget; } diff --git a/src/core/settings.js b/src/core/settings.js index bb08cb0..f4dd087 100644 --- a/src/core/settings.js +++ b/src/core/settings.js @@ -77,11 +77,11 @@ * * @static * @memberof PIXI.settings - * @type {RegExp|string} + * @type {RegExp} * @example `@2x` - * @default /@(.+)x/ + * @default /@([0-9\.]+)x/ */ - RETINA_PREFIX: /@(.+)x/, + RETINA_PREFIX: /@([0-9\.]+)x/, /** * The default render options if none are supplied to {@link PIXI.WebGLRenderer} @@ -175,14 +175,24 @@ SCALE_MODE: 0, /** - * Default specify float precision in shaders. + * Default specify float precision in vertex shader. + * + * @static + * @memberof PIXI.settings + * @type {PIXI.PRECISION} + * @default PIXI.PRECISION.HIGH + */ + PRECISION_VERTEX: 'highp', + + /** + * Default specify float precision in fragment shader. * * @static * @memberof PIXI.settings * @type {PIXI.PRECISION} * @default PIXI.PRECISION.MEDIUM */ - PRECISION: 'mediump', + PRECISION_FRAGMENT: 'mediump', /** * Can we upload the same buffer in a single frame? diff --git a/src/core/sprites/Sprite.js b/src/core/sprites/Sprite.js index 28b0b18..121bf58 100644 --- a/src/core/sprites/Sprite.js +++ b/src/core/sprites/Sprite.js @@ -212,11 +212,11 @@ } else { - w0 = orig.width * (1 - anchor._x); - w1 = orig.width * -anchor._x; + w1 = -anchor._x * orig.width; + w0 = w1 + orig.width; - h0 = orig.height * (1 - anchor._y); - h1 = orig.height * -anchor._y; + h1 = -anchor._y * orig.height; + h0 = h1 + orig.height; } // xy @@ -269,11 +269,11 @@ const tx = wt.tx; const ty = wt.ty; - const w0 = (orig.width) * (1 - anchor._x); - const w1 = (orig.width) * -anchor._x; + const w1 = -anchor._x * orig.width; + const w0 = w1 + orig.width; - const h0 = orig.height * (1 - anchor._y); - const h1 = orig.height * -anchor._y; + const h1 = -anchor._y * orig.height; + const h0 = h1 + orig.height; // xy vertexData[0] = (a * w1) + (c * h1) + tx; @@ -440,7 +440,7 @@ * * @static * @param {number|string|PIXI.BaseTexture|HTMLCanvasElement|HTMLVideoElement} source Source to create texture from - * @return {PIXI.Sprite} The newly created texture + * @return {PIXI.Sprite} The newly created sprite */ static from(source) { @@ -538,8 +538,8 @@ } /** - * The tint applied to the sprite. This is a hex value. A value of - * 0xFFFFFF will remove any tint effect. + * The tint applied to the sprite. This is a hex value. + * A value of 0xFFFFFF will remove any tint effect. * * @member {number} * @default 0xFFFFFF diff --git a/src/core/sprites/canvas/CanvasTinter.js b/src/core/sprites/canvas/CanvasTinter.js index 1cf316b..27e5e0c 100644 --- a/src/core/sprites/canvas/CanvasTinter.js +++ b/src/core/sprites/canvas/CanvasTinter.js @@ -75,8 +75,8 @@ crop.width *= resolution; crop.height *= resolution; - canvas.width = crop.width; - canvas.height = crop.height; + canvas.width = Math.ceil(crop.width); + canvas.height = Math.ceil(crop.height); context.fillStyle = `#${(`00000${(color | 0).toString(16)}`).substr(-6)}`; @@ -130,8 +130,8 @@ crop.width *= resolution; crop.height *= resolution; - canvas.width = crop.width; - canvas.height = crop.height; + canvas.width = Math.ceil(crop.width); + canvas.height = Math.ceil(crop.height); context.globalCompositeOperation = 'copy'; context.fillStyle = `#${(`00000${(color | 0).toString(16)}`).substr(-6)}`; @@ -172,8 +172,8 @@ crop.width *= resolution; crop.height *= resolution; - canvas.width = crop.width; - canvas.height = crop.height; + canvas.width = Math.ceil(crop.width); + canvas.height = Math.ceil(crop.height); context.globalCompositeOperation = 'copy'; context.drawImage( diff --git a/src/core/sprites/webgl/SpriteRenderer.js b/src/core/sprites/webgl/SpriteRenderer.js index 2affcd5..7c352e2 100644 --- a/src/core/sprites/webgl/SpriteRenderer.js +++ b/src/core/sprites/webgl/SpriteRenderer.js @@ -30,7 +30,7 @@ /** * Number of values sent in the vertex buffer. - * positionX, positionY, colorR, colorG, colorB = 5 + * aVertexPosition(2), aTextureCoord(1), aColor(1), aTextureId(1) = 5 * * @member {number} */ @@ -75,7 +75,6 @@ this.shader = null; this.currentIndex = 0; - TICK = 0; this.groups = []; for (let k = 0; k < this.size; k++) diff --git a/src/core/textures/BaseTexture.js b/src/core/textures/BaseTexture.js index 853de7e..aeccf39 100644 --- a/src/core/textures/BaseTexture.js +++ b/src/core/textures/BaseTexture.js @@ -232,16 +232,24 @@ this.realWidth = this.source.naturalWidth || this.source.videoWidth || this.source.width; this.realHeight = this.source.naturalHeight || this.source.videoHeight || this.source.height; - this.width = this.realWidth / this.resolution; - this.height = this.realHeight / this.resolution; - - this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); + this._updateDimensions(); } this.emit('update', this); } /** + * Update dimensions from real values + */ + _updateDimensions() + { + this.width = this.realWidth / this.resolution; + this.height = this.realHeight / this.resolution; + + this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); + } + + /** * Load a source. * * If the source is not-immediately-available, such as an image that needs to be @@ -525,11 +533,7 @@ this.realWidth = Math.round(svgWidth * this.sourceScale); this.realHeight = Math.round(svgHeight * this.sourceScale); - this.width = this.realWidth / this.resolution; - this.height = this.realHeight / this.resolution; - - // Check pow2 after scale - this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); + this._updateDimensions(); // Create a canvas element const canvas = document.createElement('canvas'); @@ -690,4 +694,52 @@ return baseTexture; } + + /** + * Helper function that creates a base texture based on the source you provide. + * The source can be - image url, image element, canvas element. + * + * @static + * @param {string|HTMLImageElement|HTMLCanvasElement} source - The source to create base texture from. + * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values + * @param {number} [sourceScale=(auto)] - Scale for the original image, used with Svg images. + * @return {PIXI.BaseTexture} The new base texture. + */ + static from(source, scaleMode, sourceScale) + { + if (typeof source === 'string') + { + return BaseTexture.fromImage(source, undefined, scaleMode, sourceScale); + } + else if (source instanceof HTMLImageElement) + { + const imageUrl = source.src; + let baseTexture = BaseTextureCache[imageUrl]; + + if (!baseTexture) + { + baseTexture = new BaseTexture(source, scaleMode); + baseTexture.imageUrl = imageUrl; + + if (sourceScale) + { + baseTexture.sourceScale = sourceScale; + } + + // if there is an @2x at the end of the url we are going to assume its a highres image + baseTexture.resolution = getResolutionOfUrl(imageUrl); + + BaseTextureCache[imageUrl] = baseTexture; + } + + return baseTexture; + } + else if (source instanceof HTMLCanvasElement) + { + return BaseTexture.fromCanvas(source, scaleMode); + } + + // lets assume its a base texture! + return source; + } } diff --git a/src/core/textures/Spritesheet.js b/src/core/textures/Spritesheet.js new file mode 100644 index 0000000..d469c62 --- /dev/null +++ b/src/core/textures/Spritesheet.js @@ -0,0 +1,274 @@ +import { Rectangle, Texture } from '../'; +import { getResolutionOfUrl, TextureCache } from '../utils'; + +/** + * Utility class for maintaining reference to a collection + * of Textures on a single Spritesheet. + * + * @class + * @memberof PIXI + */ +export default class Spritesheet +{ + /** + * The maximum number of Textures to build per process. + * + * @type {number} + * @default 1000 + */ + static get BATCH_SIZE() + { + return 1000; + } + + /** + * @param {PIXI.BaseTexture} baseTexture Reference to the source BaseTexture object. + * @param {Object} data - Spritesheet image data. + * @param {string} [resolutionFilename] - The filename to consider when determining + * the resolution of the spritesheet. If not provided, the imageUrl will + * be used on the BaseTexture. + */ + constructor(baseTexture, data, resolutionFilename = null) + { + /** + * Reference to ths source texture + * @type {PIXI.BaseTexture} + */ + this.baseTexture = baseTexture; + + /** + * Map of spritesheet textures. + * @type {Object} + */ + this.textures = {}; + + /** + * Reference to the original JSON data. + * @type {Object} + */ + this.data = data; + + /** + * The resolution of the spritesheet. + * @type {number} + */ + this.resolution = this._updateResolution( + resolutionFilename || this.baseTexture.imageUrl + ); + + /** + * Map of spritesheet frames. + * @type {Object} + * @private + */ + this._frames = this.data.frames; + + /** + * Collection of frame names. + * @type {string[]} + * @private + */ + this._frameKeys = Object.keys(this._frames); + + /** + * Current batch index being processed. + * @type {number} + * @private + */ + this._batchIndex = 0; + + /** + * Callback when parse is completed. + * @type {Function} + * @private + */ + this._callback = null; + } + + /** + * Generate the resolution from the filename or fallback + * to the meta.scale field of the JSON data. + * + * @private + * @param {string} resolutionFilename - The filename to use for resolving + * the default resolution. + * @return {number} Resolution to use for spritesheet. + */ + _updateResolution(resolutionFilename) + { + const scale = this.data.meta.scale; + + // Use a defaultValue of `null` to check if a url-based resolution is set + let resolution = getResolutionOfUrl(resolutionFilename, null); + + // No resolution found via URL + if (resolution === null) + { + // Use the scale value or default to 1 + resolution = scale !== undefined ? parseFloat(scale) : 1; + } + + // For non-1 resolutions, update baseTexture + if (resolution !== 1) + { + this.baseTexture.resolution = resolution; + this.baseTexture.update(); + } + + return resolution; + } + + /** + * Parser spritesheet from loaded data. This is done asynchronously + * to prevent creating too many Texture within a single process. + * + * @param {Function} callback - Callback when complete returns + * a map of the Textures for this spritesheet. + */ + parse(callback) + { + this._batchIndex = 0; + this._callback = callback; + + if (this._frameKeys.length <= Spritesheet.BATCH_SIZE) + { + this._processFrames(0); + this._parseComplete(); + } + else + { + this._nextBatch(); + } + } + + /** + * Process a batch of frames + * + * @private + * @param {number} initialFrameIndex - The index of frame to start. + */ + _processFrames(initialFrameIndex) + { + let frameIndex = initialFrameIndex; + const maxFrames = Spritesheet.BATCH_SIZE; + + while (frameIndex - initialFrameIndex < maxFrames && frameIndex < this._frameKeys.length) + { + const i = this._frameKeys[frameIndex]; + const rect = this._frames[i].frame; + + if (rect) + { + let frame = null; + let trim = null; + const orig = new Rectangle( + 0, + 0, + this._frames[i].sourceSize.w / this.resolution, + this._frames[i].sourceSize.h / this.resolution + ); + + if (this._frames[i].rotated) + { + frame = new Rectangle( + rect.x / this.resolution, + rect.y / this.resolution, + rect.h / this.resolution, + rect.w / this.resolution + ); + } + else + { + frame = new Rectangle( + rect.x / this.resolution, + rect.y / this.resolution, + rect.w / this.resolution, + rect.h / this.resolution + ); + } + + // Check to see if the sprite is trimmed + if (this._frames[i].trimmed) + { + trim = new Rectangle( + this._frames[i].spriteSourceSize.x / this.resolution, + this._frames[i].spriteSourceSize.y / this.resolution, + rect.w / this.resolution, + rect.h / this.resolution + ); + } + + this.textures[i] = new Texture( + this.baseTexture, + frame, + orig, + trim, + this._frames[i].rotated ? 2 : 0 + ); + + // lets also add the frame to pixi's global cache for fromFrame and fromImage functions + TextureCache[i] = this.textures[i]; + } + + frameIndex++; + } + } + + /** + * The parse has completed. + * + * @private + */ + _parseComplete() + { + const callback = this._callback; + + this._callback = null; + this._batchIndex = 0; + callback.call(this, this.textures); + } + + /** + * Begin the next batch of textures. + * + * @private + */ + _nextBatch() + { + this._processFrames(this._batchIndex * Spritesheet.BATCH_SIZE); + this._batchIndex++; + setTimeout(() => + { + if (this._batchIndex * Spritesheet.BATCH_SIZE < this._frameKeys.length) + { + this._nextBatch(); + } + else + { + this._parseComplete(); + } + }, 0); + } + + /** + * Destroy Spritesheet and don't use after this. + * + * @param {boolean} [destroyBase=false] Whether to destroy the base texture as well + */ + destroy(destroyBase = false) + { + for (const i in this.textures) + { + this.textures[i].destroy(); + } + this._frames = null; + this._frameKeys = null; + this.data = null; + this.textures = null; + if (destroyBase) + { + this.baseTexture.destroy(); + } + this.baseTexture = null; + } +} diff --git a/src/core/textures/Texture.js b/src/core/textures/Texture.js index 6403d4f..afe4254 100644 --- a/src/core/textures/Texture.js +++ b/src/core/textures/Texture.js @@ -3,7 +3,7 @@ import TextureUvs from './TextureUvs'; import EventEmitter from 'eventemitter3'; import { Rectangle } from '../math'; -import { TextureCache, BaseTextureCache } from '../utils'; +import { TextureCache, BaseTextureCache, getResolutionOfUrl } from '../utils'; /** * A texture stores the information that represents an image or part of an image. It cannot be added @@ -365,7 +365,8 @@ * The source can be - frame id, image url, video url, canvas element, video element, base texture * * @static - * @param {number|string|PIXI.BaseTexture|HTMLCanvasElement|HTMLVideoElement} source - Source to create texture from + * @param {number|string|HTMLImageElement|HTMLCanvasElement|HTMLVideoElement|PIXI.BaseTexture} + * source - Source to create texture from * @return {PIXI.Texture} The newly created texture */ static from(source) @@ -393,7 +394,7 @@ } else if (source instanceof HTMLImageElement) { - return new Texture(new BaseTexture(source)); + return new Texture(BaseTexture.from(source)); } else if (source instanceof HTMLCanvasElement) { @@ -413,6 +414,43 @@ } /** + * Create a texture from a source and add to the cache. + * + * @static + * @param {HTMLImageElement|HTMLCanvasElement} source - The input source. + * @param {String} imageUrl - File name of texture, for cache and resolving resolution. + * @param {String} [name] - Human readible name for the texture cache. If no name is + * specified, only `imageUrl` will be used as the cache ID. + * @return {PIXI.Texture} Output texture + */ + static fromLoader(source, imageUrl, name) + { + const baseTexture = new BaseTexture(source, null, getResolutionOfUrl(imageUrl)); + const texture = new Texture(baseTexture); + + baseTexture.imageUrl = imageUrl; + + // No name, use imageUrl instead + if (!name) + { + name = imageUrl; + } + + // lets also add the frame to pixi's global cache for fromFrame and fromImage fucntions + BaseTextureCache[name] = baseTexture; + TextureCache[name] = texture; + + // also add references by url if they are different. + if (name !== imageUrl) + { + BaseTextureCache[imageUrl] = baseTexture; + TextureCache[imageUrl] = texture; + } + + return texture; + } + + /** * Adds a texture to the global TextureCache. This cache is shared across the whole PIXI object. * * @static @@ -522,6 +560,29 @@ } } +function createWhiteTexture() +{ + const canvas = document.createElement('canvas'); + + canvas.width = 10; + canvas.height = 10; + + const context = canvas.getContext('2d'); + + context.fillStyle = 'white'; + context.fillRect(0, 0, 10, 10); + + return new Texture(new BaseTexture(canvas)); +} + +function removeAllHandlers(tex) +{ + tex.destroy = function _emptyDestroy() { /* empty */ }; + tex.on = function _emptyOn() { /* empty */ }; + tex.once = function _emptyOnce() { /* empty */ }; + tex.emit = function _emptyEmit() { /* empty */ }; +} + /** * An empty texture, used often to not have to create multiple empty textures. * Can not be destroyed. @@ -530,7 +591,14 @@ * @constant */ Texture.EMPTY = new Texture(new BaseTexture()); -Texture.EMPTY.destroy = function _emptyDestroy() { /* empty */ }; -Texture.EMPTY.on = function _emptyOn() { /* empty */ }; -Texture.EMPTY.once = function _emptyOnce() { /* empty */ }; -Texture.EMPTY.emit = function _emptyEmit() { /* empty */ }; +removeAllHandlers(Texture.EMPTY); + +/** + * A white texture of 10x10 size, used for graphics and other things + * Can not be destroyed. + * + * @static + * @constant + */ +Texture.WHITE = createWhiteTexture(); +removeAllHandlers(Texture.WHITE); diff --git a/src/core/ticker/Ticker.js b/src/core/ticker/Ticker.js index d98a1f0..17a1517 100644 --- a/src/core/ticker/Ticker.js +++ b/src/core/ticker/Ticker.js @@ -67,11 +67,12 @@ * is based, this value is neither capped nor scaled. * If the platform supports DOMHighResTimeStamp, * this value will have a precision of 1 µs. + * Defaults to target frame time * * @member {number} - * @default 1 / TARGET_FPMS + * @default 16.66 */ - this.elapsedMS = 1 / settings.TARGET_FPMS; // default to target frame time + this.elapsedMS = 1 / settings.TARGET_FPMS; /** * The last time {@link PIXI.ticker.Ticker#update} was invoked. diff --git a/src/core/utils/index.js b/src/core/utils/index.js index 6ff609f..be44750 100644 --- a/src/core/utils/index.js +++ b/src/core/utils/index.js @@ -2,6 +2,7 @@ import settings from '../settings'; import EventEmitter from 'eventemitter3'; import pluginTarget from './pluginTarget'; +import * as mixins from './mixin'; import * as isMobile from 'ismobilejs'; let nextUid = 0; @@ -33,6 +34,7 @@ * @type {mixin} */ pluginTarget, + mixins, }; /** @@ -93,7 +95,7 @@ */ export function rgb2hex(rgb) { - return (((rgb[0] * 255) << 16) + ((rgb[1] * 255) << 8) + (rgb[2] * 255)); + return (((rgb[0] * 255) << 16) + ((rgb[1] * 255) << 8) + (rgb[2] * 255 | 0)); } /** @@ -368,3 +370,43 @@ * @private */ export const ProgramCache = {}; + +/** + * Destroys all texture in the cache + * + * @memberof PIXI.utils + * @function destroyTextureCache + */ +export function destroyTextureCache() +{ + let key; + + for (key in TextureCache) + { + TextureCache[key].destroy(); + } + for (key in BaseTextureCache) + { + BaseTextureCache[key].destroy(); + } +} + +/** + * Removes all textures from cache, but does not destroy them + * + * @memberof PIXI.utils + * @function clearTextureCache + */ +export function clearTextureCache() +{ + let key; + + for (key in TextureCache) + { + delete TextureCache[key]; + } + for (key in BaseTextureCache) + { + delete BaseTextureCache[key]; + } +} diff --git a/src/core/utils/mixin.js b/src/core/utils/mixin.js new file mode 100644 index 0000000..114de2a --- /dev/null +++ b/src/core/utils/mixin.js @@ -0,0 +1,59 @@ +/** + * Mixes all enumerable properties and methods from a source object to a target object. + * + * @memberof PIXI.utils.mixins + * @function mixin + * @param {object} target The prototype or instance that properties and methods should be added to. + * @param {object} source The source of properties and methods to mix in. + */ +export function mixin(target, source) +{ + if (!target || !source) return; + // in ES8/ES2017, this would be really easy: + // Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); + + // get all the enumerable property keys + const keys = Object.keys(source); + + // loop through properties + for (let i = 0; i < keys.length; ++i) + { + const propertyName = keys[i]; + + // Set the property using the property descriptor - this works for accessors and normal value properties + Object.defineProperty(target, propertyName, Object.getOwnPropertyDescriptor(source, propertyName)); + } +} + +const mixins = []; + +/** + * Queues a mixin to be handled towards the end of the initialization of PIXI, so that deprecation + * can take effect. + * + * @memberof PIXI.utils.mixins + * @function delayMixin + * @private + * @param {object} target The prototype or instance that properties and methods should be added to. + * @param {object} source The source of properties and methods to mix in. + */ +export function delayMixin(target, source) +{ + mixins.push(target, source); +} + +/** + * Handles all mixins queued via delayMixin(). + * + * @memberof PIXI.utils.mixins + * @function performMixins + * @private + */ +export function performMixins() +{ + for (let i = 0; i < mixins.length; i += 2) + { + mixin(mixins[i], mixins[i + 1]); + } + mixins.length = 0; +} diff --git a/src/deprecation.js b/src/deprecation.js index 045a19f..b9c73b3 100644 --- a/src/deprecation.js +++ b/src/deprecation.js @@ -5,6 +5,7 @@ import * as filters from './filters'; import * as prepare from './prepare'; import * as loaders from './loaders'; +import * as interaction from './interaction'; // provide method to give a stack track for warnings // useful for tracking-down where deprecated methods/properties/classes @@ -534,7 +535,7 @@ { parent: 'GC_MODES', target: 'GC_MODE' }, { parent: 'WRAP_MODES', target: 'WRAP_MODE' }, { parent: 'SCALE_MODES', target: 'SCALE_MODE' }, - { parent: 'PRECISION', target: 'PRECISION' }, + { parent: 'PRECISION', target: 'PRECISION_FRAGMENT' }, ]; for (let i = 0; i < defaults.length; i++) @@ -558,6 +559,32 @@ }); } +Object.defineProperties(core.settings, { + + /** + * @static + * @name PRECISION + * @memberof PIXI.settings + * @see PIXI.PRECISION + * @deprecated since version 4.4.0 + */ + PRECISION: { + enumerable: true, + get() + { + warn('PIXI.settings.PRECISION has been deprecated, please use PIXI.settings.PRECISION_FRAGMENT'); + + return core.settings.PRECISION_FRAGMENT; + }, + set(value) + { + warn('PIXI.settings.PRECISION has been deprecated, please use PIXI.settings.PRECISION_FRAGMENT'); + + core.settings.PRECISION_FRAGMENT = value; + }, + }, +}); + Object.defineProperties(extras, { /** @@ -694,7 +721,7 @@ warn('determineFontProperties is now deprecated, please use the static calculateFontProperties method, ' + 'e.g : Text.calculateFontProperties(fontStyle);'); - return Text.calculateFontProperties(fontStyle); + return core.Text.calculateFontProperties(fontStyle); }; Object.defineProperties(core.TextStyle.prototype, { @@ -1003,3 +1030,69 @@ }, }, }); + +/** + * @name PIXI.interaction.interactiveTarget#defaultCursor + * @static + * @type {number} + * @see PIXI.interaction.interactiveTarget#cursor + * @deprecated since 4.3.0 + */ +Object.defineProperty(interaction.interactiveTarget, 'defaultCursor', { + set(value) + { + warn('Property defaultCursor has been replaced with \'cursor\'. '); + this.cursor = value; + }, + get() + { + warn('Property defaultCursor has been replaced with \'cursor\'. '); + + return this.cursor; + }, + enumerable: true, +}); + +/** + * @name PIXI.interaction.InteractionManager#defaultCursorStyle + * @static + * @type {string} + * @see PIXI.interaction.InteractionManager#cursorStyles + * @deprecated since 4.3.0 + */ +Object.defineProperty(interaction.InteractionManager, 'defaultCursorStyle', { + set(value) + { + warn('Property defaultCursorStyle has been replaced with \'cursorStyles.default\'. '); + this.cursorStyles.default = value; + }, + get() + { + warn('Property defaultCursorStyle has been replaced with \'cursorStyles.default\'. '); + + return this.cursorStyles.default; + }, +}); + +/** + * @name PIXI.interaction.InteractionManager#currentCursorStyle + * @static + * @type {string} + * @see PIXI.interaction.InteractionManager#cursorStyles + * @deprecated since 4.3.0 + */ +Object.defineProperty(interaction.InteractionManager, 'currentCursorStyle', { + set(value) + { + warn('Property currentCursorStyle has been removed.' + + 'See the currentCursorMode property, which works differently.'); + this.currentCursorMode = value; + }, + get() + { + warn('Property currentCursorStyle has been removed.' + + 'See the currentCursorMode property, which works differently.'); + + return this.currentCursorMode; + }, +}); diff --git a/src/extras/BitmapText.js b/src/extras/BitmapText.js index fb6c343..0a0e150 100644 --- a/src/extras/BitmapText.js +++ b/src/extras/BitmapText.js @@ -455,6 +455,73 @@ return this._textHeight; } + + /** + * Register a bitmap font with data and a texture. + * + * @static + * @param {XMLDocument} xml - The XML document data. + * @param {PIXI.Texture} texture - Texture with all symbols. + * @return {Object} Result font object with font, size, lineHeight and char fields. + */ + static registerFont(xml, texture) + { + const data = {}; + const info = xml.getElementsByTagName('info')[0]; + const common = xml.getElementsByTagName('common')[0]; + + data.font = info.getAttribute('face'); + data.size = parseInt(info.getAttribute('size'), 10); + data.lineHeight = parseInt(common.getAttribute('lineHeight'), 10); + data.chars = {}; + + // parse letters + const letters = xml.getElementsByTagName('char'); + + for (let i = 0; i < letters.length; i++) + { + const letter = letters[i]; + const charCode = parseInt(letter.getAttribute('id'), 10); + + const textureRect = new core.Rectangle( + parseInt(letter.getAttribute('x'), 10) + texture.frame.x, + parseInt(letter.getAttribute('y'), 10) + texture.frame.y, + parseInt(letter.getAttribute('width'), 10), + parseInt(letter.getAttribute('height'), 10) + ); + + data.chars[charCode] = { + xOffset: parseInt(letter.getAttribute('xoffset'), 10), + yOffset: parseInt(letter.getAttribute('yoffset'), 10), + xAdvance: parseInt(letter.getAttribute('xadvance'), 10), + kerning: {}, + texture: new core.Texture(texture.baseTexture, textureRect), + + }; + } + + // parse kernings + const kernings = xml.getElementsByTagName('kerning'); + + for (let i = 0; i < kernings.length; i++) + { + const kerning = kernings[i]; + const first = parseInt(kerning.getAttribute('first'), 10); + const second = parseInt(kerning.getAttribute('second'), 10); + const amount = parseInt(kerning.getAttribute('amount'), 10); + + if (data.chars[second]) + { + data.chars[second].kerning[first] = amount; + } + } + + // I'm leaving this as a temporary fix so we can test the bitmap fonts in v3 + // but it's very likely to change + BitmapText.fonts[data.font] = data; + + return data; + } } BitmapText.fonts = {}; diff --git a/src/index.js b/src/index.js index 59356cf..c553427 100644 --- a/src/index.js +++ b/src/index.js @@ -16,6 +16,10 @@ import * as particles from './particles'; import * as prepare from './prepare'; +// handle mixins now, after all code has been added, including deprecation +import { utils } from './core'; +utils.mixins.performMixins(); + export { accessibility, extract, diff --git a/src/interaction/InteractionManager.js b/src/interaction/InteractionManager.js index 7d51134..8baf13a 100644 --- a/src/interaction/InteractionManager.js +++ b/src/interaction/InteractionManager.js @@ -1,18 +1,20 @@ 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'; -// Mix interactiveTarget into core.DisplayObject.prototype -Object.assign( +// Mix interactiveTarget into core.DisplayObject.prototype, after deprecation has been handled +core.utils.mixins.delayMixin( core.DisplayObject.prototype, 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 +70,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 +101,6 @@ this.eventData = new InteractionEvent(); /** - * Tiny little interactiveData pool ! - * - * @member {PIXI.interaction.InteractionData[]} - */ - this.interactiveDataPool = []; - - /** * The DOM element to bind to. * * @private @@ -153,66 +155,12 @@ */ this.supportsPointerEvents = !!window.PointerEvent; - /** - * Are touch events being 'normalized' and converted into pointer events if pointer events are not supported - * For example, on a touch screen mobile device, a touchstart would also be emitted as a pointerdown - * - * @private - * @readonly - * @member {boolean} - */ - this.normalizeTouchEvents = !this.supportsPointerEvents && this.supportsTouchEvents; - - /** - * Are mouse events being 'normalized' and converted into pointer events if pointer events are not supported - * For example, on a desktop pc, a mousedown would also be emitted as a pointerdown - * - * @private - * @readonly - * @member {boolean} - */ - this.normalizeMouseEvents = !this.supportsPointerEvents && !MobileDevice.any; - // this will make it so that you don't have to call bind all the time /** * @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 +168,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,41 +199,32 @@ this.onPointerOver = this.onPointerOver.bind(this); /** - * @private - * @member {Function} + * Dictionary of how different cursor modes are handled. Strings are handled as CSS cursor + * values, objects are handled as dictionaries of CSS values for interactionDOMElement, + * and functions are called instead of changing the CSS. + * Default CSS cursor values are provided for 'default' and 'pointer' modes. + * @member {Object.)>} */ - this.onTouchStart = this.onTouchStart.bind(this); - this.processTouchStart = this.processTouchStart.bind(this); + this.cursorStyles = { + default: 'inherit', + pointer: 'pointer', + }; /** - * @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. - * - * @member {string} - * @default 'inherit' - */ - this.defaultCursorStyle = 'inherit'; - - /** - * The css style of the cursor that is being used. + * The mode of the cursor that is being used. + * The value of this is a key from the cursorStyles dictionary. * * @member {string} */ - this.currentCursorStyle = 'inherit'; + this.currentCursorMode = null; + + /** + * Internal cached let. + * + * @private + * @member {string} + */ + this.cursor = null; /** * Internal cached let. @@ -303,6 +249,7 @@ * object. * * @event mousedown + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -311,6 +258,7 @@ * on the display object. * * @event rightdown + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -319,6 +267,7 @@ * object. * * @event mouseup + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -327,6 +276,7 @@ * over the display object. * * @event rightup + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -335,6 +285,7 @@ * the display object. * * @event click + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -343,6 +294,7 @@ * and released on the display object. * * @event rightclick + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -352,6 +304,7 @@ * [mousedown]{@link PIXI.interaction.InteractionManager#event:mousedown}. * * @event mouseupoutside + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -361,6 +314,7 @@ * [rightdown]{@link PIXI.interaction.InteractionManager#event:rightdown}. * * @event rightupoutside + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -368,6 +322,7 @@ * Fired when a pointer device (usually a mouse) is moved while over the display object * * @event mousemove + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -375,6 +330,7 @@ * Fired when a pointer device (usually a mouse) is moved onto the display object * * @event mouseover + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -382,6 +338,7 @@ * Fired when a pointer device (usually a mouse) is moved off the display object * * @event mouseout + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -389,6 +346,7 @@ * Fired when a pointer device button is pressed on the display object. * * @event pointerdown + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -396,6 +354,14 @@ * Fired when a pointer device button is released over the display object. * * @event pointerup + * @type {PIXI.interaction.InteractionData} + * @memberof PIXI.interaction.InteractionManager# + */ + + /** + * Fired when the operating system cancels a pointer event + * + * @event pointercancel * @memberof PIXI.interaction.InteractionManager# */ @@ -403,6 +369,7 @@ * Fired when a pointer device button is pressed and released on the display object. * * @event pointertap + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -411,6 +378,7 @@ * registered a [pointerdown]{@link PIXI.interaction.InteractionManager#event:pointerdown}. * * @event pointerupoutside + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -418,6 +386,7 @@ * Fired when a pointer device is moved while over the display object * * @event pointermove + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -425,6 +394,7 @@ * Fired when a pointer device is moved onto the display object * * @event pointerover + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -432,6 +402,7 @@ * Fired when a pointer device is moved off the display object * * @event pointerout + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -439,6 +410,7 @@ * Fired when a touch point is placed on the display object. * * @event touchstart + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -446,6 +418,14 @@ * Fired when a touch point is removed from the display object. * * @event touchend + * @type {PIXI.interaction.InteractionData} + * @memberof PIXI.interaction.InteractionManager# + */ + + /** + * Fired when the operating system cancels a touch + * + * @event touchcancel * @memberof PIXI.interaction.InteractionManager# */ @@ -453,6 +433,7 @@ * Fired when a touch point is placed and removed from the display object. * * @event tap + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -461,6 +442,7 @@ * registered a [touchstart]{@link PIXI.interaction.InteractionManager#event:touchstart}. * * @event touchendoutside + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -468,6 +450,7 @@ * Fired when a touch point is moved along the display object. * * @event touchmove + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ } @@ -524,45 +507,30 @@ { window.document.addEventListener('pointermove', this.onPointerMove, true); this.interactionDOMElement.addEventListener('pointerdown', this.onPointerDown, true); - this.interactionDOMElement.addEventListener('pointerout', this.onPointerOut, true); + // pointerout is fired in addition to pointerup (for touch events) and pointercancel + // we already handle those, so for the purposes of what we do in onPointerOut, we only + // care about the pointerleave event + this.interactionDOMElement.addEventListener('pointerleave', 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; @@ -596,45 +564,26 @@ { window.document.removeEventListener('pointermove', this.onPointerMove, true); this.interactionDOMElement.removeEventListener('pointerdown', this.onPointerDown, true); - this.interactionDOMElement.removeEventListener('pointerout', this.onPointerOut, true); + this.interactionDOMElement.removeEventListener('pointerleave', 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) + 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.onPointerDown, true); + this.interactionDOMElement.removeEventListener('touchcancel', this.onPointerCancel, 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); - - 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 = null; @@ -664,7 +613,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; @@ -672,25 +621,80 @@ return; } - this.cursor = this.defaultCursorStyle; + this.cursor = null; // 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(); - - this.processInteractive(this.mouse.global, this.renderer._lastObjectRendered, this.processMouseOverOut, true); - - if (this.currentCursorStyle !== this.cursor) + for (const k in this.activeInteractionData) { - this.currentCursorStyle = this.cursor; - this.interactionDOMElement.style.cursor = this.cursor; + // eslint-disable-next-line no-prototype-builtins + if (this.activeInteractionData.hasOwnProperty(k)) + { + const interactionData = this.activeInteractionData[k]; + + if (interactionData.originalEvent && interactionData.pointerType !== 'touch') + { + const interactionEvent = this.configureInteractionEventForDOMEvent( + this.eventData, + interactionData.originalEvent, + interactionData + ); + + this.processInteractive( + interactionEvent, + this.renderer._lastObjectRendered, + this.processPointerOverOut, + true + ); + } + } } + this.setCursorMode(this.cursor); + // TODO } /** + * Sets the current cursor mode, handling any callbacks or CSS style changes. + * + * @param {string} mode - cursor mode, a key from the cursorStyles dictionary + */ + setCursorMode(mode) + { + mode = mode || 'default'; + // if the mode didn't actually change, bail early + if (this.currentCursorMode === mode) + { + return; + } + this.currentCursorMode = mode; + const style = this.cursorStyles[mode]; + + // only do things if there is a cursor style for it + if (style) + { + switch (typeof style) + { + case 'string': + // string styles are handled as cursor CSS + this.interactionDOMElement.style.cursor = style; + break; + case 'function': + // functions are just called, and passed the cursor mode + style(mode); + break; + case 'object': + // if it is an object, assume that it is a dictionary of CSS styles, + // apply it to the interactionDOMElement + Object.assign(this.interactionDOMElement.style, style); + break; + } + } + } + + /** * Dispatches an event on the display object that was interacted with * * @param {PIXI.Container|PIXI.Sprite|PIXI.extras.TilingSprite} displayObject - the display object in question @@ -748,22 +752,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 @@ -787,9 +795,8 @@ { interactiveParent = false; } - - // it has a mask! Then lets hit test that before continuing.. - if (hitTest && displayObject._mask) + // it has a mask! Then lets hit test that before continuing + else if (hitTest && displayObject._mask) { if (!displayObject._mask.containsPoint(point)) { @@ -797,14 +804,7 @@ } } - // it has a filterArea! Same as mask but easier, its a rectangle - if (hitTest && displayObject.filterArea) - { - if (!displayObject.filterArea.contains(point.x, point.y)) - { - hitTest = false; - } - } + let keepHitTestingAfterChildren = hitTest; // ** FREE TIP **! If an object is not interactive or has no buttons in it // (such as a game scene!) set interactiveChildren to false for that displayObject. @@ -818,7 +818,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 @@ -838,15 +838,20 @@ // This means we no longer need to hit test anything else. We still need to run // through all objects, but we don't need to perform any hit tests. - // { - hitTest = false; - // } + keepHitTestingAfterChildren = false; + + if (child.interactive) + { + hitTest = false; + } // we can break now as we have hit an object. } } } + hitTest = keepHitTestingAfterChildren; + // no point running this if the item is not interactive or does not have an interactive parent. if (interactive) { @@ -868,14 +873,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 +886,175 @@ } /** - * 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].isNormalized) { - 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); + if (!displayObject.trackedPointers[id]) + { + displayObject.trackedPointers[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.trackedPointers[id].rightDown = true; + } + else + { + displayObject.trackedPointers[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.trackedPointers[id] !== undefined) + { + delete displayObject.trackedPointers[id]; + this.dispatchEvent(displayObject, 'pointercancel', interactionEvent); + + if (e.type === 'touchcancel' || e.pointerType === 'touch') + { + this.dispatchEvent(displayObject, 'touchcancel', interactionEvent); + } } } @@ -1159,42 +1066,94 @@ */ 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) { - if (hit) - { - this.dispatchEvent(displayObject, 'pointerup', this.eventData); + const e = interactionEvent.data.originalEvent; - if (displayObject._pointerDown) + const id = interactionEvent.data.identifier; + + const trackingData = displayObject.trackedPointers[id]; + + const isTouch = (e.type === 'touchend' || e.pointerType === 'touch'); + + const isMouse = (e.type.indexOf('mouse') === 0 || e.pointerType === 'mouse'); + + // 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) { - displayObject._pointerDown = false; - this.dispatchEvent(displayObject, 'pointertap', this.eventData); + 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); + } + // update the down state of the tracking data + if (trackingData) + { + if (isRightButton) + { + trackingData.rightDown = hit; + } + else + { + trackingData.leftDown = hit; + } } } - else if (displayObject._pointerDown) + + // Pointers and Touches, and Mouse + if (hit) { - displayObject._pointerDown = false; - this.dispatchEvent(displayObject, 'pointerupoutside', this.eventData); + this.dispatchEvent(displayObject, 'pointerup', interactionEvent); + if (isTouch) this.dispatchEvent(displayObject, 'touchend', interactionEvent); + + if (trackingData) + { + this.dispatchEvent(displayObject, 'pointertap', interactionEvent); + if (isTouch) + { + this.dispatchEvent(displayObject, 'tap', interactionEvent); + // touches are no longer over (if they ever were) when we get the touchend + // so we should ensure that we don't keep pretending that they are + trackingData.over = false; + } + } + } + else if (trackingData) + { + this.dispatchEvent(displayObject, 'pointerupoutside', interactionEvent); + if (isTouch) this.dispatchEvent(displayObject, 'touchendoutside', interactionEvent); + } + // Only remove the tracking data if there is no over/down state still associated with it + if (trackingData && trackingData.none) + { + delete displayObject.trackedPointers[id]; } } @@ -1202,39 +1161,78 @@ * 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 = null; + } - 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') + { + this.setCursorMode(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 (isMouse) { - 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 +1240,94 @@ * 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.setCursorMode(null); + } - 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'); + + let trackingData = displayObject.trackedPointers[id]; + + // if we just moused over the display object, then we need to track that state + if (hit && !trackingData) + { + trackingData = displayObject.trackedPointers[id] = new InteractionTrackingData(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); + } + } + + // only change the cursor if it has not already been changed (by something deeper in the + // display tree) + if (isMouse && this.cursor === null) + { + this.cursor = displayObject.cursor; } } - 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); + } + // if there is no mouse down information for the pointer, then it is safe to delete + if (trackingData.none) + { + delete displayObject.trackedPointers[id]; + } } } @@ -1287,253 +1335,154 @@ * 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 && pointerEvent.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; + + // mark the touch as normalized, just so that we know we did it + touch.isNormalized = true; + + normalizedEvents.push(touch); + } } - else if (this.normalizeMouseEvents) + // apparently PointerEvent subclasses MouseEvent, so yay + else if (event instanceof MouseEvent && (!this.supportsPointerEvents || !(event instanceof window.PointerEvent))) { if (typeof event.isPrimary === 'undefined') event.isPrimary = true; if (typeof event.width === 'undefined') event.width = 1; @@ -1541,10 +1490,21 @@ 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; + + // mark the mouse event as normalized, just so that we know we did it + event.isNormalized = true; + + normalizedEvents.push(event); } + else + { + normalizedEvents.push(event); + } + + return normalizedEvents; } /** @@ -1563,30 +1523,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 +1542,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..6a1f69e --- /dev/null +++ b/src/interaction/InteractionTrackingData.js @@ -0,0 +1,147 @@ +/** + * 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 inactive (not over or down)? + * + * @member {number} + * @memberof PIXI.interaction.InteractionTrackingData# + */ + get none() + { + return this._flags === this.constructor.FLAGS.NONE; + } + + /** + * 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..d252fba 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 * ); */ @@ -38,68 +38,56 @@ hitArea: null, /** - * If enabled, the mouse cursor will change when hovered over the displayObject if it is interactive + * If enabled, the mouse cursor use the pointer behavior when hovered over the displayObject if it is interactive + * Setting this changes the 'cursor' property to `'pointer'`. * - * @inner {boolean} + * @member {boolean} + * @memberof PIXI.interaction.interactiveTarget# */ - buttonMode: false, + get buttonMode() + { + return this.cursor === 'pointer'; + }, + set buttonMode(value) + { + if (value) + { + this.cursor = 'pointer'; + } + else if (this.cursor === 'pointer') + { + this.cursor = null; + } + }, /** - * If buttonMode is enabled, this defines what CSS cursor property is used when the mouse cursor - * is hovered over the displayObject + * This defines what cursor mode is used when the mouse cursor + * is hovered over the displayObject. * * @see https://developer.mozilla.org/en/docs/Web/CSS/cursor * * @inner {string} */ - defaultCursor: 'pointer', - - // some internal checks.. - /** - * Internal check to detect if the mouse cursor is hovered over the displayObject - * - * @inner {boolean} - * @private - */ - _over: false, + cursor: null, /** - * Internal check to detect if the left mouse button is pressed on the displayObject + * Internal set of all active pointers, by identifier * - * @inner {boolean} + * @member {Map} + * @memberof PIXI.interaction.interactiveTarget# * @private */ - _isLeftDown: false, + get trackedPointers() + { + if (this._trackedPointers === undefined) this._trackedPointers = {}; + + return this._trackedPointers; + }, /** - * Internal check to detect if the right mouse button is pressed on the displayObject + * Map of all tracked pointers, by identifier. Use trackedPointers to access. * - * @inner {boolean} - * @private + * @private {Map} */ - _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, + _trackedPointers: undefined, }; diff --git a/src/loaders/bitmapFontParser.js b/src/loaders/bitmapFontParser.js index cc2618c..f4cf996 100644 --- a/src/loaders/bitmapFontParser.js +++ b/src/loaders/bitmapFontParser.js @@ -1,63 +1,19 @@ import * as path from 'path'; -import { Rectangle, Texture, utils } from '../core'; +import { utils } from '../core'; import { Resource } from 'resource-loader'; import { BitmapText } from '../extras'; +/** + * Register a BitmapText font from loader resource. + * + * @function parseBitmapFontData + * @memberof PIXI.loaders + * @param {PIXI.loaders.Resource} resource - Loader resource. + * @param {PIXI.Texture} texture - Reference to texture. + */ export function parse(resource, texture) { - const data = {}; - const info = resource.data.getElementsByTagName('info')[0]; - const common = resource.data.getElementsByTagName('common')[0]; - - data.font = info.getAttribute('face'); - data.size = parseInt(info.getAttribute('size'), 10); - data.lineHeight = parseInt(common.getAttribute('lineHeight'), 10); - data.chars = {}; - - // parse letters - const letters = resource.data.getElementsByTagName('char'); - - for (let i = 0; i < letters.length; i++) - { - const charCode = parseInt(letters[i].getAttribute('id'), 10); - - const textureRect = new Rectangle( - parseInt(letters[i].getAttribute('x'), 10) + texture.frame.x, - parseInt(letters[i].getAttribute('y'), 10) + texture.frame.y, - parseInt(letters[i].getAttribute('width'), 10), - parseInt(letters[i].getAttribute('height'), 10) - ); - - data.chars[charCode] = { - xOffset: parseInt(letters[i].getAttribute('xoffset'), 10), - yOffset: parseInt(letters[i].getAttribute('yoffset'), 10), - xAdvance: parseInt(letters[i].getAttribute('xadvance'), 10), - kerning: {}, - texture: new Texture(texture.baseTexture, textureRect), - - }; - } - - // parse kernings - const kernings = resource.data.getElementsByTagName('kerning'); - - for (let i = 0; i < kernings.length; i++) - { - const first = parseInt(kernings[i].getAttribute('first'), 10); - const second = parseInt(kernings[i].getAttribute('second'), 10); - const amount = parseInt(kernings[i].getAttribute('amount'), 10); - - if (data.chars[second]) - { - data.chars[second].kerning[first] = amount; - } - } - - resource.bitmapFont = data; - - // I'm leaving this as a temporary fix so we can test the bitmap fonts in v3 - // but it's very likely to change - BitmapText.fonts[data.font] = data; + resource.bitmapFont = BitmapText.registerFont(resource.data, texture); } export default function () diff --git a/src/loaders/index.js b/src/loaders/index.js index 8942436..5189793 100644 --- a/src/loaders/index.js +++ b/src/loaders/index.js @@ -5,4 +5,11 @@ export { default as bitmapFontParser, parse as parseBitmapFontData } from './bitmapFontParser'; export { default as spritesheetParser } from './spritesheetParser'; export { default as textureParser } from './textureParser'; + +/** + * Reference to **resource-loader**'s Resource class. + * See https://github.com/englercj/resource-loader + * @class Resource + * @memberof PIXI.loaders + */ export { Resource } from 'resource-loader'; diff --git a/src/loaders/loader.js b/src/loaders/loader.js index 0a670cb..767a411 100644 --- a/src/loaders/loader.js +++ b/src/loaders/loader.js @@ -7,20 +7,47 @@ /** * - * The new loader, extends Resource Loader by Chad Engler : https://github.com/englercj/resource-loader + * The new loader, extends Resource Loader by Chad Engler: https://github.com/englercj/resource-loader * * ```js - * let loader = PIXI.loader; // pixi exposes a premade instance for you to use. + * const loader = PIXI.loader; // pixi exposes a premade instance for you to use. * //or - * let loader = new PIXI.loaders.Loader(); // you can also create your own if you want + * const loader = new PIXI.loaders.Loader(); // you can also create your own if you want * - * loader.add('bunny', 'data/bunny.png'); - * loader.add('spaceship', 'assets/spritesheet.json'); + * const sprites = {}; + * + * // Chainable `add` to enqueue a resource + * loader.add('bunny', 'data/bunny.png') + * .add('spaceship', 'assets/spritesheet.json'); * loader.add('scoreFont', 'assets/score.fnt'); * - * loader.once('complete',onAssetsLoaded); + * // Chainable `pre` to add a middleware that runs for each resource, *before* loading that resource. + * // This is useful to implement custom caching modules (using filesystem, indexeddb, memory, etc). + * loader.pre(cachingMiddleware); * - * loader.load(); + * // Chainable `use` to add a middleware that runs for each resource, *after* loading that resource. + * // This is useful to implement custom parsing modules (like spritesheet parsers, spine parser, etc). + * loader.use(parsingMiddleware); + * + * // The `load` method loads the queue of resources, and calls the passed in callback called once all + * // resources have loaded. + * loader.load((loader, resources) => { + * // resources is an object where the key is the name of the resource loaded and the value is the resource object. + * // They have a couple default properties: + * // - `url`: The URL that the resource was loaded from + * // - `error`: The error that happened when trying to load (if any) + * // - `data`: The raw data that was loaded + * // also may contain other properties based on the middleware that runs. + * sprites.bunny = new PIXI.TilingSprite(resources.bunny.texture); + * sprites.spaceship = new PIXI.TilingSprite(resources.spaceship.texture); + * sprites.scoreFont = new PIXI.TilingSprite(resources.scoreFont.texture); + * }); + * + * // throughout the process multiple signals can be dispatched. + * loader.onProgress.add(() => {}); // called once per loaded/errored file + * loader.onError.add(() => {}); // called once per errored file + * loader.onLoad.add(() => {}); // called once per loaded file + * loader.onComplete.add(() => {}); // called once when the queued resources all load. * ``` * * @see https://github.com/englercj/resource-loader diff --git a/src/loaders/spritesheetParser.js b/src/loaders/spritesheetParser.js index 7570332..e57dfb2 100644 --- a/src/loaders/spritesheetParser.js +++ b/src/loaders/spritesheetParser.js @@ -1,8 +1,6 @@ import { Resource } from 'resource-loader'; import path from 'path'; -import * as core from '../core'; - -const BATCH_SIZE = 1000; +import { Spritesheet } from '../core'; export default function () { @@ -43,134 +41,18 @@ // load the image for this sheet this.add(imageResourceName, resourcePath, loadOptions, function onImageLoad(res) { - resource.textures = {}; + const spritesheet = new Spritesheet( + res.texture.baseTexture, + resource.data, + resource.url + ); - const frames = resource.data.frames; - const frameKeys = Object.keys(frames); - const baseTexture = res.texture.baseTexture; - const scale = resource.data.meta.scale; - - // Use a defaultValue of `null` to check if a url-based resolution is set - let resolution = core.utils.getResolutionOfUrl(resource.url, null); - - // No resolution found via URL - if (resolution === null) + spritesheet.parse(() => { - // Use the scale value or default to 1 - resolution = scale !== undefined ? scale : 1; - } - - // For non-1 resolutions, update baseTexture - if (resolution !== 1) - { - baseTexture.resolution = resolution; - baseTexture.update(); - } - - let batchIndex = 0; - - function processFrames(initialFrameIndex, maxFrames) - { - let frameIndex = initialFrameIndex; - - while (frameIndex - initialFrameIndex < maxFrames && frameIndex < frameKeys.length) - { - const i = frameKeys[frameIndex]; - const rect = frames[i].frame; - - if (rect) - { - let frame = null; - let trim = null; - const orig = new core.Rectangle( - 0, - 0, - frames[i].sourceSize.w / resolution, - frames[i].sourceSize.h / resolution - ); - - if (frames[i].rotated) - { - frame = new core.Rectangle( - rect.x / resolution, - rect.y / resolution, - rect.h / resolution, - rect.w / resolution - ); - } - else - { - frame = new core.Rectangle( - rect.x / resolution, - rect.y / resolution, - rect.w / resolution, - rect.h / resolution - ); - } - - // Check to see if the sprite is trimmed - if (frames[i].trimmed) - { - trim = new core.Rectangle( - frames[i].spriteSourceSize.x / resolution, - frames[i].spriteSourceSize.y / resolution, - rect.w / resolution, - rect.h / resolution - ); - } - - resource.textures[i] = new core.Texture( - baseTexture, - frame, - orig, - trim, - frames[i].rotated ? 2 : 0 - ); - - // lets also add the frame to pixi's global cache for fromFrame and fromImage functions - core.utils.TextureCache[i] = resource.textures[i]; - } - - frameIndex++; - } - } - - function shouldProcessNextBatch() - { - return batchIndex * BATCH_SIZE < frameKeys.length; - } - - function processNextBatch(done) - { - processFrames(batchIndex * BATCH_SIZE, BATCH_SIZE); - batchIndex++; - setTimeout(done, 0); - } - - function iteration() - { - processNextBatch(() => - { - if (shouldProcessNextBatch()) - { - iteration(); - } - else - { - next(); - } - }); - } - - if (frameKeys.length <= BATCH_SIZE) - { - processFrames(0, BATCH_SIZE); + resource.spritesheet = spritesheet; + resource.textures = spritesheet.textures; next(); - } - else - { - iteration(); - } + }); }); }; } diff --git a/src/loaders/textureParser.js b/src/loaders/textureParser.js index 5398a7f..480edc1 100644 --- a/src/loaders/textureParser.js +++ b/src/loaders/textureParser.js @@ -1,5 +1,5 @@ -import * as core from '../core'; import { Resource } from 'resource-loader'; +import Texture from '../core/textures/Texture'; export default function () { @@ -8,23 +8,12 @@ // create a new texture if the data is an Image object if (resource.data && resource.type === Resource.TYPE.IMAGE) { - const baseTexture = new core.BaseTexture(resource.data, null, core.utils.getResolutionOfUrl(resource.url)); - - baseTexture.imageUrl = resource.url; - resource.texture = new core.Texture(baseTexture); - - // lets also add the frame to pixi's global cache for fromFrame and fromImage fucntions - core.utils.BaseTextureCache[resource.name] = baseTexture; - core.utils.TextureCache[resource.name] = resource.texture; - - // also add references by url if they are different. - if (resource.name !== resource.url) - { - core.utils.BaseTextureCache[resource.url] = baseTexture; - core.utils.TextureCache[resource.url] = resource.texture; - } + resource.texture = Texture.fromLoader( + resource.data, + resource.url, + resource.name + ); } - next(); }; } diff --git a/test/core/Container.js b/test/core/Container.js index 8a10b40..d11be33 100644 --- a/test/core/Container.js +++ b/test/core/Container.js @@ -1,5 +1,33 @@ 'use strict'; +function testAddChild(fn) +{ + fn(function (container, obj) + { + container.addChild(obj); + }); + fn(function (container, obj) + { + container.addChildAt(obj); + }); +} + +function testRemoveChild(fn) +{ + fn(function (container, obj) + { + container.removeChild(obj); + }); + fn(function (container, obj) + { + container.removeChildAt(container.children.indexOf(obj)); + }); + fn(function (container, obj) + { + container.removeChildren(container.children.indexOf(obj), container.children.indexOf(obj) + 1); + }); +} + describe('PIXI.Container', function () { describe('parent', function () @@ -70,6 +98,49 @@ expect(spy).to.have.been.called; expect(spy).to.have.been.calledWith(0); }); + + it('should flag child transform and container bounds for recalculation', testAddChild(function (mockAddChild) + { + const container = new PIXI.Container(); + const child = new PIXI.Container(); + + container.getBounds(); + child.getBounds(); + + const boundsID = container._boundsID; + const childParentID = child.transform._parentID; + + mockAddChild(container, child); + + expect(boundsID).to.not.be.equals(container._boundsID); + expect(childParentID).to.not.be.equals(child.transform._parentID); + })); + + it('should recalculate added child correctly', testAddChild(function (mockAddChild) + { + const parent = new PIXI.Container(); + const container = new PIXI.Container(); + const graphics = new PIXI.Graphics(); + + parent.addChild(container); + + graphics.drawRect(0, 0, 10, 10); + container.position.set(100, 200); + container.updateTransform(); + + graphics.getBounds(); + // Oops, that can happen sometimes! + graphics.transform._parentID = container.transform._worldID + 1; + + mockAddChild(container, graphics); + + const bounds = graphics.getBounds(); + + expect(bounds.x).to.be.equal(100); + expect(bounds.y).to.be.equal(200); + expect(bounds.width).to.be.equal(10); + expect(bounds.height).to.be.equal(10); + })); }); describe('removeChildAt', function () @@ -202,6 +273,44 @@ expect(spy).to.have.been.called; expect(spy).to.have.been.calledWith(0); }); + + it('should flag transform for recalculation', testRemoveChild(function (mockRemoveChild) + { + const container = new PIXI.Container(); + const child = new PIXI.Container(); + + container.addChild(child); + container.getBounds(); + + const childParentID = child.transform._parentID; + const boundsID = container._boundsID; + + mockRemoveChild(container, child); + + expect(childParentID).to.not.be.equals(child.transform._parentID); + expect(boundsID).to.not.be.equals(container._boundsID); + })); + + it('should recalculate removed child correctly', testRemoveChild(function (mockRemoveChild) + { + const parent = new PIXI.Container(); + const container = new PIXI.Container(); + const graphics = new PIXI.Graphics(); + + parent.addChild(container); + + graphics.drawRect(0, 0, 10, 10); + container.position.set(100, 200); + container.addChild(graphics); + graphics.getBounds(); + + mockRemoveChild(container, graphics); + + const bounds = graphics.getBounds(); + + expect(bounds.x).to.be.equal(0); + expect(bounds.y).to.be.equal(0); + })); }); describe('getChildIndex', function () diff --git a/test/core/Graphics.js b/test/core/Graphics.js index 98c7584..11e47a0 100644 --- a/test/core/Graphics.js +++ b/test/core/Graphics.js @@ -1,5 +1,8 @@ 'use strict'; +const MockPointer = require('../interaction/MockPointer'); +const withGL = require('../withGL'); + describe('PIXI.Graphics', function () { describe('constructor', function () @@ -218,4 +221,153 @@ expect(spy).to.have.been.calledOnce; }); }); + + describe('mask', function () + { + it('should trigger interaction callback when no mask present', function () + { + const stage = new PIXI.Container(); + const pointer = new MockPointer(stage); + const graphics = new PIXI.Graphics(); + const mask = new PIXI.Graphics(); + const spy = sinon.spy(); + + graphics.interactive = true; + graphics.beginFill(0xFF0000); + graphics.drawRect(0, 0, 50, 50); + graphics.on('click', spy); + stage.addChild(graphics); + mask.beginFill(); + mask.drawRect(0, 0, 50, 50); + graphics.mask = mask; + + pointer.click(10, 10); + + expect(spy).to.have.been.calledOnce; + }); + it('should trigger interaction callback when mask uses beginFill', function () + { + const stage = new PIXI.Container(); + const pointer = new MockPointer(stage); + const graphics = new PIXI.Graphics(); + const mask = new PIXI.Graphics(); + const spy = sinon.spy(); + + graphics.interactive = true; + graphics.beginFill(0xFF0000); + graphics.drawRect(0, 0, 50, 50); + graphics.on('click', spy); + stage.addChild(graphics); + mask.beginFill(); + mask.drawRect(0, 0, 50, 50); + graphics.mask = mask; + + pointer.click(10, 10); + + expect(spy).to.have.been.calledOnce; + }); + + it('should not trigger interaction callback when mask doesn\'t use beginFill', function () + { + const stage = new PIXI.Container(); + const pointer = new MockPointer(stage); + const graphics = new PIXI.Graphics(); + const mask = new PIXI.Graphics(); + const spy = sinon.spy(); + + graphics.interactive = true; + graphics.beginFill(0xFF0000); + graphics.drawRect(0, 0, 50, 50); + graphics.on('click', spy); + stage.addChild(graphics); + mask.drawRect(0, 0, 50, 50); + graphics.mask = mask; + + pointer.click(10, 10); + + expect(spy).to.have.not.been.called; + }); + + it('should trigger interaction callback when mask doesn\'t use beginFill but hitArea is defined', function () + { + const stage = new PIXI.Container(); + const pointer = new MockPointer(stage); + const graphics = new PIXI.Graphics(); + const mask = new PIXI.Graphics(); + const spy = sinon.spy(); + + graphics.interactive = true; + graphics.beginFill(0xFF0000); + graphics.hitArea = new PIXI.Rectangle(0, 0, 50, 50); + graphics.drawRect(0, 0, 50, 50); + graphics.on('click', spy); + stage.addChild(graphics); + mask.drawRect(0, 0, 50, 50); + graphics.mask = mask; + + pointer.click(10, 10); + + expect(spy).to.have.been.calledOnce; + }); + + it('should trigger interaction callback when mask is a sprite', function () + { + const stage = new PIXI.Container(); + const pointer = new MockPointer(stage); + const graphics = new PIXI.Graphics(); + const mask = new PIXI.Graphics(); + const spy = sinon.spy(); + + graphics.interactive = true; + graphics.beginFill(0xFF0000); + graphics.drawRect(0, 0, 50, 50); + graphics.on('click', spy); + stage.addChild(graphics); + mask.drawRect(0, 0, 50, 50); + graphics.mask = new PIXI.Sprite(mask.generateCanvasTexture()); + + pointer.click(10, 10); + + expect(spy).to.have.been.calledOnce; + }); + + it('should calculate tint, alpha and blendMode of fastRect correctly', withGL(function () + { + const renderer = new PIXI.WebGLRenderer(200, 200, {}); + + try + { + const graphics = new PIXI.Graphics(); + + graphics.beginFill(0x102030, 0.6); + graphics.drawRect(2, 3, 100, 100); + graphics.endFill(); + graphics.tint = 0x101010; + graphics.blendMode = 2; + graphics.alpha = 0.3; + + renderer.render(graphics); + + expect(graphics.isFastRect()).to.be.true; + + const sprite = graphics._spriteRect; + + expect(sprite).to.not.be.equals(null); + expect(sprite.worldAlpha).to.equals(0.18); + expect(sprite.blendMode).to.equals(2); + expect(sprite.tint).to.equals(0x010203); + + const bounds = sprite.getBounds(); + + expect(bounds.x).to.equals(2); + expect(bounds.y).to.equals(3); + expect(bounds.width).to.equals(100); + expect(bounds.height).to.equals(100); + } + finally + { + renderer.destroy(); + } + })); + }); }); diff --git a/test/core/Spritesheet.js b/test/core/Spritesheet.js new file mode 100644 index 0000000..283adf5 --- /dev/null +++ b/test/core/Spritesheet.js @@ -0,0 +1,89 @@ +'use strict'; + +const path = require('path'); + +describe('PIXI.Spritesheet', function () +{ + before(function () + { + this.resources = path.join(__dirname, 'resources'); + this.validate = function (spritesheet, done) + { + spritesheet.parse(function (textures) + { + const id = 'goldmine_10_5.png'; + + expect(Object.keys(textures).length).to.equal(1); + expect(Object.keys(spritesheet.textures).length).to.equal(1); + expect(textures[id]).to.be.an.instanceof(PIXI.Texture); + expect(textures[id].width).to.equal(spritesheet.data.frames[id].frame.w / spritesheet.resolution); + expect(textures[id].height).to.equal(spritesheet.data.frames[id].frame.h / spritesheet.resolution); + spritesheet.destroy(true); + expect(spritesheet.textures).to.be.null; + expect(spritesheet.baseTexture).to.be.null; + done(); + }); + }; + }); + + it('should exist on PIXI', function () + { + expect(PIXI.Spritesheet).to.be.a.function; + expect(PIXI.Spritesheet.BATCH_SIZE).to.be.a.number; + }); + + it('should create an instance', function () + { + const baseTexture = new PIXI.BaseTexture(); + const data = { + frames: {}, + meta: {}, + }; + const spritesheet = new PIXI.Spritesheet(baseTexture, data); + + expect(spritesheet.data).to.equal(data); + expect(spritesheet.baseTexture).to.equal(baseTexture); + expect(spritesheet.resolution).to.equal(1); + + spritesheet.destroy(true); + }); + + it('should create instance with scale resolution', function (done) + { + const data = require(path.resolve(this.resources, 'building1.json')); // eslint-disable-line global-require + const image = new Image(); + + image.src = path.join(this.resources, data.meta.image); + image.onload = () => + { + const baseTexture = new PIXI.BaseTexture(image, null, 1); + const spritesheet = new PIXI.Spritesheet(baseTexture, data); + + expect(data).to.be.an.object; + expect(data.meta.image).to.equal('building1.png'); + expect(spritesheet.resolution).to.equal(0.5); + + this.validate(spritesheet, done); + }; + }); + + it('should create instance with filename resolution', function (done) + { + const uri = path.resolve(this.resources, 'building1@2x.json'); + const data = require(uri); // eslint-disable-line global-require + const image = new Image(); + + image.src = path.join(this.resources, data.meta.image); + image.onload = () => + { + const baseTexture = new PIXI.BaseTexture(image, null, 1); + const spritesheet = new PIXI.Spritesheet(baseTexture, data, uri); + + expect(data).to.be.an.object; + expect(data.meta.image).to.equal('building1@2x.png'); + expect(spritesheet.resolution).to.equal(2); + + this.validate(spritesheet, done); + }; + }); +}); diff --git a/test/core/Texture.js b/test/core/Texture.js new file mode 100644 index 0000000..8c4ef98 --- /dev/null +++ b/test/core/Texture.js @@ -0,0 +1,19 @@ +'use strict'; + +describe('PIXI.Texture', function () +{ + it('should register Texture from Loader', function () + { + const URL = 'foo.png'; + const NAME = 'bar'; + const image = new Image(); + + const texture = PIXI.Texture.fromLoader(image, URL, NAME); + + expect(texture.baseTexture.imageUrl).to.equal('foo.png'); + expect(PIXI.utils.TextureCache[NAME]).to.equal(texture); + expect(PIXI.utils.BaseTextureCache[NAME]).to.equal(texture.baseTexture); + expect(PIXI.utils.TextureCache[URL]).to.equal(texture); + expect(PIXI.utils.BaseTextureCache[URL]).to.equal(texture.baseTexture); + }); +}); diff --git a/test/core/WebGLRenderer.js b/test/core/WebGLRenderer.js index 9bc3bc2..3d6cf98 100644 --- a/test/core/WebGLRenderer.js +++ b/test/core/WebGLRenderer.js @@ -1,21 +1,21 @@ 'use strict'; -function isWebGLSupported(fn) -{ - return PIXI.utils.isWebGLSupported() ? fn : function () {}; // eslint-disable-line no-empty-function -} +const withGL = require('../withGL'); -describe('PIXI.WebGLRenderer', isWebGLSupported(function () +describe('PIXI.WebGLRenderer', function () { - it('setting option legacy should disable VAOs and set minimum SPRITE_MAX_TEXTURES to 1', function () + it('setting option legacy should disable VAOs and SPRITE_MAX_TEXTURES', withGL(function () { const renderer = new PIXI.WebGLRenderer(1, 1, { legacy: true }); - - expect(PIXI.glCore.VertexArrayObject.FORCE_NATIVE).to.equal(true); - expect(renderer.plugins.sprite.MAX_TEXTURES).to.equal(1); - - renderer.destroy(); - }); -})); - + try + { + expect(PIXI.glCore.VertexArrayObject.FORCE_NATIVE).to.equal(true); + expect(renderer.plugins.sprite.MAX_TEXTURES).to.equal(1); + } + finally + { + renderer.destroy(); + } + })); +}); diff --git a/test/core/filters.js b/test/core/filters.js new file mode 100644 index 0000000..df459f6 --- /dev/null +++ b/test/core/filters.js @@ -0,0 +1,21 @@ +'use strict'; + +describe('PIXI.filters', function () +{ + it('should correctly form uniformData', function () + { + const sprite = new PIXI.Sprite(PIXI.Texture.EMPTY); + const displ = new PIXI.filters.DisplacementFilter(sprite); + + expect(!!displ.uniformData.scale).to.be.true; + expect(!!displ.uniformData.filterMatrix).to.be.true; + expect(!!displ.uniformData.mapSampler).to.be.true; + // it does have filterClamp, but it is handled by FilterManager + expect(!!displ.uniformData.filterClamp).to.be.false; + + const fxaa = new PIXI.filters.FXAAFilter(); + + // it does have filterArea, but it is handled by FilterManager + expect(!!fxaa.uniformData.filterArea).to.be.false; + }); +}); diff --git a/test/core/index.js b/test/core/index.js index 63a8f21..8530131 100755 --- a/test/core/index.js +++ b/test/core/index.js @@ -7,6 +7,7 @@ require('./DisplayObject'); require('./getLocalBounds'); require('./Sprite'); +require('./Spritesheet'); require('./TilingSprite'); require('./TextStyle'); require('./Text'); @@ -25,3 +26,5 @@ require('./SpriteRenderer'); require('./WebGLRenderer'); require('./Ellipse'); +require('./Texture'); +require('./filters'); diff --git a/test/core/resources/building1.json b/test/core/resources/building1.json new file mode 100755 index 0000000..03fa5c1 --- /dev/null +++ b/test/core/resources/building1.json @@ -0,0 +1,21 @@ +{"frames": { + +"goldmine_10_5.png": +{ + "frame": {"x":1,"y":1,"w":95,"h":115}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":95,"h":115}, + "sourceSize": {"w":95,"h":115}, + "pivot": {"x":0.5,"y":0.5} +}}, +"meta": { + "app": "http://www.codeandweb.com/texturepacker", + "version": "1.0", + "image": "building1.png", + "format": "RGBA8888", + "size": {"w":128,"h":128}, + "scale": "0.5", + "smartupdate": "$TexturePacker:SmartUpdate:d7a5e54c8f8a3fecd508baf190c44807:99a0e3a4dc0f441e4aad77614191ab38:6046b8eb706ddefaa771c33ceb7cb6d5$" +} +} diff --git a/test/core/resources/building1.png b/test/core/resources/building1.png new file mode 100755 index 0000000..7e1e114 --- /dev/null +++ b/test/core/resources/building1.png Binary files differ diff --git a/package.json b/package.json index 6a366ba..71ade15 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pixi.js", - "version": "4.3.4", + "version": "4.3.5", "description": "Pixi.js is a fast lightweight 2D library that works across all devices.", "author": "Mat Groves", "contributors": [ @@ -61,7 +61,7 @@ "ismobilejs": "^0.4.0", "object-assign": "^4.0.1", "pixi-gl-core": "^1.0.3", - "resource-loader": "^2.0.4" + "resource-loader": "^2.0.6" }, "devDependencies": { "babel-cli": "^6.18.0", @@ -70,9 +70,9 @@ "babel-preset-es2015": "^6.14.0", "babelify": "^7.3.0", "del": "^2.2.0", - "electron-prebuilt": "^1.3.2", + "electron": "^1.4.15", "eslint": "^3.5.0", - "floss": "^1.2.0", + "floss": "^2.0.1", "gh-pages": "^0.11.0", "jaguarjs-jsdoc": "^1.0.1", "js-md5": "^0.4.1", diff --git a/src/accessibility/AccessibilityManager.js b/src/accessibility/AccessibilityManager.js index df2716a..f16bcb1 100644 --- a/src/accessibility/AccessibilityManager.js +++ b/src/accessibility/AccessibilityManager.js @@ -3,7 +3,7 @@ import accessibleTarget from './accessibleTarget'; // add some extra variables to the container.. -Object.assign( +core.utils.mixins.delayMixin( core.DisplayObject.prototype, accessibleTarget ); diff --git a/src/core/Application.js b/src/core/Application.js index ea0f058..5340578 100644 --- a/src/core/Application.js +++ b/src/core/Application.js @@ -1,6 +1,6 @@ import { autoDetectRenderer } from './autoDetectRenderer'; import Container from './display/Container'; -import Ticker from './ticker/Ticker'; +import { shared, Ticker } from './ticker'; /** * Convenience class to create a new PIXI application. @@ -35,9 +35,9 @@ * @param {boolean} [noWebGL=false] - prevents selection of WebGL renderer, even if such is present * @param {boolean} [options.legacy=false] - If true Pixi will aim to ensure compatibility * with older / less advanced devices. If you experience unexplained flickering try setting this to true. - + * @param {boolean} [useSharedTicker=false] - `true` to use PIXI.ticker.shared, `false` to create new ticker. */ - constructor(width, height, options, noWebGL) + constructor(width, height, options, noWebGL, useSharedTicker = false) { /** * WebGL renderer if available, otherwise CanvasRenderer @@ -46,23 +46,46 @@ this.renderer = autoDetectRenderer(width, height, options, noWebGL); /** - * The root display container that's renderered. + * The root display container that's rendered. * @member {PIXI.Container} */ this.stage = new Container(); /** + * Internal reference to the ticker + * @member {PIXI.ticker.Ticker} + * @private + */ + this._ticker = null; + + /** * Ticker for doing render updates. * @member {PIXI.ticker.Ticker} + * @default PIXI.ticker.shared */ - this.ticker = new Ticker(); - - this.ticker.add(this.render, this); + this.ticker = useSharedTicker ? shared : new Ticker(); // Start the rendering this.start(); } + set ticker(ticker) // eslint-disable-line require-jsdoc + { + if (this._ticker) + { + this._ticker.remove(this.render, this); + } + this._ticker = ticker; + if (ticker) + { + ticker.add(this.render, this); + } + } + get ticker() // eslint-disable-line require-jsdoc + { + return this._ticker; + } + /** * Render the current stage. */ @@ -76,7 +99,7 @@ */ stop() { - this.ticker.stop(); + this._ticker.stop(); } /** @@ -84,7 +107,7 @@ */ start() { - this.ticker.start(); + this._ticker.start(); } /** @@ -98,13 +121,22 @@ } /** + * Reference to the renderer's screen rectangle. Its safe to use as filterArea or hitArea for whole screen + * @member {PIXI.Rectangle} + * @readonly + */ + get screen() + { + return this.renderer.screen; + } + + /** * Destroy and don't use after this. * @param {Boolean} [removeView=false] Automatically remove canvas from DOM. */ destroy(removeView) { this.stop(); - this.ticker.remove(this.render, this); this.ticker = null; this.stage.destroy(); diff --git a/src/core/Shader.js b/src/core/Shader.js new file mode 100644 index 0000000..78b17e6 --- /dev/null +++ b/src/core/Shader.js @@ -0,0 +1,46 @@ +import { GLShader } from 'pixi-gl-core'; +import settings from './settings'; + +function checkPrecision(src, def) +{ + if (src instanceof Array) + { + if (src[0].substring(0, 9) !== 'precision') + { + const copy = src.slice(0); + + copy.unshift(`precision ${def} float;`); + + return copy; + } + } + else if (src.substring(0, 9) !== 'precision') + { + return `precision ${def} float;\n${src}`; + } + + return src; +} + +/** + * Wrapper class, webGL Shader for Pixi. + * Adds precision string if vertexSrc or fragmentSrc have no mention of it. + * + * @class + * @extends GLShader + * @memberof PIXI + */ +export default class Shader extends GLShader +{ + /** + * + * @param {WebGLRenderingContext} gl - The current WebGL rendering context + * @param {string|string[]} vertexSrc - The vertex shader source as an array of strings. + * @param {string|string[]} fragmentSrc - The fragment shader source as an array of strings. + */ + constructor(gl, vertexSrc, fragmentSrc) + { + super(gl, checkPrecision(vertexSrc, settings.PRECISION_VERTEX), + checkPrecision(fragmentSrc, settings.PRECISION_FRAGMENT)); + } +} diff --git a/src/core/const.js b/src/core/const.js index ef6f662..3de0efe 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -188,7 +188,7 @@ * The gc modes that are supported by pixi. * * The {@link PIXI.settings.GC_MODE} Garbage Collection mode for pixi textures is AUTO - * If set to GC_MODE, the renderer will occasianally check textures usage. If they are not + * If set to GC_MODE, the renderer will occasionally check textures usage. If they are not * used for a specified period of time they will be removed from the GPU. They will of course * be uploaded again when they are required. This is a silent behind the scenes process that * should ensure that the GPU does not get filled up. diff --git a/src/core/display/Container.js b/src/core/display/Container.js index ed8cd5f..17fde8d 100644 --- a/src/core/display/Container.js +++ b/src/core/display/Container.js @@ -73,11 +73,12 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.push(child); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -108,11 +109,12 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.splice(index, 0, child); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -225,10 +227,11 @@ if (index === -1) return null; child.parent = null; + // ensure child transform will be recalculated + child.transform._parentID = -1; removeItems(this.children, index, 1); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -249,11 +252,12 @@ { const child = this.getChildAt(index); + // ensure child transform will be recalculated.. child.parent = null; + child.transform._parentID = -1; removeItems(this.children, index, 1); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -284,13 +288,12 @@ for (let i = 0; i < removed.length; ++i) { removed[i].parent = null; + if (removed[i].transform) + { + removed[i].transform._parentID = -1; + } } - // ensure a transform will be recalculated.. - if (this.transform) - { - this.transform._parentID = -1; - } this._boundsID++; this.onChildrenChange(beginIndex); diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index f37f2f5..be4c4fb 100644 --- a/src/core/graphics/Graphics.js +++ b/src/core/graphics/Graphics.js @@ -28,8 +28,9 @@ { /** * + * @param {boolean} [nativeLines=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP */ - constructor() + constructor(nativeLines = false) { super(); @@ -50,6 +51,13 @@ this.lineWidth = 0; /** + * If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * + * @member {boolean} + */ + this.nativeLines = nativeLines; + + /** * The color of any lines drawn. * * @member {string} @@ -757,28 +765,14 @@ if (!this._spriteRect) { - if (!Graphics._SPRITE_TEXTURE) - { - Graphics._SPRITE_TEXTURE = RenderTexture.create(10, 10); - - const canvas = document.createElement('canvas'); - - canvas.width = 10; - canvas.height = 10; - - const context = canvas.getContext('2d'); - - context.fillStyle = 'white'; - context.fillRect(0, 0, 10, 10); - - Graphics._SPRITE_TEXTURE = Texture.fromCanvas(canvas); - } - - this._spriteRect = new Sprite(Graphics._SPRITE_TEXTURE); + this._spriteRect = new Sprite(new Texture(Texture.WHITE)); } + + const sprite = this._spriteRect; + if (this.tint === 0xffffff) { - this._spriteRect.tint = this.graphicsData[0].fillColor; + sprite.tint = this.graphicsData[0].fillColor; } else { @@ -792,20 +786,21 @@ t1[1] *= t2[1]; t1[2] *= t2[2]; - this._spriteRect.tint = rgb2hex(t1); + sprite.tint = rgb2hex(t1); } - this._spriteRect.alpha = this.graphicsData[0].fillAlpha; - this._spriteRect.worldAlpha = this.worldAlpha * this._spriteRect.alpha; + sprite.alpha = this.graphicsData[0].fillAlpha; + sprite.worldAlpha = this.worldAlpha * sprite.alpha; + sprite.blendMode = this.blendMode; - Graphics._SPRITE_TEXTURE._frame.width = rect.width; - Graphics._SPRITE_TEXTURE._frame.height = rect.height; + sprite.texture._frame.width = rect.width; + sprite.texture._frame.height = rect.height; - this._spriteRect.transform.worldTransform = this.transform.worldTransform; + sprite.transform.worldTransform = this.transform.worldTransform; - this._spriteRect.anchor.set(-rect.x / rect.width, -rect.y / rect.height); - this._spriteRect._onAnchorUpdate(); + sprite.anchor.set(-rect.x / rect.width, -rect.y / rect.height); + sprite._onAnchorUpdate(); - this._spriteRect._renderWebGL(renderer); + sprite._renderWebGL(renderer); } /** @@ -1031,6 +1026,7 @@ this.fillColor, this.fillAlpha, this.filling, + this.nativeLines, shape ); diff --git a/src/core/graphics/GraphicsData.js b/src/core/graphics/GraphicsData.js index f5a87ae..b6f03c0 100644 --- a/src/core/graphics/GraphicsData.js +++ b/src/core/graphics/GraphicsData.js @@ -14,14 +14,19 @@ * @param {number} fillColor - the color of the fill * @param {number} fillAlpha - the alpha of the fill * @param {boolean} fill - whether or not the shape is filled with a colour + * @param {boolean} nativeLines - the method for drawing lines * @param {PIXI.Circle|PIXI.Rectangle|PIXI.Ellipse|PIXI.Polygon} shape - The shape object to draw. */ - constructor(lineWidth, lineColor, lineAlpha, fillColor, fillAlpha, fill, shape) + constructor(lineWidth, lineColor, lineAlpha, fillColor, fillAlpha, fill, nativeLines, shape) { /** * @member {number} the width of the line to draw */ this.lineWidth = lineWidth; + /** + * @member {boolean} if true the liens will be draw using LINES instead of TRIANGLE_STRIP + */ + this.nativeLines = nativeLines; /** * @member {number} the color of the line to draw @@ -85,6 +90,7 @@ this.fillColor, this.fillAlpha, this.fill, + this.nativeLines, this.shape ); } diff --git a/src/core/graphics/webgl/GraphicsRenderer.js b/src/core/graphics/webgl/GraphicsRenderer.js index 14083fe..67f5e92 100644 --- a/src/core/graphics/webgl/GraphicsRenderer.js +++ b/src/core/graphics/webgl/GraphicsRenderer.js @@ -102,7 +102,15 @@ shaderTemp.uniforms.alpha = graphics.worldAlpha; renderer.bindVao(webGLData.vao); - webGLData.vao.draw(gl.TRIANGLE_STRIP, webGLData.indices.length); + + if (graphics.nativeLines) + { + gl.drawArrays(gl.LINES, 0, webGLData.points.length / 6); + } + else + { + webGLData.vao.draw(gl.TRIANGLE_STRIP, webGLData.indices.length); + } } } diff --git a/src/core/graphics/webgl/utils/buildLine.js b/src/core/graphics/webgl/utils/buildLine.js index ecf43f7..4d6862e 100644 --- a/src/core/graphics/webgl/utils/buildLine.js +++ b/src/core/graphics/webgl/utils/buildLine.js @@ -11,7 +11,29 @@ * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties * @param {object} webGLData - an object containing all the webGL-specific information to create this shape */ -export default function buildLine(graphicsData, webGLData) +export default function (graphicsData, webGLData) +{ + if (graphicsData.nativeLines) + { + buildNativeLine(graphicsData, webGLData); + } + else + { + buildLine(graphicsData, webGLData); + } +} + +/** + * Builds a line to draw using the poligon method. + * + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @private + * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties + * @param {object} webGLData - an object containing all the webGL-specific information to create this shape + */ +function buildLine(graphicsData, webGLData) { // TODO OPTIMISE! let points = graphicsData.points; @@ -224,3 +246,46 @@ indices.push(indexStart - 1); } + +/** + * Builds a line to draw using the gl.drawArrays(gl.LINES) method + * + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @private + * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties + * @param {object} webGLData - an object containing all the webGL-specific information to create this shape + */ +function buildNativeLine(graphicsData, webGLData) +{ + let i = 0; + const points = graphicsData.points; + + if (points.length === 0) return; + + const verts = webGLData.points; + const length = points.length / 2; + + // sort color + const color = hex2rgb(graphicsData.lineColor); + const alpha = graphicsData.lineAlpha; + const r = color[0] * alpha; + const g = color[1] * alpha; + const b = color[2] * alpha; + + for (i = 1; i < length; i++) + { + const p1x = points[(i - 1) * 2]; + const p1y = points[((i - 1) * 2) + 1]; + + const p2x = points[i * 2]; + const p2y = points[(i * 2) + 1]; + + verts.push(p1x, p1y); + verts.push(r, g, b, alpha); + + verts.push(p2x, p2y); + verts.push(r, g, b, alpha); + } +} diff --git a/src/core/graphics/webgl/utils/buildRoundedRectangle.js b/src/core/graphics/webgl/utils/buildRoundedRectangle.js index 9abc807..a6c8461 100644 --- a/src/core/graphics/webgl/utils/buildRoundedRectangle.js +++ b/src/core/graphics/webgl/utils/buildRoundedRectangle.js @@ -77,6 +77,26 @@ } /** + * Calculate a single point for a quadratic bezier curve. + * Utility function used by quadraticBezierCurve. + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @private + * @param {number} n1 - first number + * @param {number} n2 - second number + * @param {number} perc - percentage + * @return {number} the result + * + */ +function getPt(n1, n2, perc) +{ + const diff = n2 - n1; + + return n1 + (diff * perc); +} + +/** * Calculate the points for a quadratic bezier curve. (helper function..) * Based on: https://stackoverflow.com/questions/785097/how-do-i-implement-a-bezier-curve-in-c * @@ -105,13 +125,6 @@ let x = 0; let y = 0; - function getPt(n1, n2, perc) - { - const diff = n2 - n1; - - return n1 + (diff * perc); - } - for (let i = 0, j = 0; i <= n; ++i) { j = i / n; diff --git a/src/core/index.js b/src/core/index.js index f7a817c..4f414e8 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -30,6 +30,7 @@ export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; export { default as CanvasGraphicsRenderer } from './graphics/canvas/CanvasGraphicsRenderer'; +export { default as Spritesheet } from './textures/Spritesheet'; export { default as Texture } from './textures/Texture'; export { default as BaseTexture } from './textures/BaseTexture'; export { default as RenderTexture } from './textures/RenderTexture'; diff --git a/src/core/math/Matrix.js b/src/core/math/Matrix.js index 0b77477..3679860 100644 --- a/src/core/math/Matrix.js +++ b/src/core/math/Matrix.js @@ -13,45 +13,50 @@ export default class Matrix { /** - * + * @param {number} [a=1] - x scale + * @param {number} [b=0] - y skew + * @param {number} [c=0] - x skew + * @param {number} [d=1] - y scale + * @param {number} [tx=0] - x translation + * @param {number} [ty=0] - y translation */ - constructor() + constructor(a = 1, b = 0, c = 0, d = 1, tx = 0, ty = 0) { /** * @member {number} * @default 1 */ - this.a = 1; + this.a = a; /** * @member {number} * @default 0 */ - this.b = 0; + this.b = b; /** * @member {number} * @default 0 */ - this.c = 0; + this.c = c; /** * @member {number} * @default 1 */ - this.d = 1; + this.d = d; /** * @member {number} * @default 0 */ - this.tx = 0; + this.tx = tx; /** * @member {number} * @default 0 */ - this.ty = 0; + this.ty = ty; this.array = null; } diff --git a/src/core/renderers/SystemRenderer.js b/src/core/renderers/SystemRenderer.js index fa116a6..a24c775 100644 --- a/src/core/renderers/SystemRenderer.js +++ b/src/core/renderers/SystemRenderer.js @@ -1,5 +1,5 @@ import { sayHello, hex2string, hex2rgb } from '../utils'; -import { Matrix } from '../math'; +import { Matrix, Rectangle } from '../math'; import { RENDERER_TYPE } from '../const'; import settings from '../settings'; import Container from '../display/Container'; @@ -21,8 +21,8 @@ { /** * @param {string} system - The name of the system this renderer is for. - * @param {number} [width=800] - the width of the canvas view - * @param {number} [height=600] - the height of the canvas view + * @param {number} [screenWidth=800] - the width of the screen + * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false @@ -37,7 +37,7 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(system, width, height, options) + constructor(system, screenWidth, screenHeight, options) { super(); @@ -69,20 +69,13 @@ this.type = RENDERER_TYPE.UNKNOWN; /** - * The width of the canvas view + * Measurements of the screen. (0, 0, screenWidth, screenHeight) * - * @member {number} - * @default 800 - */ - this.width = width || 800; - - /** - * The height of the canvas view + * Its safe to use as filterArea or hitArea for whole stage * - * @member {number} - * @default 600 + * @member {PIXI.Rectangle} */ - this.height = height || 600; + this.screen = new Rectangle(0, 0, screenWidth || 800, screenHeight || 600); /** * The canvas element that everything is drawn to @@ -107,7 +100,7 @@ this.transparent = options.transparent; /** - * Whether the render view should be resized automatically + * Whether css dimensions of canvas view should be resized to screen dimensions automatically * * @member {boolean} */ @@ -192,23 +185,48 @@ } /** - * Resizes the canvas view to the specified width and height + * Same as view.width, actual number of pixels in the canvas by horizontal * - * @param {number} width - the new width of the canvas view - * @param {number} height - the new height of the canvas view + * @member {number} + * @readonly + * @default 800 */ - resize(width, height) + get width() { - this.width = width * this.resolution; - this.height = height * this.resolution; + return this.view.width; + } - this.view.width = this.width; - this.view.height = this.height; + /** + * Same as view.height, actual number of pixels in the canvas by vertical + * + * @member {number} + * @readonly + * @default 600 + */ + get height() + { + return this.view.height; + } + + /** + * Resizes the screen and canvas to the specified width and height + * Canvas dimensions are multiplied by resolution + * + * @param {number} screenWidth - the new width of the screen + * @param {number} screenHeight - the new height of the screen + */ + resize(screenWidth, screenHeight) + { + this.screen.width = screenWidth; + this.screen.height = screenHeight; + + this.view.width = screenWidth * this.resolution; + this.view.height = screenHeight * this.resolution; if (this.autoResize) { - this.view.style.width = `${this.width / this.resolution}px`; - this.view.style.height = `${this.height / this.resolution}px`; + this.view.style.width = `${screenWidth}px`; + this.view.style.height = `${screenHeight}px`; } } @@ -249,11 +267,10 @@ this.type = RENDERER_TYPE.UNKNOWN; - this.width = 0; - this.height = 0; - this.view = null; + this.screen = null; + this.resolution = 0; this.transparent = false; diff --git a/src/core/renderers/canvas/CanvasRenderer.js b/src/core/renderers/canvas/CanvasRenderer.js index 1bd2d5e..e84e49c 100644 --- a/src/core/renderers/canvas/CanvasRenderer.js +++ b/src/core/renderers/canvas/CanvasRenderer.js @@ -18,8 +18,8 @@ export default class CanvasRenderer extends SystemRenderer { /** - * @param {number} [width=800] - the width of the canvas view - * @param {number} [height=600] - the height of the canvas view + * @param {number} [screenWidth=800] - the width of the screen + * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false @@ -34,9 +34,9 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(width, height, options = {}) + constructor(screenWidth, screenHeight, options = {}) { - super('Canvas', width, height, options); + super('Canvas', screenWidth, screenHeight, options); this.type = RENDERER_TYPE.CANVAS; @@ -96,7 +96,7 @@ this.context = null; this.renderingToScreen = false; - this.resize(width, height); + this.resize(screenWidth, screenHeight); } /** @@ -283,12 +283,12 @@ * * @extends PIXI.SystemRenderer#resize * - * @param {number} width - The new width of the canvas view - * @param {number} height - The new height of the canvas view + * @param {number} screenWidth - the new width of the screen + * @param {number} screenHeight - the new height of the screen */ - resize(width, height) + resize(screenWidth, screenHeight) { - super.resize(width, height); + super.resize(screenWidth, screenHeight); // reset the scale mode.. oddly this seems to be reset when the canvas is resized. // surely a browser bug?? Let pixi fix that for you.. diff --git a/src/core/renderers/webgl/TextureManager.js b/src/core/renderers/webgl/TextureManager.js index b685c1a..8227289 100644 --- a/src/core/renderers/webgl/TextureManager.js +++ b/src/core/renderers/webgl/TextureManager.js @@ -61,7 +61,7 @@ * Updates and/or Creates a WebGL texture for the renderer's context. * * @param {PIXI.BaseTexture|PIXI.Texture} texture - the texture to update - * @param {Number} location - the location the texture will be bound to. + * @param {number} location - the location the texture will be bound to. * @return {GLTexture} The gl texture. */ updateTexture(texture, location) diff --git a/src/core/renderers/webgl/WebGLRenderer.js b/src/core/renderers/webgl/WebGLRenderer.js index e4cd8a0..830b382 100644 --- a/src/core/renderers/webgl/WebGLRenderer.js +++ b/src/core/renderers/webgl/WebGLRenderer.js @@ -31,8 +31,8 @@ { /** * - * @param {number} [width=0] - the width of the canvas view - * @param {number} [height=0] - the height of the canvas view + * @param {number} [screenWidth=800] - the width of the screen + * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false @@ -53,9 +53,9 @@ * @param {boolean} [options.legacy=false] - If true Pixi will aim to ensure compatibility * with older / less advanced devices. If you experiance unexplained flickering try setting this to true. */ - constructor(width, height, options = {}) + constructor(screenWidth, screenHeight, options = {}) { - super('WebGL', width, height, options); + super('WebGL', screenWidth, screenHeight, options); this.legacy = !!options.legacy; @@ -245,7 +245,7 @@ glCore._testingContext = gl; // setup the width/height properties and gl viewport - this.resize(this.width, this.height); + this.resize(this.screen.width, this.screen.height); } /** @@ -343,16 +343,16 @@ /** * Resizes the webGL view to the specified width and height. * - * @param {number} width - the new width of the webGL view - * @param {number} height - the new height of the webGL view + * @param {number} screenWidth - the new width of the screen + * @param {number} screenHeight - the new height of the screen */ - resize(width, height) + resize(screenWidth, screenHeight) { // if(width * this.resolution === this.width && height * this.resolution === this.height)return; - SystemRenderer.prototype.resize.call(this, width, height); + SystemRenderer.prototype.resize.call(this, screenWidth, screenHeight); - this.rootRenderTarget.resize(width, height); + this.rootRenderTarget.resize(screenWidth, screenHeight); if (this._activeRenderTarget === this.rootRenderTarget) { @@ -396,6 +396,26 @@ } /** + * Erases the render texture and fills the drawing area with a colour + * + * @param {PIXI.RenderTexture} renderTexture - The render texture to clear + * @param {number} [clearColor] - The colour + * @return {PIXI.WebGLRenderer} Returns itself. + */ + clearRenderTexture(renderTexture, clearColor) + { + const baseTexture = renderTexture.baseTexture; + const renderTarget = baseTexture._glRenderTargets[this.CONTEXT_UID]; + + if (renderTarget) + { + renderTarget.clear(clearColor); + } + + return this; + } + + /** * Binds a render texture for rendering * * @param {PIXI.RenderTexture} renderTexture - The render texture to render @@ -474,9 +494,10 @@ * Changes the current GLShader to the one given in parameter * * @param {PIXI.glCore.Shader} shader - the new glShader + * @param {boolean} [autoProject=true] - Whether automatically set the projection matrix * @return {PIXI.WebGLRenderer} Returns itself. */ - _bindGLShader(shader) + _bindGLShader(shader, autoProject) { // TODO cache if (this._activeShader !== shader) @@ -484,8 +505,14 @@ this._activeShader = shader; shader.bind(); - // automatically set the projection matrix - shader.uniforms.projectionMatrix = this._activeRenderTarget.projectionMatrix.toArray(true); + // `autoProject` normally would be a default parameter set to true + // but because of how Babel transpiles default parameters + // it hinders the performance of this method. + if (autoProject !== false) + { + // automatically set the projection matrix + shader.uniforms.projectionMatrix = this._activeRenderTarget.projectionMatrix.toArray(true); + } } return this; diff --git a/src/core/renderers/webgl/WebGLState.js b/src/core/renderers/webgl/WebGLState.js index ba6ecc5..6c846d5 100755 --- a/src/core/renderers/webgl/WebGLState.js +++ b/src/core/renderers/webgl/WebGLState.js @@ -82,18 +82,20 @@ push() { // next state.. - let state = this.stack[++this.stackIndex]; + let state = this.stack[this.stackIndex]; if (!state) { state = this.stack[this.stackIndex] = new Uint8Array(16); } + ++this.stackIndex; + // copy state.. // set active state so we can force overrides of gl state for (let i = 0; i < this.activeState.length; i++) { - this.activeState[i] = state[i]; + state[i] = this.activeState[i]; } } diff --git a/src/core/renderers/webgl/filters/Filter.js b/src/core/renderers/webgl/filters/Filter.js index 2c3eaad..4eb6128 100644 --- a/src/core/renderers/webgl/filters/Filter.js +++ b/src/core/renderers/webgl/filters/Filter.js @@ -9,7 +9,7 @@ * @memberof PIXI * @extends PIXI.Shader */ -class Filter extends Shader +export default class Filter extends Shader { /** * @param {string} [vertexSrc] - The source of the vertex shader. @@ -56,15 +56,16 @@ * @param {PIXI.RenderTarget} input - The input render target. * @param {PIXI.RenderTarget} output - The target to output to. * @param {boolean} clear - Should the output be cleared before rendering to it + * @param {object} [currentState] - It's current state of filter. + * There are some useful properties in the currentState : + * target, filters, sourceFrame, destinationFrame, renderTarget, resolution */ - apply(filterManager, input, output, clear) + apply(filterManager, input, output, clear, currentState) // eslint-disable-line no-unused-vars { // do as you please! - filterManager.applyFilter(this, input, output, clear); + filterManager.applyFilter(this, input, output, clear, currentState); // or just do a regular render.. } } - -export default Filter; diff --git a/src/core/renderers/webgl/filters/extractUniformsFromSrc.js b/src/core/renderers/webgl/filters/extractUniformsFromSrc.js index d78a0b1..4893214 100644 --- a/src/core/renderers/webgl/filters/extractUniformsFromSrc.js +++ b/src/core/renderers/webgl/filters/extractUniformsFromSrc.js @@ -12,7 +12,7 @@ function extractUniformsFromString(string) { - const maskRegex = new RegExp('^(projectionMatrix|uSampler|filterArea)$'); + const maskRegex = new RegExp('^(projectionMatrix|uSampler|filterArea|filterClamp)$'); const uniforms = {}; let nameSplit; diff --git a/src/core/renderers/webgl/managers/FilterManager.js b/src/core/renderers/webgl/managers/FilterManager.js index 43fc869..6c7422d 100644 --- a/src/core/renderers/webgl/managers/FilterManager.js +++ b/src/core/renderers/webgl/managers/FilterManager.js @@ -2,6 +2,7 @@ import RenderTarget from '../utils/RenderTarget'; import Quad from '../utils/Quad'; import { Rectangle } from '../../../math'; +import Shader from '../../../Shader'; import * as filterTransforms from '../filters/filterTransforms'; import bitTwiddle from 'bit-twiddle'; @@ -22,8 +23,6 @@ this.filters = []; this.target = null; this.resolution = 1; - - this.firstRun = true; } } @@ -155,7 +154,7 @@ if (filters.length === 1) { - filters[0].apply(this, currentState.renderTarget, lastState.renderTarget, false); + filters[0].apply(this, currentState.renderTarget, lastState.renderTarget, false, currentState); this.freePotRenderTarget(currentState.renderTarget); } else @@ -177,7 +176,7 @@ for (i = 0; i < filters.length - 1; ++i) { - filters[i].apply(this, flip, flop, true); + filters[i].apply(this, flip, flop, true, currentState); const t = flip; @@ -185,7 +184,7 @@ flop = t; } - filters[i].apply(this, flip, lastState.renderTarget, false); + filters[i].apply(this, flip, lastState.renderTarget, false, currentState); this.freePotRenderTarget(flip); this.freePotRenderTarget(flop); @@ -212,16 +211,28 @@ const renderer = this.renderer; const gl = renderer.gl; - renderer.bindShader(filter, true); + let shader = filter.glShaders[renderer.CONTEXT_UID]; - // TODO theres a better way! - // quad can be a mesh and we then should be able to pull this off without accessing the shader directly - const shader = renderer.shaderManager.getGLShader(); - - // TODO change quad to mesh.. - if (!this.firstRun) + // cacheing.. + if (!shader) { - this.firstRun = true; + if (filter.glShaderKey) + { + shader = this.shaderCache[filter.glShaderKey]; + + if (!shader) + { + shader = new Shader(this.gl, filter.vertexSrc, filter.fragmentSrc); + + filter.glShaders[renderer.CONTEXT_UID] = this.shaderCache[filter.glShaderKey] = shader; + } + } + else + { + shader = filter.glShaders[renderer.CONTEXT_UID] = new Shader(this.gl, filter.vertexSrc, filter.fragmentSrc); + } + + // TODO - this only needs to be done once? renderer.bindVao(null); this.quad.initVao(shader); @@ -244,58 +255,146 @@ renderer.maskManager.pushScissorMask(null, renderer.maskManager.scissorData); } - // renderer._bindGLShader(shader); + renderer.bindShader(shader); - let currentState; - const uniforms = filter.uniforms; + // free unit 0 for us, doesn't matter what was there + // don't try to restore it, because syncUniforms can upload it to another slot + // and it'll be a problem + const tex = this.renderer.emptyTextures[0]; - if (uniforms.filterArea) - { - currentState = this.filterData.stack[this.filterData.index]; - - const filterArea = uniforms.filterArea; - - filterArea[0] = currentState.renderTarget.size.width; - filterArea[1] = currentState.renderTarget.size.height; - filterArea[2] = currentState.sourceFrame.x; - filterArea[3] = currentState.sourceFrame.y; - - filter.uniforms.filterArea = filterArea; - } - - // use this to clamp displaced texture coords so they belong to filterArea - // see displacementFilter fragment shader for an example - if (uniforms.filterClamp) - { - currentState = this.filterData.stack[this.filterData.index]; - - const filterClamp = uniforms.filterClamp; - - filterClamp[0] = 0; - filterClamp[1] = 0; - filterClamp[2] = (currentState.sourceFrame.width - 1) / currentState.renderTarget.size.width; - filterClamp[3] = (currentState.sourceFrame.height - 1) / currentState.renderTarget.size.height; - - filter.uniforms.filterClamp = filterClamp; - } - - renderer.shaderManager.setUniforms(filter.uniforms); + this.renderer.boundTextures[0] = tex; + // this syncs the pixi filters uniforms with glsl uniforms + this.syncUniforms(shader, filter); renderer.state.setBlendMode(filter.blendMode); - // temporary bypass cache.. - const tex = this.renderer.boundTextures[0]; - gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, input.texture.texture); this.quad.vao.draw(this.renderer.gl.TRIANGLES, 6, 0); - // restore cache. gl.bindTexture(gl.TEXTURE_2D, tex._glTextures[this.renderer.CONTEXT_UID].texture); } /** + * Uploads the uniforms of the filter. + * + * @param {GLShader} shader - The underlying gl shader. + * @param {PIXI.Filter} filter - The filter we are synchronizing. + */ + syncUniforms(shader, filter) + { + const uniformData = filter.uniformData; + const uniforms = filter.uniforms; + + // 0 is reserved for the pixi texture so we start at 1! + let textureCount = 1; + let currentState; + + // filterArea and filterClamp that are handled by FilterManager directly + // they must not appear in uniformData + + if (shader.uniforms.filterArea) + { + currentState = this.filterData.stack[this.filterData.index]; + + const filterArea = shader.uniforms.filterArea; + + filterArea[0] = currentState.renderTarget.size.width; + filterArea[1] = currentState.renderTarget.size.height; + filterArea[2] = currentState.sourceFrame.x; + filterArea[3] = currentState.sourceFrame.y; + + shader.uniforms.filterArea = filterArea; + } + + // use this to clamp displaced texture coords so they belong to filterArea + // see displacementFilter fragment shader for an example + if (shader.uniforms.filterClamp) + { + currentState = currentState || this.filterData.stack[this.filterData.index]; + + const filterClamp = shader.uniforms.filterClamp; + + filterClamp[0] = 0; + filterClamp[1] = 0; + filterClamp[2] = (currentState.sourceFrame.width - 1) / currentState.renderTarget.size.width; + filterClamp[3] = (currentState.sourceFrame.height - 1) / currentState.renderTarget.size.height; + + shader.uniforms.filterClamp = filterClamp; + } + + // TODO Cacheing layer.. + for (const i in uniformData) + { + if (uniformData[i].type === 'sampler2D' && uniforms[i] !== 0) + { + if (uniforms[i].baseTexture) + { + shader.uniforms[i] = this.renderer.bindTexture(uniforms[i].baseTexture, textureCount); + } + else + { + shader.uniforms[i] = textureCount; + + // TODO + // this is helpful as renderTargets can also be set. + // Although thinking about it, we could probably + // make the filter texture cache return a RenderTexture + // rather than a renderTarget + const gl = this.renderer.gl; + + this.renderer.boundTextures[textureCount] = this.renderer.emptyTextures[textureCount]; + gl.activeTexture(gl.TEXTURE0 + textureCount); + + uniforms[i].texture.bind(); + } + + textureCount++; + } + else if (uniformData[i].type === 'mat3') + { + // check if its pixi matrix.. + if (uniforms[i].a !== undefined) + { + shader.uniforms[i] = uniforms[i].toArray(true); + } + else + { + shader.uniforms[i] = uniforms[i]; + } + } + else if (uniformData[i].type === 'vec2') + { + // check if its a point.. + if (uniforms[i].x !== undefined) + { + const val = shader.uniforms[i] || new Float32Array(2); + + val[0] = uniforms[i].x; + val[1] = uniforms[i].y; + shader.uniforms[i] = val; + } + else + { + shader.uniforms[i] = uniforms[i]; + } + } + else if (uniformData[i].type === 'float') + { + if (shader.uniforms.data[i].value !== uniformData[i]) + { + shader.uniforms[i] = uniforms[i]; + } + } + else + { + shader.uniforms[i] = uniforms[i]; + } + } + } + + /** * Gets a render target from the pool, or creates a new one. * * @param {boolean} clear - Should we clear the render texture when we get it? @@ -442,7 +541,6 @@ renderTarget.resolution = resolution; renderTarget.defaultFrame.width = renderTarget.size.width = minWidth / resolution; renderTarget.defaultFrame.height = renderTarget.size.height = minHeight / resolution; - renderTarget.attachStencilBuffer(); return renderTarget; } diff --git a/src/core/settings.js b/src/core/settings.js index bb08cb0..f4dd087 100644 --- a/src/core/settings.js +++ b/src/core/settings.js @@ -77,11 +77,11 @@ * * @static * @memberof PIXI.settings - * @type {RegExp|string} + * @type {RegExp} * @example `@2x` - * @default /@(.+)x/ + * @default /@([0-9\.]+)x/ */ - RETINA_PREFIX: /@(.+)x/, + RETINA_PREFIX: /@([0-9\.]+)x/, /** * The default render options if none are supplied to {@link PIXI.WebGLRenderer} @@ -175,14 +175,24 @@ SCALE_MODE: 0, /** - * Default specify float precision in shaders. + * Default specify float precision in vertex shader. + * + * @static + * @memberof PIXI.settings + * @type {PIXI.PRECISION} + * @default PIXI.PRECISION.HIGH + */ + PRECISION_VERTEX: 'highp', + + /** + * Default specify float precision in fragment shader. * * @static * @memberof PIXI.settings * @type {PIXI.PRECISION} * @default PIXI.PRECISION.MEDIUM */ - PRECISION: 'mediump', + PRECISION_FRAGMENT: 'mediump', /** * Can we upload the same buffer in a single frame? diff --git a/src/core/sprites/Sprite.js b/src/core/sprites/Sprite.js index 28b0b18..121bf58 100644 --- a/src/core/sprites/Sprite.js +++ b/src/core/sprites/Sprite.js @@ -212,11 +212,11 @@ } else { - w0 = orig.width * (1 - anchor._x); - w1 = orig.width * -anchor._x; + w1 = -anchor._x * orig.width; + w0 = w1 + orig.width; - h0 = orig.height * (1 - anchor._y); - h1 = orig.height * -anchor._y; + h1 = -anchor._y * orig.height; + h0 = h1 + orig.height; } // xy @@ -269,11 +269,11 @@ const tx = wt.tx; const ty = wt.ty; - const w0 = (orig.width) * (1 - anchor._x); - const w1 = (orig.width) * -anchor._x; + const w1 = -anchor._x * orig.width; + const w0 = w1 + orig.width; - const h0 = orig.height * (1 - anchor._y); - const h1 = orig.height * -anchor._y; + const h1 = -anchor._y * orig.height; + const h0 = h1 + orig.height; // xy vertexData[0] = (a * w1) + (c * h1) + tx; @@ -440,7 +440,7 @@ * * @static * @param {number|string|PIXI.BaseTexture|HTMLCanvasElement|HTMLVideoElement} source Source to create texture from - * @return {PIXI.Sprite} The newly created texture + * @return {PIXI.Sprite} The newly created sprite */ static from(source) { @@ -538,8 +538,8 @@ } /** - * The tint applied to the sprite. This is a hex value. A value of - * 0xFFFFFF will remove any tint effect. + * The tint applied to the sprite. This is a hex value. + * A value of 0xFFFFFF will remove any tint effect. * * @member {number} * @default 0xFFFFFF diff --git a/src/core/sprites/canvas/CanvasTinter.js b/src/core/sprites/canvas/CanvasTinter.js index 1cf316b..27e5e0c 100644 --- a/src/core/sprites/canvas/CanvasTinter.js +++ b/src/core/sprites/canvas/CanvasTinter.js @@ -75,8 +75,8 @@ crop.width *= resolution; crop.height *= resolution; - canvas.width = crop.width; - canvas.height = crop.height; + canvas.width = Math.ceil(crop.width); + canvas.height = Math.ceil(crop.height); context.fillStyle = `#${(`00000${(color | 0).toString(16)}`).substr(-6)}`; @@ -130,8 +130,8 @@ crop.width *= resolution; crop.height *= resolution; - canvas.width = crop.width; - canvas.height = crop.height; + canvas.width = Math.ceil(crop.width); + canvas.height = Math.ceil(crop.height); context.globalCompositeOperation = 'copy'; context.fillStyle = `#${(`00000${(color | 0).toString(16)}`).substr(-6)}`; @@ -172,8 +172,8 @@ crop.width *= resolution; crop.height *= resolution; - canvas.width = crop.width; - canvas.height = crop.height; + canvas.width = Math.ceil(crop.width); + canvas.height = Math.ceil(crop.height); context.globalCompositeOperation = 'copy'; context.drawImage( diff --git a/src/core/sprites/webgl/SpriteRenderer.js b/src/core/sprites/webgl/SpriteRenderer.js index 2affcd5..7c352e2 100644 --- a/src/core/sprites/webgl/SpriteRenderer.js +++ b/src/core/sprites/webgl/SpriteRenderer.js @@ -30,7 +30,7 @@ /** * Number of values sent in the vertex buffer. - * positionX, positionY, colorR, colorG, colorB = 5 + * aVertexPosition(2), aTextureCoord(1), aColor(1), aTextureId(1) = 5 * * @member {number} */ @@ -75,7 +75,6 @@ this.shader = null; this.currentIndex = 0; - TICK = 0; this.groups = []; for (let k = 0; k < this.size; k++) diff --git a/src/core/textures/BaseTexture.js b/src/core/textures/BaseTexture.js index 853de7e..aeccf39 100644 --- a/src/core/textures/BaseTexture.js +++ b/src/core/textures/BaseTexture.js @@ -232,16 +232,24 @@ this.realWidth = this.source.naturalWidth || this.source.videoWidth || this.source.width; this.realHeight = this.source.naturalHeight || this.source.videoHeight || this.source.height; - this.width = this.realWidth / this.resolution; - this.height = this.realHeight / this.resolution; - - this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); + this._updateDimensions(); } this.emit('update', this); } /** + * Update dimensions from real values + */ + _updateDimensions() + { + this.width = this.realWidth / this.resolution; + this.height = this.realHeight / this.resolution; + + this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); + } + + /** * Load a source. * * If the source is not-immediately-available, such as an image that needs to be @@ -525,11 +533,7 @@ this.realWidth = Math.round(svgWidth * this.sourceScale); this.realHeight = Math.round(svgHeight * this.sourceScale); - this.width = this.realWidth / this.resolution; - this.height = this.realHeight / this.resolution; - - // Check pow2 after scale - this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); + this._updateDimensions(); // Create a canvas element const canvas = document.createElement('canvas'); @@ -690,4 +694,52 @@ return baseTexture; } + + /** + * Helper function that creates a base texture based on the source you provide. + * The source can be - image url, image element, canvas element. + * + * @static + * @param {string|HTMLImageElement|HTMLCanvasElement} source - The source to create base texture from. + * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values + * @param {number} [sourceScale=(auto)] - Scale for the original image, used with Svg images. + * @return {PIXI.BaseTexture} The new base texture. + */ + static from(source, scaleMode, sourceScale) + { + if (typeof source === 'string') + { + return BaseTexture.fromImage(source, undefined, scaleMode, sourceScale); + } + else if (source instanceof HTMLImageElement) + { + const imageUrl = source.src; + let baseTexture = BaseTextureCache[imageUrl]; + + if (!baseTexture) + { + baseTexture = new BaseTexture(source, scaleMode); + baseTexture.imageUrl = imageUrl; + + if (sourceScale) + { + baseTexture.sourceScale = sourceScale; + } + + // if there is an @2x at the end of the url we are going to assume its a highres image + baseTexture.resolution = getResolutionOfUrl(imageUrl); + + BaseTextureCache[imageUrl] = baseTexture; + } + + return baseTexture; + } + else if (source instanceof HTMLCanvasElement) + { + return BaseTexture.fromCanvas(source, scaleMode); + } + + // lets assume its a base texture! + return source; + } } diff --git a/src/core/textures/Spritesheet.js b/src/core/textures/Spritesheet.js new file mode 100644 index 0000000..d469c62 --- /dev/null +++ b/src/core/textures/Spritesheet.js @@ -0,0 +1,274 @@ +import { Rectangle, Texture } from '../'; +import { getResolutionOfUrl, TextureCache } from '../utils'; + +/** + * Utility class for maintaining reference to a collection + * of Textures on a single Spritesheet. + * + * @class + * @memberof PIXI + */ +export default class Spritesheet +{ + /** + * The maximum number of Textures to build per process. + * + * @type {number} + * @default 1000 + */ + static get BATCH_SIZE() + { + return 1000; + } + + /** + * @param {PIXI.BaseTexture} baseTexture Reference to the source BaseTexture object. + * @param {Object} data - Spritesheet image data. + * @param {string} [resolutionFilename] - The filename to consider when determining + * the resolution of the spritesheet. If not provided, the imageUrl will + * be used on the BaseTexture. + */ + constructor(baseTexture, data, resolutionFilename = null) + { + /** + * Reference to ths source texture + * @type {PIXI.BaseTexture} + */ + this.baseTexture = baseTexture; + + /** + * Map of spritesheet textures. + * @type {Object} + */ + this.textures = {}; + + /** + * Reference to the original JSON data. + * @type {Object} + */ + this.data = data; + + /** + * The resolution of the spritesheet. + * @type {number} + */ + this.resolution = this._updateResolution( + resolutionFilename || this.baseTexture.imageUrl + ); + + /** + * Map of spritesheet frames. + * @type {Object} + * @private + */ + this._frames = this.data.frames; + + /** + * Collection of frame names. + * @type {string[]} + * @private + */ + this._frameKeys = Object.keys(this._frames); + + /** + * Current batch index being processed. + * @type {number} + * @private + */ + this._batchIndex = 0; + + /** + * Callback when parse is completed. + * @type {Function} + * @private + */ + this._callback = null; + } + + /** + * Generate the resolution from the filename or fallback + * to the meta.scale field of the JSON data. + * + * @private + * @param {string} resolutionFilename - The filename to use for resolving + * the default resolution. + * @return {number} Resolution to use for spritesheet. + */ + _updateResolution(resolutionFilename) + { + const scale = this.data.meta.scale; + + // Use a defaultValue of `null` to check if a url-based resolution is set + let resolution = getResolutionOfUrl(resolutionFilename, null); + + // No resolution found via URL + if (resolution === null) + { + // Use the scale value or default to 1 + resolution = scale !== undefined ? parseFloat(scale) : 1; + } + + // For non-1 resolutions, update baseTexture + if (resolution !== 1) + { + this.baseTexture.resolution = resolution; + this.baseTexture.update(); + } + + return resolution; + } + + /** + * Parser spritesheet from loaded data. This is done asynchronously + * to prevent creating too many Texture within a single process. + * + * @param {Function} callback - Callback when complete returns + * a map of the Textures for this spritesheet. + */ + parse(callback) + { + this._batchIndex = 0; + this._callback = callback; + + if (this._frameKeys.length <= Spritesheet.BATCH_SIZE) + { + this._processFrames(0); + this._parseComplete(); + } + else + { + this._nextBatch(); + } + } + + /** + * Process a batch of frames + * + * @private + * @param {number} initialFrameIndex - The index of frame to start. + */ + _processFrames(initialFrameIndex) + { + let frameIndex = initialFrameIndex; + const maxFrames = Spritesheet.BATCH_SIZE; + + while (frameIndex - initialFrameIndex < maxFrames && frameIndex < this._frameKeys.length) + { + const i = this._frameKeys[frameIndex]; + const rect = this._frames[i].frame; + + if (rect) + { + let frame = null; + let trim = null; + const orig = new Rectangle( + 0, + 0, + this._frames[i].sourceSize.w / this.resolution, + this._frames[i].sourceSize.h / this.resolution + ); + + if (this._frames[i].rotated) + { + frame = new Rectangle( + rect.x / this.resolution, + rect.y / this.resolution, + rect.h / this.resolution, + rect.w / this.resolution + ); + } + else + { + frame = new Rectangle( + rect.x / this.resolution, + rect.y / this.resolution, + rect.w / this.resolution, + rect.h / this.resolution + ); + } + + // Check to see if the sprite is trimmed + if (this._frames[i].trimmed) + { + trim = new Rectangle( + this._frames[i].spriteSourceSize.x / this.resolution, + this._frames[i].spriteSourceSize.y / this.resolution, + rect.w / this.resolution, + rect.h / this.resolution + ); + } + + this.textures[i] = new Texture( + this.baseTexture, + frame, + orig, + trim, + this._frames[i].rotated ? 2 : 0 + ); + + // lets also add the frame to pixi's global cache for fromFrame and fromImage functions + TextureCache[i] = this.textures[i]; + } + + frameIndex++; + } + } + + /** + * The parse has completed. + * + * @private + */ + _parseComplete() + { + const callback = this._callback; + + this._callback = null; + this._batchIndex = 0; + callback.call(this, this.textures); + } + + /** + * Begin the next batch of textures. + * + * @private + */ + _nextBatch() + { + this._processFrames(this._batchIndex * Spritesheet.BATCH_SIZE); + this._batchIndex++; + setTimeout(() => + { + if (this._batchIndex * Spritesheet.BATCH_SIZE < this._frameKeys.length) + { + this._nextBatch(); + } + else + { + this._parseComplete(); + } + }, 0); + } + + /** + * Destroy Spritesheet and don't use after this. + * + * @param {boolean} [destroyBase=false] Whether to destroy the base texture as well + */ + destroy(destroyBase = false) + { + for (const i in this.textures) + { + this.textures[i].destroy(); + } + this._frames = null; + this._frameKeys = null; + this.data = null; + this.textures = null; + if (destroyBase) + { + this.baseTexture.destroy(); + } + this.baseTexture = null; + } +} diff --git a/src/core/textures/Texture.js b/src/core/textures/Texture.js index 6403d4f..afe4254 100644 --- a/src/core/textures/Texture.js +++ b/src/core/textures/Texture.js @@ -3,7 +3,7 @@ import TextureUvs from './TextureUvs'; import EventEmitter from 'eventemitter3'; import { Rectangle } from '../math'; -import { TextureCache, BaseTextureCache } from '../utils'; +import { TextureCache, BaseTextureCache, getResolutionOfUrl } from '../utils'; /** * A texture stores the information that represents an image or part of an image. It cannot be added @@ -365,7 +365,8 @@ * The source can be - frame id, image url, video url, canvas element, video element, base texture * * @static - * @param {number|string|PIXI.BaseTexture|HTMLCanvasElement|HTMLVideoElement} source - Source to create texture from + * @param {number|string|HTMLImageElement|HTMLCanvasElement|HTMLVideoElement|PIXI.BaseTexture} + * source - Source to create texture from * @return {PIXI.Texture} The newly created texture */ static from(source) @@ -393,7 +394,7 @@ } else if (source instanceof HTMLImageElement) { - return new Texture(new BaseTexture(source)); + return new Texture(BaseTexture.from(source)); } else if (source instanceof HTMLCanvasElement) { @@ -413,6 +414,43 @@ } /** + * Create a texture from a source and add to the cache. + * + * @static + * @param {HTMLImageElement|HTMLCanvasElement} source - The input source. + * @param {String} imageUrl - File name of texture, for cache and resolving resolution. + * @param {String} [name] - Human readible name for the texture cache. If no name is + * specified, only `imageUrl` will be used as the cache ID. + * @return {PIXI.Texture} Output texture + */ + static fromLoader(source, imageUrl, name) + { + const baseTexture = new BaseTexture(source, null, getResolutionOfUrl(imageUrl)); + const texture = new Texture(baseTexture); + + baseTexture.imageUrl = imageUrl; + + // No name, use imageUrl instead + if (!name) + { + name = imageUrl; + } + + // lets also add the frame to pixi's global cache for fromFrame and fromImage fucntions + BaseTextureCache[name] = baseTexture; + TextureCache[name] = texture; + + // also add references by url if they are different. + if (name !== imageUrl) + { + BaseTextureCache[imageUrl] = baseTexture; + TextureCache[imageUrl] = texture; + } + + return texture; + } + + /** * Adds a texture to the global TextureCache. This cache is shared across the whole PIXI object. * * @static @@ -522,6 +560,29 @@ } } +function createWhiteTexture() +{ + const canvas = document.createElement('canvas'); + + canvas.width = 10; + canvas.height = 10; + + const context = canvas.getContext('2d'); + + context.fillStyle = 'white'; + context.fillRect(0, 0, 10, 10); + + return new Texture(new BaseTexture(canvas)); +} + +function removeAllHandlers(tex) +{ + tex.destroy = function _emptyDestroy() { /* empty */ }; + tex.on = function _emptyOn() { /* empty */ }; + tex.once = function _emptyOnce() { /* empty */ }; + tex.emit = function _emptyEmit() { /* empty */ }; +} + /** * An empty texture, used often to not have to create multiple empty textures. * Can not be destroyed. @@ -530,7 +591,14 @@ * @constant */ Texture.EMPTY = new Texture(new BaseTexture()); -Texture.EMPTY.destroy = function _emptyDestroy() { /* empty */ }; -Texture.EMPTY.on = function _emptyOn() { /* empty */ }; -Texture.EMPTY.once = function _emptyOnce() { /* empty */ }; -Texture.EMPTY.emit = function _emptyEmit() { /* empty */ }; +removeAllHandlers(Texture.EMPTY); + +/** + * A white texture of 10x10 size, used for graphics and other things + * Can not be destroyed. + * + * @static + * @constant + */ +Texture.WHITE = createWhiteTexture(); +removeAllHandlers(Texture.WHITE); diff --git a/src/core/ticker/Ticker.js b/src/core/ticker/Ticker.js index d98a1f0..17a1517 100644 --- a/src/core/ticker/Ticker.js +++ b/src/core/ticker/Ticker.js @@ -67,11 +67,12 @@ * is based, this value is neither capped nor scaled. * If the platform supports DOMHighResTimeStamp, * this value will have a precision of 1 µs. + * Defaults to target frame time * * @member {number} - * @default 1 / TARGET_FPMS + * @default 16.66 */ - this.elapsedMS = 1 / settings.TARGET_FPMS; // default to target frame time + this.elapsedMS = 1 / settings.TARGET_FPMS; /** * The last time {@link PIXI.ticker.Ticker#update} was invoked. diff --git a/src/core/utils/index.js b/src/core/utils/index.js index 6ff609f..be44750 100644 --- a/src/core/utils/index.js +++ b/src/core/utils/index.js @@ -2,6 +2,7 @@ import settings from '../settings'; import EventEmitter from 'eventemitter3'; import pluginTarget from './pluginTarget'; +import * as mixins from './mixin'; import * as isMobile from 'ismobilejs'; let nextUid = 0; @@ -33,6 +34,7 @@ * @type {mixin} */ pluginTarget, + mixins, }; /** @@ -93,7 +95,7 @@ */ export function rgb2hex(rgb) { - return (((rgb[0] * 255) << 16) + ((rgb[1] * 255) << 8) + (rgb[2] * 255)); + return (((rgb[0] * 255) << 16) + ((rgb[1] * 255) << 8) + (rgb[2] * 255 | 0)); } /** @@ -368,3 +370,43 @@ * @private */ export const ProgramCache = {}; + +/** + * Destroys all texture in the cache + * + * @memberof PIXI.utils + * @function destroyTextureCache + */ +export function destroyTextureCache() +{ + let key; + + for (key in TextureCache) + { + TextureCache[key].destroy(); + } + for (key in BaseTextureCache) + { + BaseTextureCache[key].destroy(); + } +} + +/** + * Removes all textures from cache, but does not destroy them + * + * @memberof PIXI.utils + * @function clearTextureCache + */ +export function clearTextureCache() +{ + let key; + + for (key in TextureCache) + { + delete TextureCache[key]; + } + for (key in BaseTextureCache) + { + delete BaseTextureCache[key]; + } +} diff --git a/src/core/utils/mixin.js b/src/core/utils/mixin.js new file mode 100644 index 0000000..114de2a --- /dev/null +++ b/src/core/utils/mixin.js @@ -0,0 +1,59 @@ +/** + * Mixes all enumerable properties and methods from a source object to a target object. + * + * @memberof PIXI.utils.mixins + * @function mixin + * @param {object} target The prototype or instance that properties and methods should be added to. + * @param {object} source The source of properties and methods to mix in. + */ +export function mixin(target, source) +{ + if (!target || !source) return; + // in ES8/ES2017, this would be really easy: + // Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); + + // get all the enumerable property keys + const keys = Object.keys(source); + + // loop through properties + for (let i = 0; i < keys.length; ++i) + { + const propertyName = keys[i]; + + // Set the property using the property descriptor - this works for accessors and normal value properties + Object.defineProperty(target, propertyName, Object.getOwnPropertyDescriptor(source, propertyName)); + } +} + +const mixins = []; + +/** + * Queues a mixin to be handled towards the end of the initialization of PIXI, so that deprecation + * can take effect. + * + * @memberof PIXI.utils.mixins + * @function delayMixin + * @private + * @param {object} target The prototype or instance that properties and methods should be added to. + * @param {object} source The source of properties and methods to mix in. + */ +export function delayMixin(target, source) +{ + mixins.push(target, source); +} + +/** + * Handles all mixins queued via delayMixin(). + * + * @memberof PIXI.utils.mixins + * @function performMixins + * @private + */ +export function performMixins() +{ + for (let i = 0; i < mixins.length; i += 2) + { + mixin(mixins[i], mixins[i + 1]); + } + mixins.length = 0; +} diff --git a/src/deprecation.js b/src/deprecation.js index 045a19f..b9c73b3 100644 --- a/src/deprecation.js +++ b/src/deprecation.js @@ -5,6 +5,7 @@ import * as filters from './filters'; import * as prepare from './prepare'; import * as loaders from './loaders'; +import * as interaction from './interaction'; // provide method to give a stack track for warnings // useful for tracking-down where deprecated methods/properties/classes @@ -534,7 +535,7 @@ { parent: 'GC_MODES', target: 'GC_MODE' }, { parent: 'WRAP_MODES', target: 'WRAP_MODE' }, { parent: 'SCALE_MODES', target: 'SCALE_MODE' }, - { parent: 'PRECISION', target: 'PRECISION' }, + { parent: 'PRECISION', target: 'PRECISION_FRAGMENT' }, ]; for (let i = 0; i < defaults.length; i++) @@ -558,6 +559,32 @@ }); } +Object.defineProperties(core.settings, { + + /** + * @static + * @name PRECISION + * @memberof PIXI.settings + * @see PIXI.PRECISION + * @deprecated since version 4.4.0 + */ + PRECISION: { + enumerable: true, + get() + { + warn('PIXI.settings.PRECISION has been deprecated, please use PIXI.settings.PRECISION_FRAGMENT'); + + return core.settings.PRECISION_FRAGMENT; + }, + set(value) + { + warn('PIXI.settings.PRECISION has been deprecated, please use PIXI.settings.PRECISION_FRAGMENT'); + + core.settings.PRECISION_FRAGMENT = value; + }, + }, +}); + Object.defineProperties(extras, { /** @@ -694,7 +721,7 @@ warn('determineFontProperties is now deprecated, please use the static calculateFontProperties method, ' + 'e.g : Text.calculateFontProperties(fontStyle);'); - return Text.calculateFontProperties(fontStyle); + return core.Text.calculateFontProperties(fontStyle); }; Object.defineProperties(core.TextStyle.prototype, { @@ -1003,3 +1030,69 @@ }, }, }); + +/** + * @name PIXI.interaction.interactiveTarget#defaultCursor + * @static + * @type {number} + * @see PIXI.interaction.interactiveTarget#cursor + * @deprecated since 4.3.0 + */ +Object.defineProperty(interaction.interactiveTarget, 'defaultCursor', { + set(value) + { + warn('Property defaultCursor has been replaced with \'cursor\'. '); + this.cursor = value; + }, + get() + { + warn('Property defaultCursor has been replaced with \'cursor\'. '); + + return this.cursor; + }, + enumerable: true, +}); + +/** + * @name PIXI.interaction.InteractionManager#defaultCursorStyle + * @static + * @type {string} + * @see PIXI.interaction.InteractionManager#cursorStyles + * @deprecated since 4.3.0 + */ +Object.defineProperty(interaction.InteractionManager, 'defaultCursorStyle', { + set(value) + { + warn('Property defaultCursorStyle has been replaced with \'cursorStyles.default\'. '); + this.cursorStyles.default = value; + }, + get() + { + warn('Property defaultCursorStyle has been replaced with \'cursorStyles.default\'. '); + + return this.cursorStyles.default; + }, +}); + +/** + * @name PIXI.interaction.InteractionManager#currentCursorStyle + * @static + * @type {string} + * @see PIXI.interaction.InteractionManager#cursorStyles + * @deprecated since 4.3.0 + */ +Object.defineProperty(interaction.InteractionManager, 'currentCursorStyle', { + set(value) + { + warn('Property currentCursorStyle has been removed.' + + 'See the currentCursorMode property, which works differently.'); + this.currentCursorMode = value; + }, + get() + { + warn('Property currentCursorStyle has been removed.' + + 'See the currentCursorMode property, which works differently.'); + + return this.currentCursorMode; + }, +}); diff --git a/src/extras/BitmapText.js b/src/extras/BitmapText.js index fb6c343..0a0e150 100644 --- a/src/extras/BitmapText.js +++ b/src/extras/BitmapText.js @@ -455,6 +455,73 @@ return this._textHeight; } + + /** + * Register a bitmap font with data and a texture. + * + * @static + * @param {XMLDocument} xml - The XML document data. + * @param {PIXI.Texture} texture - Texture with all symbols. + * @return {Object} Result font object with font, size, lineHeight and char fields. + */ + static registerFont(xml, texture) + { + const data = {}; + const info = xml.getElementsByTagName('info')[0]; + const common = xml.getElementsByTagName('common')[0]; + + data.font = info.getAttribute('face'); + data.size = parseInt(info.getAttribute('size'), 10); + data.lineHeight = parseInt(common.getAttribute('lineHeight'), 10); + data.chars = {}; + + // parse letters + const letters = xml.getElementsByTagName('char'); + + for (let i = 0; i < letters.length; i++) + { + const letter = letters[i]; + const charCode = parseInt(letter.getAttribute('id'), 10); + + const textureRect = new core.Rectangle( + parseInt(letter.getAttribute('x'), 10) + texture.frame.x, + parseInt(letter.getAttribute('y'), 10) + texture.frame.y, + parseInt(letter.getAttribute('width'), 10), + parseInt(letter.getAttribute('height'), 10) + ); + + data.chars[charCode] = { + xOffset: parseInt(letter.getAttribute('xoffset'), 10), + yOffset: parseInt(letter.getAttribute('yoffset'), 10), + xAdvance: parseInt(letter.getAttribute('xadvance'), 10), + kerning: {}, + texture: new core.Texture(texture.baseTexture, textureRect), + + }; + } + + // parse kernings + const kernings = xml.getElementsByTagName('kerning'); + + for (let i = 0; i < kernings.length; i++) + { + const kerning = kernings[i]; + const first = parseInt(kerning.getAttribute('first'), 10); + const second = parseInt(kerning.getAttribute('second'), 10); + const amount = parseInt(kerning.getAttribute('amount'), 10); + + if (data.chars[second]) + { + data.chars[second].kerning[first] = amount; + } + } + + // I'm leaving this as a temporary fix so we can test the bitmap fonts in v3 + // but it's very likely to change + BitmapText.fonts[data.font] = data; + + return data; + } } BitmapText.fonts = {}; diff --git a/src/index.js b/src/index.js index 59356cf..c553427 100644 --- a/src/index.js +++ b/src/index.js @@ -16,6 +16,10 @@ import * as particles from './particles'; import * as prepare from './prepare'; +// handle mixins now, after all code has been added, including deprecation +import { utils } from './core'; +utils.mixins.performMixins(); + export { accessibility, extract, diff --git a/src/interaction/InteractionManager.js b/src/interaction/InteractionManager.js index 7d51134..8baf13a 100644 --- a/src/interaction/InteractionManager.js +++ b/src/interaction/InteractionManager.js @@ -1,18 +1,20 @@ 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'; -// Mix interactiveTarget into core.DisplayObject.prototype -Object.assign( +// Mix interactiveTarget into core.DisplayObject.prototype, after deprecation has been handled +core.utils.mixins.delayMixin( core.DisplayObject.prototype, 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 +70,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 +101,6 @@ this.eventData = new InteractionEvent(); /** - * Tiny little interactiveData pool ! - * - * @member {PIXI.interaction.InteractionData[]} - */ - this.interactiveDataPool = []; - - /** * The DOM element to bind to. * * @private @@ -153,66 +155,12 @@ */ this.supportsPointerEvents = !!window.PointerEvent; - /** - * Are touch events being 'normalized' and converted into pointer events if pointer events are not supported - * For example, on a touch screen mobile device, a touchstart would also be emitted as a pointerdown - * - * @private - * @readonly - * @member {boolean} - */ - this.normalizeTouchEvents = !this.supportsPointerEvents && this.supportsTouchEvents; - - /** - * Are mouse events being 'normalized' and converted into pointer events if pointer events are not supported - * For example, on a desktop pc, a mousedown would also be emitted as a pointerdown - * - * @private - * @readonly - * @member {boolean} - */ - this.normalizeMouseEvents = !this.supportsPointerEvents && !MobileDevice.any; - // this will make it so that you don't have to call bind all the time /** * @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 +168,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,41 +199,32 @@ this.onPointerOver = this.onPointerOver.bind(this); /** - * @private - * @member {Function} + * Dictionary of how different cursor modes are handled. Strings are handled as CSS cursor + * values, objects are handled as dictionaries of CSS values for interactionDOMElement, + * and functions are called instead of changing the CSS. + * Default CSS cursor values are provided for 'default' and 'pointer' modes. + * @member {Object.)>} */ - this.onTouchStart = this.onTouchStart.bind(this); - this.processTouchStart = this.processTouchStart.bind(this); + this.cursorStyles = { + default: 'inherit', + pointer: 'pointer', + }; /** - * @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. - * - * @member {string} - * @default 'inherit' - */ - this.defaultCursorStyle = 'inherit'; - - /** - * The css style of the cursor that is being used. + * The mode of the cursor that is being used. + * The value of this is a key from the cursorStyles dictionary. * * @member {string} */ - this.currentCursorStyle = 'inherit'; + this.currentCursorMode = null; + + /** + * Internal cached let. + * + * @private + * @member {string} + */ + this.cursor = null; /** * Internal cached let. @@ -303,6 +249,7 @@ * object. * * @event mousedown + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -311,6 +258,7 @@ * on the display object. * * @event rightdown + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -319,6 +267,7 @@ * object. * * @event mouseup + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -327,6 +276,7 @@ * over the display object. * * @event rightup + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -335,6 +285,7 @@ * the display object. * * @event click + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -343,6 +294,7 @@ * and released on the display object. * * @event rightclick + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -352,6 +304,7 @@ * [mousedown]{@link PIXI.interaction.InteractionManager#event:mousedown}. * * @event mouseupoutside + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -361,6 +314,7 @@ * [rightdown]{@link PIXI.interaction.InteractionManager#event:rightdown}. * * @event rightupoutside + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -368,6 +322,7 @@ * Fired when a pointer device (usually a mouse) is moved while over the display object * * @event mousemove + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -375,6 +330,7 @@ * Fired when a pointer device (usually a mouse) is moved onto the display object * * @event mouseover + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -382,6 +338,7 @@ * Fired when a pointer device (usually a mouse) is moved off the display object * * @event mouseout + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -389,6 +346,7 @@ * Fired when a pointer device button is pressed on the display object. * * @event pointerdown + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -396,6 +354,14 @@ * Fired when a pointer device button is released over the display object. * * @event pointerup + * @type {PIXI.interaction.InteractionData} + * @memberof PIXI.interaction.InteractionManager# + */ + + /** + * Fired when the operating system cancels a pointer event + * + * @event pointercancel * @memberof PIXI.interaction.InteractionManager# */ @@ -403,6 +369,7 @@ * Fired when a pointer device button is pressed and released on the display object. * * @event pointertap + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -411,6 +378,7 @@ * registered a [pointerdown]{@link PIXI.interaction.InteractionManager#event:pointerdown}. * * @event pointerupoutside + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -418,6 +386,7 @@ * Fired when a pointer device is moved while over the display object * * @event pointermove + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -425,6 +394,7 @@ * Fired when a pointer device is moved onto the display object * * @event pointerover + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -432,6 +402,7 @@ * Fired when a pointer device is moved off the display object * * @event pointerout + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -439,6 +410,7 @@ * Fired when a touch point is placed on the display object. * * @event touchstart + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -446,6 +418,14 @@ * Fired when a touch point is removed from the display object. * * @event touchend + * @type {PIXI.interaction.InteractionData} + * @memberof PIXI.interaction.InteractionManager# + */ + + /** + * Fired when the operating system cancels a touch + * + * @event touchcancel * @memberof PIXI.interaction.InteractionManager# */ @@ -453,6 +433,7 @@ * Fired when a touch point is placed and removed from the display object. * * @event tap + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -461,6 +442,7 @@ * registered a [touchstart]{@link PIXI.interaction.InteractionManager#event:touchstart}. * * @event touchendoutside + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -468,6 +450,7 @@ * Fired when a touch point is moved along the display object. * * @event touchmove + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ } @@ -524,45 +507,30 @@ { window.document.addEventListener('pointermove', this.onPointerMove, true); this.interactionDOMElement.addEventListener('pointerdown', this.onPointerDown, true); - this.interactionDOMElement.addEventListener('pointerout', this.onPointerOut, true); + // pointerout is fired in addition to pointerup (for touch events) and pointercancel + // we already handle those, so for the purposes of what we do in onPointerOut, we only + // care about the pointerleave event + this.interactionDOMElement.addEventListener('pointerleave', 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; @@ -596,45 +564,26 @@ { window.document.removeEventListener('pointermove', this.onPointerMove, true); this.interactionDOMElement.removeEventListener('pointerdown', this.onPointerDown, true); - this.interactionDOMElement.removeEventListener('pointerout', this.onPointerOut, true); + this.interactionDOMElement.removeEventListener('pointerleave', 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) + 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.onPointerDown, true); + this.interactionDOMElement.removeEventListener('touchcancel', this.onPointerCancel, 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); - - 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 = null; @@ -664,7 +613,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; @@ -672,25 +621,80 @@ return; } - this.cursor = this.defaultCursorStyle; + this.cursor = null; // 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(); - - this.processInteractive(this.mouse.global, this.renderer._lastObjectRendered, this.processMouseOverOut, true); - - if (this.currentCursorStyle !== this.cursor) + for (const k in this.activeInteractionData) { - this.currentCursorStyle = this.cursor; - this.interactionDOMElement.style.cursor = this.cursor; + // eslint-disable-next-line no-prototype-builtins + if (this.activeInteractionData.hasOwnProperty(k)) + { + const interactionData = this.activeInteractionData[k]; + + if (interactionData.originalEvent && interactionData.pointerType !== 'touch') + { + const interactionEvent = this.configureInteractionEventForDOMEvent( + this.eventData, + interactionData.originalEvent, + interactionData + ); + + this.processInteractive( + interactionEvent, + this.renderer._lastObjectRendered, + this.processPointerOverOut, + true + ); + } + } } + this.setCursorMode(this.cursor); + // TODO } /** + * Sets the current cursor mode, handling any callbacks or CSS style changes. + * + * @param {string} mode - cursor mode, a key from the cursorStyles dictionary + */ + setCursorMode(mode) + { + mode = mode || 'default'; + // if the mode didn't actually change, bail early + if (this.currentCursorMode === mode) + { + return; + } + this.currentCursorMode = mode; + const style = this.cursorStyles[mode]; + + // only do things if there is a cursor style for it + if (style) + { + switch (typeof style) + { + case 'string': + // string styles are handled as cursor CSS + this.interactionDOMElement.style.cursor = style; + break; + case 'function': + // functions are just called, and passed the cursor mode + style(mode); + break; + case 'object': + // if it is an object, assume that it is a dictionary of CSS styles, + // apply it to the interactionDOMElement + Object.assign(this.interactionDOMElement.style, style); + break; + } + } + } + + /** * Dispatches an event on the display object that was interacted with * * @param {PIXI.Container|PIXI.Sprite|PIXI.extras.TilingSprite} displayObject - the display object in question @@ -748,22 +752,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 @@ -787,9 +795,8 @@ { interactiveParent = false; } - - // it has a mask! Then lets hit test that before continuing.. - if (hitTest && displayObject._mask) + // it has a mask! Then lets hit test that before continuing + else if (hitTest && displayObject._mask) { if (!displayObject._mask.containsPoint(point)) { @@ -797,14 +804,7 @@ } } - // it has a filterArea! Same as mask but easier, its a rectangle - if (hitTest && displayObject.filterArea) - { - if (!displayObject.filterArea.contains(point.x, point.y)) - { - hitTest = false; - } - } + let keepHitTestingAfterChildren = hitTest; // ** FREE TIP **! If an object is not interactive or has no buttons in it // (such as a game scene!) set interactiveChildren to false for that displayObject. @@ -818,7 +818,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 @@ -838,15 +838,20 @@ // This means we no longer need to hit test anything else. We still need to run // through all objects, but we don't need to perform any hit tests. - // { - hitTest = false; - // } + keepHitTestingAfterChildren = false; + + if (child.interactive) + { + hitTest = false; + } // we can break now as we have hit an object. } } } + hitTest = keepHitTestingAfterChildren; + // no point running this if the item is not interactive or does not have an interactive parent. if (interactive) { @@ -868,14 +873,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 +886,175 @@ } /** - * 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].isNormalized) { - 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); + if (!displayObject.trackedPointers[id]) + { + displayObject.trackedPointers[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.trackedPointers[id].rightDown = true; + } + else + { + displayObject.trackedPointers[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.trackedPointers[id] !== undefined) + { + delete displayObject.trackedPointers[id]; + this.dispatchEvent(displayObject, 'pointercancel', interactionEvent); + + if (e.type === 'touchcancel' || e.pointerType === 'touch') + { + this.dispatchEvent(displayObject, 'touchcancel', interactionEvent); + } } } @@ -1159,42 +1066,94 @@ */ 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) { - if (hit) - { - this.dispatchEvent(displayObject, 'pointerup', this.eventData); + const e = interactionEvent.data.originalEvent; - if (displayObject._pointerDown) + const id = interactionEvent.data.identifier; + + const trackingData = displayObject.trackedPointers[id]; + + const isTouch = (e.type === 'touchend' || e.pointerType === 'touch'); + + const isMouse = (e.type.indexOf('mouse') === 0 || e.pointerType === 'mouse'); + + // 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) { - displayObject._pointerDown = false; - this.dispatchEvent(displayObject, 'pointertap', this.eventData); + 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); + } + // update the down state of the tracking data + if (trackingData) + { + if (isRightButton) + { + trackingData.rightDown = hit; + } + else + { + trackingData.leftDown = hit; + } } } - else if (displayObject._pointerDown) + + // Pointers and Touches, and Mouse + if (hit) { - displayObject._pointerDown = false; - this.dispatchEvent(displayObject, 'pointerupoutside', this.eventData); + this.dispatchEvent(displayObject, 'pointerup', interactionEvent); + if (isTouch) this.dispatchEvent(displayObject, 'touchend', interactionEvent); + + if (trackingData) + { + this.dispatchEvent(displayObject, 'pointertap', interactionEvent); + if (isTouch) + { + this.dispatchEvent(displayObject, 'tap', interactionEvent); + // touches are no longer over (if they ever were) when we get the touchend + // so we should ensure that we don't keep pretending that they are + trackingData.over = false; + } + } + } + else if (trackingData) + { + this.dispatchEvent(displayObject, 'pointerupoutside', interactionEvent); + if (isTouch) this.dispatchEvent(displayObject, 'touchendoutside', interactionEvent); + } + // Only remove the tracking data if there is no over/down state still associated with it + if (trackingData && trackingData.none) + { + delete displayObject.trackedPointers[id]; } } @@ -1202,39 +1161,78 @@ * 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 = null; + } - 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') + { + this.setCursorMode(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 (isMouse) { - 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 +1240,94 @@ * 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.setCursorMode(null); + } - 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'); + + let trackingData = displayObject.trackedPointers[id]; + + // if we just moused over the display object, then we need to track that state + if (hit && !trackingData) + { + trackingData = displayObject.trackedPointers[id] = new InteractionTrackingData(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); + } + } + + // only change the cursor if it has not already been changed (by something deeper in the + // display tree) + if (isMouse && this.cursor === null) + { + this.cursor = displayObject.cursor; } } - 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); + } + // if there is no mouse down information for the pointer, then it is safe to delete + if (trackingData.none) + { + delete displayObject.trackedPointers[id]; + } } } @@ -1287,253 +1335,154 @@ * 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 && pointerEvent.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; + + // mark the touch as normalized, just so that we know we did it + touch.isNormalized = true; + + normalizedEvents.push(touch); + } } - else if (this.normalizeMouseEvents) + // apparently PointerEvent subclasses MouseEvent, so yay + else if (event instanceof MouseEvent && (!this.supportsPointerEvents || !(event instanceof window.PointerEvent))) { if (typeof event.isPrimary === 'undefined') event.isPrimary = true; if (typeof event.width === 'undefined') event.width = 1; @@ -1541,10 +1490,21 @@ 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; + + // mark the mouse event as normalized, just so that we know we did it + event.isNormalized = true; + + normalizedEvents.push(event); } + else + { + normalizedEvents.push(event); + } + + return normalizedEvents; } /** @@ -1563,30 +1523,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 +1542,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..6a1f69e --- /dev/null +++ b/src/interaction/InteractionTrackingData.js @@ -0,0 +1,147 @@ +/** + * 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 inactive (not over or down)? + * + * @member {number} + * @memberof PIXI.interaction.InteractionTrackingData# + */ + get none() + { + return this._flags === this.constructor.FLAGS.NONE; + } + + /** + * 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..d252fba 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 * ); */ @@ -38,68 +38,56 @@ hitArea: null, /** - * If enabled, the mouse cursor will change when hovered over the displayObject if it is interactive + * If enabled, the mouse cursor use the pointer behavior when hovered over the displayObject if it is interactive + * Setting this changes the 'cursor' property to `'pointer'`. * - * @inner {boolean} + * @member {boolean} + * @memberof PIXI.interaction.interactiveTarget# */ - buttonMode: false, + get buttonMode() + { + return this.cursor === 'pointer'; + }, + set buttonMode(value) + { + if (value) + { + this.cursor = 'pointer'; + } + else if (this.cursor === 'pointer') + { + this.cursor = null; + } + }, /** - * If buttonMode is enabled, this defines what CSS cursor property is used when the mouse cursor - * is hovered over the displayObject + * This defines what cursor mode is used when the mouse cursor + * is hovered over the displayObject. * * @see https://developer.mozilla.org/en/docs/Web/CSS/cursor * * @inner {string} */ - defaultCursor: 'pointer', - - // some internal checks.. - /** - * Internal check to detect if the mouse cursor is hovered over the displayObject - * - * @inner {boolean} - * @private - */ - _over: false, + cursor: null, /** - * Internal check to detect if the left mouse button is pressed on the displayObject + * Internal set of all active pointers, by identifier * - * @inner {boolean} + * @member {Map} + * @memberof PIXI.interaction.interactiveTarget# * @private */ - _isLeftDown: false, + get trackedPointers() + { + if (this._trackedPointers === undefined) this._trackedPointers = {}; + + return this._trackedPointers; + }, /** - * Internal check to detect if the right mouse button is pressed on the displayObject + * Map of all tracked pointers, by identifier. Use trackedPointers to access. * - * @inner {boolean} - * @private + * @private {Map} */ - _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, + _trackedPointers: undefined, }; diff --git a/src/loaders/bitmapFontParser.js b/src/loaders/bitmapFontParser.js index cc2618c..f4cf996 100644 --- a/src/loaders/bitmapFontParser.js +++ b/src/loaders/bitmapFontParser.js @@ -1,63 +1,19 @@ import * as path from 'path'; -import { Rectangle, Texture, utils } from '../core'; +import { utils } from '../core'; import { Resource } from 'resource-loader'; import { BitmapText } from '../extras'; +/** + * Register a BitmapText font from loader resource. + * + * @function parseBitmapFontData + * @memberof PIXI.loaders + * @param {PIXI.loaders.Resource} resource - Loader resource. + * @param {PIXI.Texture} texture - Reference to texture. + */ export function parse(resource, texture) { - const data = {}; - const info = resource.data.getElementsByTagName('info')[0]; - const common = resource.data.getElementsByTagName('common')[0]; - - data.font = info.getAttribute('face'); - data.size = parseInt(info.getAttribute('size'), 10); - data.lineHeight = parseInt(common.getAttribute('lineHeight'), 10); - data.chars = {}; - - // parse letters - const letters = resource.data.getElementsByTagName('char'); - - for (let i = 0; i < letters.length; i++) - { - const charCode = parseInt(letters[i].getAttribute('id'), 10); - - const textureRect = new Rectangle( - parseInt(letters[i].getAttribute('x'), 10) + texture.frame.x, - parseInt(letters[i].getAttribute('y'), 10) + texture.frame.y, - parseInt(letters[i].getAttribute('width'), 10), - parseInt(letters[i].getAttribute('height'), 10) - ); - - data.chars[charCode] = { - xOffset: parseInt(letters[i].getAttribute('xoffset'), 10), - yOffset: parseInt(letters[i].getAttribute('yoffset'), 10), - xAdvance: parseInt(letters[i].getAttribute('xadvance'), 10), - kerning: {}, - texture: new Texture(texture.baseTexture, textureRect), - - }; - } - - // parse kernings - const kernings = resource.data.getElementsByTagName('kerning'); - - for (let i = 0; i < kernings.length; i++) - { - const first = parseInt(kernings[i].getAttribute('first'), 10); - const second = parseInt(kernings[i].getAttribute('second'), 10); - const amount = parseInt(kernings[i].getAttribute('amount'), 10); - - if (data.chars[second]) - { - data.chars[second].kerning[first] = amount; - } - } - - resource.bitmapFont = data; - - // I'm leaving this as a temporary fix so we can test the bitmap fonts in v3 - // but it's very likely to change - BitmapText.fonts[data.font] = data; + resource.bitmapFont = BitmapText.registerFont(resource.data, texture); } export default function () diff --git a/src/loaders/index.js b/src/loaders/index.js index 8942436..5189793 100644 --- a/src/loaders/index.js +++ b/src/loaders/index.js @@ -5,4 +5,11 @@ export { default as bitmapFontParser, parse as parseBitmapFontData } from './bitmapFontParser'; export { default as spritesheetParser } from './spritesheetParser'; export { default as textureParser } from './textureParser'; + +/** + * Reference to **resource-loader**'s Resource class. + * See https://github.com/englercj/resource-loader + * @class Resource + * @memberof PIXI.loaders + */ export { Resource } from 'resource-loader'; diff --git a/src/loaders/loader.js b/src/loaders/loader.js index 0a670cb..767a411 100644 --- a/src/loaders/loader.js +++ b/src/loaders/loader.js @@ -7,20 +7,47 @@ /** * - * The new loader, extends Resource Loader by Chad Engler : https://github.com/englercj/resource-loader + * The new loader, extends Resource Loader by Chad Engler: https://github.com/englercj/resource-loader * * ```js - * let loader = PIXI.loader; // pixi exposes a premade instance for you to use. + * const loader = PIXI.loader; // pixi exposes a premade instance for you to use. * //or - * let loader = new PIXI.loaders.Loader(); // you can also create your own if you want + * const loader = new PIXI.loaders.Loader(); // you can also create your own if you want * - * loader.add('bunny', 'data/bunny.png'); - * loader.add('spaceship', 'assets/spritesheet.json'); + * const sprites = {}; + * + * // Chainable `add` to enqueue a resource + * loader.add('bunny', 'data/bunny.png') + * .add('spaceship', 'assets/spritesheet.json'); * loader.add('scoreFont', 'assets/score.fnt'); * - * loader.once('complete',onAssetsLoaded); + * // Chainable `pre` to add a middleware that runs for each resource, *before* loading that resource. + * // This is useful to implement custom caching modules (using filesystem, indexeddb, memory, etc). + * loader.pre(cachingMiddleware); * - * loader.load(); + * // Chainable `use` to add a middleware that runs for each resource, *after* loading that resource. + * // This is useful to implement custom parsing modules (like spritesheet parsers, spine parser, etc). + * loader.use(parsingMiddleware); + * + * // The `load` method loads the queue of resources, and calls the passed in callback called once all + * // resources have loaded. + * loader.load((loader, resources) => { + * // resources is an object where the key is the name of the resource loaded and the value is the resource object. + * // They have a couple default properties: + * // - `url`: The URL that the resource was loaded from + * // - `error`: The error that happened when trying to load (if any) + * // - `data`: The raw data that was loaded + * // also may contain other properties based on the middleware that runs. + * sprites.bunny = new PIXI.TilingSprite(resources.bunny.texture); + * sprites.spaceship = new PIXI.TilingSprite(resources.spaceship.texture); + * sprites.scoreFont = new PIXI.TilingSprite(resources.scoreFont.texture); + * }); + * + * // throughout the process multiple signals can be dispatched. + * loader.onProgress.add(() => {}); // called once per loaded/errored file + * loader.onError.add(() => {}); // called once per errored file + * loader.onLoad.add(() => {}); // called once per loaded file + * loader.onComplete.add(() => {}); // called once when the queued resources all load. * ``` * * @see https://github.com/englercj/resource-loader diff --git a/src/loaders/spritesheetParser.js b/src/loaders/spritesheetParser.js index 7570332..e57dfb2 100644 --- a/src/loaders/spritesheetParser.js +++ b/src/loaders/spritesheetParser.js @@ -1,8 +1,6 @@ import { Resource } from 'resource-loader'; import path from 'path'; -import * as core from '../core'; - -const BATCH_SIZE = 1000; +import { Spritesheet } from '../core'; export default function () { @@ -43,134 +41,18 @@ // load the image for this sheet this.add(imageResourceName, resourcePath, loadOptions, function onImageLoad(res) { - resource.textures = {}; + const spritesheet = new Spritesheet( + res.texture.baseTexture, + resource.data, + resource.url + ); - const frames = resource.data.frames; - const frameKeys = Object.keys(frames); - const baseTexture = res.texture.baseTexture; - const scale = resource.data.meta.scale; - - // Use a defaultValue of `null` to check if a url-based resolution is set - let resolution = core.utils.getResolutionOfUrl(resource.url, null); - - // No resolution found via URL - if (resolution === null) + spritesheet.parse(() => { - // Use the scale value or default to 1 - resolution = scale !== undefined ? scale : 1; - } - - // For non-1 resolutions, update baseTexture - if (resolution !== 1) - { - baseTexture.resolution = resolution; - baseTexture.update(); - } - - let batchIndex = 0; - - function processFrames(initialFrameIndex, maxFrames) - { - let frameIndex = initialFrameIndex; - - while (frameIndex - initialFrameIndex < maxFrames && frameIndex < frameKeys.length) - { - const i = frameKeys[frameIndex]; - const rect = frames[i].frame; - - if (rect) - { - let frame = null; - let trim = null; - const orig = new core.Rectangle( - 0, - 0, - frames[i].sourceSize.w / resolution, - frames[i].sourceSize.h / resolution - ); - - if (frames[i].rotated) - { - frame = new core.Rectangle( - rect.x / resolution, - rect.y / resolution, - rect.h / resolution, - rect.w / resolution - ); - } - else - { - frame = new core.Rectangle( - rect.x / resolution, - rect.y / resolution, - rect.w / resolution, - rect.h / resolution - ); - } - - // Check to see if the sprite is trimmed - if (frames[i].trimmed) - { - trim = new core.Rectangle( - frames[i].spriteSourceSize.x / resolution, - frames[i].spriteSourceSize.y / resolution, - rect.w / resolution, - rect.h / resolution - ); - } - - resource.textures[i] = new core.Texture( - baseTexture, - frame, - orig, - trim, - frames[i].rotated ? 2 : 0 - ); - - // lets also add the frame to pixi's global cache for fromFrame and fromImage functions - core.utils.TextureCache[i] = resource.textures[i]; - } - - frameIndex++; - } - } - - function shouldProcessNextBatch() - { - return batchIndex * BATCH_SIZE < frameKeys.length; - } - - function processNextBatch(done) - { - processFrames(batchIndex * BATCH_SIZE, BATCH_SIZE); - batchIndex++; - setTimeout(done, 0); - } - - function iteration() - { - processNextBatch(() => - { - if (shouldProcessNextBatch()) - { - iteration(); - } - else - { - next(); - } - }); - } - - if (frameKeys.length <= BATCH_SIZE) - { - processFrames(0, BATCH_SIZE); + resource.spritesheet = spritesheet; + resource.textures = spritesheet.textures; next(); - } - else - { - iteration(); - } + }); }); }; } diff --git a/src/loaders/textureParser.js b/src/loaders/textureParser.js index 5398a7f..480edc1 100644 --- a/src/loaders/textureParser.js +++ b/src/loaders/textureParser.js @@ -1,5 +1,5 @@ -import * as core from '../core'; import { Resource } from 'resource-loader'; +import Texture from '../core/textures/Texture'; export default function () { @@ -8,23 +8,12 @@ // create a new texture if the data is an Image object if (resource.data && resource.type === Resource.TYPE.IMAGE) { - const baseTexture = new core.BaseTexture(resource.data, null, core.utils.getResolutionOfUrl(resource.url)); - - baseTexture.imageUrl = resource.url; - resource.texture = new core.Texture(baseTexture); - - // lets also add the frame to pixi's global cache for fromFrame and fromImage fucntions - core.utils.BaseTextureCache[resource.name] = baseTexture; - core.utils.TextureCache[resource.name] = resource.texture; - - // also add references by url if they are different. - if (resource.name !== resource.url) - { - core.utils.BaseTextureCache[resource.url] = baseTexture; - core.utils.TextureCache[resource.url] = resource.texture; - } + resource.texture = Texture.fromLoader( + resource.data, + resource.url, + resource.name + ); } - next(); }; } diff --git a/test/core/Container.js b/test/core/Container.js index 8a10b40..d11be33 100644 --- a/test/core/Container.js +++ b/test/core/Container.js @@ -1,5 +1,33 @@ 'use strict'; +function testAddChild(fn) +{ + fn(function (container, obj) + { + container.addChild(obj); + }); + fn(function (container, obj) + { + container.addChildAt(obj); + }); +} + +function testRemoveChild(fn) +{ + fn(function (container, obj) + { + container.removeChild(obj); + }); + fn(function (container, obj) + { + container.removeChildAt(container.children.indexOf(obj)); + }); + fn(function (container, obj) + { + container.removeChildren(container.children.indexOf(obj), container.children.indexOf(obj) + 1); + }); +} + describe('PIXI.Container', function () { describe('parent', function () @@ -70,6 +98,49 @@ expect(spy).to.have.been.called; expect(spy).to.have.been.calledWith(0); }); + + it('should flag child transform and container bounds for recalculation', testAddChild(function (mockAddChild) + { + const container = new PIXI.Container(); + const child = new PIXI.Container(); + + container.getBounds(); + child.getBounds(); + + const boundsID = container._boundsID; + const childParentID = child.transform._parentID; + + mockAddChild(container, child); + + expect(boundsID).to.not.be.equals(container._boundsID); + expect(childParentID).to.not.be.equals(child.transform._parentID); + })); + + it('should recalculate added child correctly', testAddChild(function (mockAddChild) + { + const parent = new PIXI.Container(); + const container = new PIXI.Container(); + const graphics = new PIXI.Graphics(); + + parent.addChild(container); + + graphics.drawRect(0, 0, 10, 10); + container.position.set(100, 200); + container.updateTransform(); + + graphics.getBounds(); + // Oops, that can happen sometimes! + graphics.transform._parentID = container.transform._worldID + 1; + + mockAddChild(container, graphics); + + const bounds = graphics.getBounds(); + + expect(bounds.x).to.be.equal(100); + expect(bounds.y).to.be.equal(200); + expect(bounds.width).to.be.equal(10); + expect(bounds.height).to.be.equal(10); + })); }); describe('removeChildAt', function () @@ -202,6 +273,44 @@ expect(spy).to.have.been.called; expect(spy).to.have.been.calledWith(0); }); + + it('should flag transform for recalculation', testRemoveChild(function (mockRemoveChild) + { + const container = new PIXI.Container(); + const child = new PIXI.Container(); + + container.addChild(child); + container.getBounds(); + + const childParentID = child.transform._parentID; + const boundsID = container._boundsID; + + mockRemoveChild(container, child); + + expect(childParentID).to.not.be.equals(child.transform._parentID); + expect(boundsID).to.not.be.equals(container._boundsID); + })); + + it('should recalculate removed child correctly', testRemoveChild(function (mockRemoveChild) + { + const parent = new PIXI.Container(); + const container = new PIXI.Container(); + const graphics = new PIXI.Graphics(); + + parent.addChild(container); + + graphics.drawRect(0, 0, 10, 10); + container.position.set(100, 200); + container.addChild(graphics); + graphics.getBounds(); + + mockRemoveChild(container, graphics); + + const bounds = graphics.getBounds(); + + expect(bounds.x).to.be.equal(0); + expect(bounds.y).to.be.equal(0); + })); }); describe('getChildIndex', function () diff --git a/test/core/Graphics.js b/test/core/Graphics.js index 98c7584..11e47a0 100644 --- a/test/core/Graphics.js +++ b/test/core/Graphics.js @@ -1,5 +1,8 @@ 'use strict'; +const MockPointer = require('../interaction/MockPointer'); +const withGL = require('../withGL'); + describe('PIXI.Graphics', function () { describe('constructor', function () @@ -218,4 +221,153 @@ expect(spy).to.have.been.calledOnce; }); }); + + describe('mask', function () + { + it('should trigger interaction callback when no mask present', function () + { + const stage = new PIXI.Container(); + const pointer = new MockPointer(stage); + const graphics = new PIXI.Graphics(); + const mask = new PIXI.Graphics(); + const spy = sinon.spy(); + + graphics.interactive = true; + graphics.beginFill(0xFF0000); + graphics.drawRect(0, 0, 50, 50); + graphics.on('click', spy); + stage.addChild(graphics); + mask.beginFill(); + mask.drawRect(0, 0, 50, 50); + graphics.mask = mask; + + pointer.click(10, 10); + + expect(spy).to.have.been.calledOnce; + }); + it('should trigger interaction callback when mask uses beginFill', function () + { + const stage = new PIXI.Container(); + const pointer = new MockPointer(stage); + const graphics = new PIXI.Graphics(); + const mask = new PIXI.Graphics(); + const spy = sinon.spy(); + + graphics.interactive = true; + graphics.beginFill(0xFF0000); + graphics.drawRect(0, 0, 50, 50); + graphics.on('click', spy); + stage.addChild(graphics); + mask.beginFill(); + mask.drawRect(0, 0, 50, 50); + graphics.mask = mask; + + pointer.click(10, 10); + + expect(spy).to.have.been.calledOnce; + }); + + it('should not trigger interaction callback when mask doesn\'t use beginFill', function () + { + const stage = new PIXI.Container(); + const pointer = new MockPointer(stage); + const graphics = new PIXI.Graphics(); + const mask = new PIXI.Graphics(); + const spy = sinon.spy(); + + graphics.interactive = true; + graphics.beginFill(0xFF0000); + graphics.drawRect(0, 0, 50, 50); + graphics.on('click', spy); + stage.addChild(graphics); + mask.drawRect(0, 0, 50, 50); + graphics.mask = mask; + + pointer.click(10, 10); + + expect(spy).to.have.not.been.called; + }); + + it('should trigger interaction callback when mask doesn\'t use beginFill but hitArea is defined', function () + { + const stage = new PIXI.Container(); + const pointer = new MockPointer(stage); + const graphics = new PIXI.Graphics(); + const mask = new PIXI.Graphics(); + const spy = sinon.spy(); + + graphics.interactive = true; + graphics.beginFill(0xFF0000); + graphics.hitArea = new PIXI.Rectangle(0, 0, 50, 50); + graphics.drawRect(0, 0, 50, 50); + graphics.on('click', spy); + stage.addChild(graphics); + mask.drawRect(0, 0, 50, 50); + graphics.mask = mask; + + pointer.click(10, 10); + + expect(spy).to.have.been.calledOnce; + }); + + it('should trigger interaction callback when mask is a sprite', function () + { + const stage = new PIXI.Container(); + const pointer = new MockPointer(stage); + const graphics = new PIXI.Graphics(); + const mask = new PIXI.Graphics(); + const spy = sinon.spy(); + + graphics.interactive = true; + graphics.beginFill(0xFF0000); + graphics.drawRect(0, 0, 50, 50); + graphics.on('click', spy); + stage.addChild(graphics); + mask.drawRect(0, 0, 50, 50); + graphics.mask = new PIXI.Sprite(mask.generateCanvasTexture()); + + pointer.click(10, 10); + + expect(spy).to.have.been.calledOnce; + }); + + it('should calculate tint, alpha and blendMode of fastRect correctly', withGL(function () + { + const renderer = new PIXI.WebGLRenderer(200, 200, {}); + + try + { + const graphics = new PIXI.Graphics(); + + graphics.beginFill(0x102030, 0.6); + graphics.drawRect(2, 3, 100, 100); + graphics.endFill(); + graphics.tint = 0x101010; + graphics.blendMode = 2; + graphics.alpha = 0.3; + + renderer.render(graphics); + + expect(graphics.isFastRect()).to.be.true; + + const sprite = graphics._spriteRect; + + expect(sprite).to.not.be.equals(null); + expect(sprite.worldAlpha).to.equals(0.18); + expect(sprite.blendMode).to.equals(2); + expect(sprite.tint).to.equals(0x010203); + + const bounds = sprite.getBounds(); + + expect(bounds.x).to.equals(2); + expect(bounds.y).to.equals(3); + expect(bounds.width).to.equals(100); + expect(bounds.height).to.equals(100); + } + finally + { + renderer.destroy(); + } + })); + }); }); diff --git a/test/core/Spritesheet.js b/test/core/Spritesheet.js new file mode 100644 index 0000000..283adf5 --- /dev/null +++ b/test/core/Spritesheet.js @@ -0,0 +1,89 @@ +'use strict'; + +const path = require('path'); + +describe('PIXI.Spritesheet', function () +{ + before(function () + { + this.resources = path.join(__dirname, 'resources'); + this.validate = function (spritesheet, done) + { + spritesheet.parse(function (textures) + { + const id = 'goldmine_10_5.png'; + + expect(Object.keys(textures).length).to.equal(1); + expect(Object.keys(spritesheet.textures).length).to.equal(1); + expect(textures[id]).to.be.an.instanceof(PIXI.Texture); + expect(textures[id].width).to.equal(spritesheet.data.frames[id].frame.w / spritesheet.resolution); + expect(textures[id].height).to.equal(spritesheet.data.frames[id].frame.h / spritesheet.resolution); + spritesheet.destroy(true); + expect(spritesheet.textures).to.be.null; + expect(spritesheet.baseTexture).to.be.null; + done(); + }); + }; + }); + + it('should exist on PIXI', function () + { + expect(PIXI.Spritesheet).to.be.a.function; + expect(PIXI.Spritesheet.BATCH_SIZE).to.be.a.number; + }); + + it('should create an instance', function () + { + const baseTexture = new PIXI.BaseTexture(); + const data = { + frames: {}, + meta: {}, + }; + const spritesheet = new PIXI.Spritesheet(baseTexture, data); + + expect(spritesheet.data).to.equal(data); + expect(spritesheet.baseTexture).to.equal(baseTexture); + expect(spritesheet.resolution).to.equal(1); + + spritesheet.destroy(true); + }); + + it('should create instance with scale resolution', function (done) + { + const data = require(path.resolve(this.resources, 'building1.json')); // eslint-disable-line global-require + const image = new Image(); + + image.src = path.join(this.resources, data.meta.image); + image.onload = () => + { + const baseTexture = new PIXI.BaseTexture(image, null, 1); + const spritesheet = new PIXI.Spritesheet(baseTexture, data); + + expect(data).to.be.an.object; + expect(data.meta.image).to.equal('building1.png'); + expect(spritesheet.resolution).to.equal(0.5); + + this.validate(spritesheet, done); + }; + }); + + it('should create instance with filename resolution', function (done) + { + const uri = path.resolve(this.resources, 'building1@2x.json'); + const data = require(uri); // eslint-disable-line global-require + const image = new Image(); + + image.src = path.join(this.resources, data.meta.image); + image.onload = () => + { + const baseTexture = new PIXI.BaseTexture(image, null, 1); + const spritesheet = new PIXI.Spritesheet(baseTexture, data, uri); + + expect(data).to.be.an.object; + expect(data.meta.image).to.equal('building1@2x.png'); + expect(spritesheet.resolution).to.equal(2); + + this.validate(spritesheet, done); + }; + }); +}); diff --git a/test/core/Texture.js b/test/core/Texture.js new file mode 100644 index 0000000..8c4ef98 --- /dev/null +++ b/test/core/Texture.js @@ -0,0 +1,19 @@ +'use strict'; + +describe('PIXI.Texture', function () +{ + it('should register Texture from Loader', function () + { + const URL = 'foo.png'; + const NAME = 'bar'; + const image = new Image(); + + const texture = PIXI.Texture.fromLoader(image, URL, NAME); + + expect(texture.baseTexture.imageUrl).to.equal('foo.png'); + expect(PIXI.utils.TextureCache[NAME]).to.equal(texture); + expect(PIXI.utils.BaseTextureCache[NAME]).to.equal(texture.baseTexture); + expect(PIXI.utils.TextureCache[URL]).to.equal(texture); + expect(PIXI.utils.BaseTextureCache[URL]).to.equal(texture.baseTexture); + }); +}); diff --git a/test/core/WebGLRenderer.js b/test/core/WebGLRenderer.js index 9bc3bc2..3d6cf98 100644 --- a/test/core/WebGLRenderer.js +++ b/test/core/WebGLRenderer.js @@ -1,21 +1,21 @@ 'use strict'; -function isWebGLSupported(fn) -{ - return PIXI.utils.isWebGLSupported() ? fn : function () {}; // eslint-disable-line no-empty-function -} +const withGL = require('../withGL'); -describe('PIXI.WebGLRenderer', isWebGLSupported(function () +describe('PIXI.WebGLRenderer', function () { - it('setting option legacy should disable VAOs and set minimum SPRITE_MAX_TEXTURES to 1', function () + it('setting option legacy should disable VAOs and SPRITE_MAX_TEXTURES', withGL(function () { const renderer = new PIXI.WebGLRenderer(1, 1, { legacy: true }); - - expect(PIXI.glCore.VertexArrayObject.FORCE_NATIVE).to.equal(true); - expect(renderer.plugins.sprite.MAX_TEXTURES).to.equal(1); - - renderer.destroy(); - }); -})); - + try + { + expect(PIXI.glCore.VertexArrayObject.FORCE_NATIVE).to.equal(true); + expect(renderer.plugins.sprite.MAX_TEXTURES).to.equal(1); + } + finally + { + renderer.destroy(); + } + })); +}); diff --git a/test/core/filters.js b/test/core/filters.js new file mode 100644 index 0000000..df459f6 --- /dev/null +++ b/test/core/filters.js @@ -0,0 +1,21 @@ +'use strict'; + +describe('PIXI.filters', function () +{ + it('should correctly form uniformData', function () + { + const sprite = new PIXI.Sprite(PIXI.Texture.EMPTY); + const displ = new PIXI.filters.DisplacementFilter(sprite); + + expect(!!displ.uniformData.scale).to.be.true; + expect(!!displ.uniformData.filterMatrix).to.be.true; + expect(!!displ.uniformData.mapSampler).to.be.true; + // it does have filterClamp, but it is handled by FilterManager + expect(!!displ.uniformData.filterClamp).to.be.false; + + const fxaa = new PIXI.filters.FXAAFilter(); + + // it does have filterArea, but it is handled by FilterManager + expect(!!fxaa.uniformData.filterArea).to.be.false; + }); +}); diff --git a/test/core/index.js b/test/core/index.js index 63a8f21..8530131 100755 --- a/test/core/index.js +++ b/test/core/index.js @@ -7,6 +7,7 @@ require('./DisplayObject'); require('./getLocalBounds'); require('./Sprite'); +require('./Spritesheet'); require('./TilingSprite'); require('./TextStyle'); require('./Text'); @@ -25,3 +26,5 @@ require('./SpriteRenderer'); require('./WebGLRenderer'); require('./Ellipse'); +require('./Texture'); +require('./filters'); diff --git a/test/core/resources/building1.json b/test/core/resources/building1.json new file mode 100755 index 0000000..03fa5c1 --- /dev/null +++ b/test/core/resources/building1.json @@ -0,0 +1,21 @@ +{"frames": { + +"goldmine_10_5.png": +{ + "frame": {"x":1,"y":1,"w":95,"h":115}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":95,"h":115}, + "sourceSize": {"w":95,"h":115}, + "pivot": {"x":0.5,"y":0.5} +}}, +"meta": { + "app": "http://www.codeandweb.com/texturepacker", + "version": "1.0", + "image": "building1.png", + "format": "RGBA8888", + "size": {"w":128,"h":128}, + "scale": "0.5", + "smartupdate": "$TexturePacker:SmartUpdate:d7a5e54c8f8a3fecd508baf190c44807:99a0e3a4dc0f441e4aad77614191ab38:6046b8eb706ddefaa771c33ceb7cb6d5$" +} +} diff --git a/test/core/resources/building1.png b/test/core/resources/building1.png new file mode 100755 index 0000000..7e1e114 --- /dev/null +++ b/test/core/resources/building1.png Binary files differ diff --git a/test/core/resources/building1@2x.json b/test/core/resources/building1@2x.json new file mode 100755 index 0000000..24e25ff --- /dev/null +++ b/test/core/resources/building1@2x.json @@ -0,0 +1,21 @@ +{"frames": { + +"goldmine_10_5.png": +{ + "frame": {"x":1,"y":1,"w":190,"h":229}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":190,"h":229}, + "sourceSize": {"w":190,"h":229}, + "pivot": {"x":0.5,"y":0.5} +}}, +"meta": { + "app": "http://www.codeandweb.com/texturepacker", + "version": "1.0", + "image": "building1@2x.png", + "format": "RGBA8888", + "size": {"w":256,"h":256}, + "scale": "1", + "smartupdate": "$TexturePacker:SmartUpdate:d7a5e54c8f8a3fecd508baf190c44807:99a0e3a4dc0f441e4aad77614191ab38:6046b8eb706ddefaa771c33ceb7cb6d5$" +} +} diff --git a/package.json b/package.json index 6a366ba..71ade15 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pixi.js", - "version": "4.3.4", + "version": "4.3.5", "description": "Pixi.js is a fast lightweight 2D library that works across all devices.", "author": "Mat Groves", "contributors": [ @@ -61,7 +61,7 @@ "ismobilejs": "^0.4.0", "object-assign": "^4.0.1", "pixi-gl-core": "^1.0.3", - "resource-loader": "^2.0.4" + "resource-loader": "^2.0.6" }, "devDependencies": { "babel-cli": "^6.18.0", @@ -70,9 +70,9 @@ "babel-preset-es2015": "^6.14.0", "babelify": "^7.3.0", "del": "^2.2.0", - "electron-prebuilt": "^1.3.2", + "electron": "^1.4.15", "eslint": "^3.5.0", - "floss": "^1.2.0", + "floss": "^2.0.1", "gh-pages": "^0.11.0", "jaguarjs-jsdoc": "^1.0.1", "js-md5": "^0.4.1", diff --git a/src/accessibility/AccessibilityManager.js b/src/accessibility/AccessibilityManager.js index df2716a..f16bcb1 100644 --- a/src/accessibility/AccessibilityManager.js +++ b/src/accessibility/AccessibilityManager.js @@ -3,7 +3,7 @@ import accessibleTarget from './accessibleTarget'; // add some extra variables to the container.. -Object.assign( +core.utils.mixins.delayMixin( core.DisplayObject.prototype, accessibleTarget ); diff --git a/src/core/Application.js b/src/core/Application.js index ea0f058..5340578 100644 --- a/src/core/Application.js +++ b/src/core/Application.js @@ -1,6 +1,6 @@ import { autoDetectRenderer } from './autoDetectRenderer'; import Container from './display/Container'; -import Ticker from './ticker/Ticker'; +import { shared, Ticker } from './ticker'; /** * Convenience class to create a new PIXI application. @@ -35,9 +35,9 @@ * @param {boolean} [noWebGL=false] - prevents selection of WebGL renderer, even if such is present * @param {boolean} [options.legacy=false] - If true Pixi will aim to ensure compatibility * with older / less advanced devices. If you experience unexplained flickering try setting this to true. - + * @param {boolean} [useSharedTicker=false] - `true` to use PIXI.ticker.shared, `false` to create new ticker. */ - constructor(width, height, options, noWebGL) + constructor(width, height, options, noWebGL, useSharedTicker = false) { /** * WebGL renderer if available, otherwise CanvasRenderer @@ -46,23 +46,46 @@ this.renderer = autoDetectRenderer(width, height, options, noWebGL); /** - * The root display container that's renderered. + * The root display container that's rendered. * @member {PIXI.Container} */ this.stage = new Container(); /** + * Internal reference to the ticker + * @member {PIXI.ticker.Ticker} + * @private + */ + this._ticker = null; + + /** * Ticker for doing render updates. * @member {PIXI.ticker.Ticker} + * @default PIXI.ticker.shared */ - this.ticker = new Ticker(); - - this.ticker.add(this.render, this); + this.ticker = useSharedTicker ? shared : new Ticker(); // Start the rendering this.start(); } + set ticker(ticker) // eslint-disable-line require-jsdoc + { + if (this._ticker) + { + this._ticker.remove(this.render, this); + } + this._ticker = ticker; + if (ticker) + { + ticker.add(this.render, this); + } + } + get ticker() // eslint-disable-line require-jsdoc + { + return this._ticker; + } + /** * Render the current stage. */ @@ -76,7 +99,7 @@ */ stop() { - this.ticker.stop(); + this._ticker.stop(); } /** @@ -84,7 +107,7 @@ */ start() { - this.ticker.start(); + this._ticker.start(); } /** @@ -98,13 +121,22 @@ } /** + * Reference to the renderer's screen rectangle. Its safe to use as filterArea or hitArea for whole screen + * @member {PIXI.Rectangle} + * @readonly + */ + get screen() + { + return this.renderer.screen; + } + + /** * Destroy and don't use after this. * @param {Boolean} [removeView=false] Automatically remove canvas from DOM. */ destroy(removeView) { this.stop(); - this.ticker.remove(this.render, this); this.ticker = null; this.stage.destroy(); diff --git a/src/core/Shader.js b/src/core/Shader.js new file mode 100644 index 0000000..78b17e6 --- /dev/null +++ b/src/core/Shader.js @@ -0,0 +1,46 @@ +import { GLShader } from 'pixi-gl-core'; +import settings from './settings'; + +function checkPrecision(src, def) +{ + if (src instanceof Array) + { + if (src[0].substring(0, 9) !== 'precision') + { + const copy = src.slice(0); + + copy.unshift(`precision ${def} float;`); + + return copy; + } + } + else if (src.substring(0, 9) !== 'precision') + { + return `precision ${def} float;\n${src}`; + } + + return src; +} + +/** + * Wrapper class, webGL Shader for Pixi. + * Adds precision string if vertexSrc or fragmentSrc have no mention of it. + * + * @class + * @extends GLShader + * @memberof PIXI + */ +export default class Shader extends GLShader +{ + /** + * + * @param {WebGLRenderingContext} gl - The current WebGL rendering context + * @param {string|string[]} vertexSrc - The vertex shader source as an array of strings. + * @param {string|string[]} fragmentSrc - The fragment shader source as an array of strings. + */ + constructor(gl, vertexSrc, fragmentSrc) + { + super(gl, checkPrecision(vertexSrc, settings.PRECISION_VERTEX), + checkPrecision(fragmentSrc, settings.PRECISION_FRAGMENT)); + } +} diff --git a/src/core/const.js b/src/core/const.js index ef6f662..3de0efe 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -188,7 +188,7 @@ * The gc modes that are supported by pixi. * * The {@link PIXI.settings.GC_MODE} Garbage Collection mode for pixi textures is AUTO - * If set to GC_MODE, the renderer will occasianally check textures usage. If they are not + * If set to GC_MODE, the renderer will occasionally check textures usage. If they are not * used for a specified period of time they will be removed from the GPU. They will of course * be uploaded again when they are required. This is a silent behind the scenes process that * should ensure that the GPU does not get filled up. diff --git a/src/core/display/Container.js b/src/core/display/Container.js index ed8cd5f..17fde8d 100644 --- a/src/core/display/Container.js +++ b/src/core/display/Container.js @@ -73,11 +73,12 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.push(child); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -108,11 +109,12 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.splice(index, 0, child); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -225,10 +227,11 @@ if (index === -1) return null; child.parent = null; + // ensure child transform will be recalculated + child.transform._parentID = -1; removeItems(this.children, index, 1); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -249,11 +252,12 @@ { const child = this.getChildAt(index); + // ensure child transform will be recalculated.. child.parent = null; + child.transform._parentID = -1; removeItems(this.children, index, 1); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -284,13 +288,12 @@ for (let i = 0; i < removed.length; ++i) { removed[i].parent = null; + if (removed[i].transform) + { + removed[i].transform._parentID = -1; + } } - // ensure a transform will be recalculated.. - if (this.transform) - { - this.transform._parentID = -1; - } this._boundsID++; this.onChildrenChange(beginIndex); diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index f37f2f5..be4c4fb 100644 --- a/src/core/graphics/Graphics.js +++ b/src/core/graphics/Graphics.js @@ -28,8 +28,9 @@ { /** * + * @param {boolean} [nativeLines=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP */ - constructor() + constructor(nativeLines = false) { super(); @@ -50,6 +51,13 @@ this.lineWidth = 0; /** + * If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * + * @member {boolean} + */ + this.nativeLines = nativeLines; + + /** * The color of any lines drawn. * * @member {string} @@ -757,28 +765,14 @@ if (!this._spriteRect) { - if (!Graphics._SPRITE_TEXTURE) - { - Graphics._SPRITE_TEXTURE = RenderTexture.create(10, 10); - - const canvas = document.createElement('canvas'); - - canvas.width = 10; - canvas.height = 10; - - const context = canvas.getContext('2d'); - - context.fillStyle = 'white'; - context.fillRect(0, 0, 10, 10); - - Graphics._SPRITE_TEXTURE = Texture.fromCanvas(canvas); - } - - this._spriteRect = new Sprite(Graphics._SPRITE_TEXTURE); + this._spriteRect = new Sprite(new Texture(Texture.WHITE)); } + + const sprite = this._spriteRect; + if (this.tint === 0xffffff) { - this._spriteRect.tint = this.graphicsData[0].fillColor; + sprite.tint = this.graphicsData[0].fillColor; } else { @@ -792,20 +786,21 @@ t1[1] *= t2[1]; t1[2] *= t2[2]; - this._spriteRect.tint = rgb2hex(t1); + sprite.tint = rgb2hex(t1); } - this._spriteRect.alpha = this.graphicsData[0].fillAlpha; - this._spriteRect.worldAlpha = this.worldAlpha * this._spriteRect.alpha; + sprite.alpha = this.graphicsData[0].fillAlpha; + sprite.worldAlpha = this.worldAlpha * sprite.alpha; + sprite.blendMode = this.blendMode; - Graphics._SPRITE_TEXTURE._frame.width = rect.width; - Graphics._SPRITE_TEXTURE._frame.height = rect.height; + sprite.texture._frame.width = rect.width; + sprite.texture._frame.height = rect.height; - this._spriteRect.transform.worldTransform = this.transform.worldTransform; + sprite.transform.worldTransform = this.transform.worldTransform; - this._spriteRect.anchor.set(-rect.x / rect.width, -rect.y / rect.height); - this._spriteRect._onAnchorUpdate(); + sprite.anchor.set(-rect.x / rect.width, -rect.y / rect.height); + sprite._onAnchorUpdate(); - this._spriteRect._renderWebGL(renderer); + sprite._renderWebGL(renderer); } /** @@ -1031,6 +1026,7 @@ this.fillColor, this.fillAlpha, this.filling, + this.nativeLines, shape ); diff --git a/src/core/graphics/GraphicsData.js b/src/core/graphics/GraphicsData.js index f5a87ae..b6f03c0 100644 --- a/src/core/graphics/GraphicsData.js +++ b/src/core/graphics/GraphicsData.js @@ -14,14 +14,19 @@ * @param {number} fillColor - the color of the fill * @param {number} fillAlpha - the alpha of the fill * @param {boolean} fill - whether or not the shape is filled with a colour + * @param {boolean} nativeLines - the method for drawing lines * @param {PIXI.Circle|PIXI.Rectangle|PIXI.Ellipse|PIXI.Polygon} shape - The shape object to draw. */ - constructor(lineWidth, lineColor, lineAlpha, fillColor, fillAlpha, fill, shape) + constructor(lineWidth, lineColor, lineAlpha, fillColor, fillAlpha, fill, nativeLines, shape) { /** * @member {number} the width of the line to draw */ this.lineWidth = lineWidth; + /** + * @member {boolean} if true the liens will be draw using LINES instead of TRIANGLE_STRIP + */ + this.nativeLines = nativeLines; /** * @member {number} the color of the line to draw @@ -85,6 +90,7 @@ this.fillColor, this.fillAlpha, this.fill, + this.nativeLines, this.shape ); } diff --git a/src/core/graphics/webgl/GraphicsRenderer.js b/src/core/graphics/webgl/GraphicsRenderer.js index 14083fe..67f5e92 100644 --- a/src/core/graphics/webgl/GraphicsRenderer.js +++ b/src/core/graphics/webgl/GraphicsRenderer.js @@ -102,7 +102,15 @@ shaderTemp.uniforms.alpha = graphics.worldAlpha; renderer.bindVao(webGLData.vao); - webGLData.vao.draw(gl.TRIANGLE_STRIP, webGLData.indices.length); + + if (graphics.nativeLines) + { + gl.drawArrays(gl.LINES, 0, webGLData.points.length / 6); + } + else + { + webGLData.vao.draw(gl.TRIANGLE_STRIP, webGLData.indices.length); + } } } diff --git a/src/core/graphics/webgl/utils/buildLine.js b/src/core/graphics/webgl/utils/buildLine.js index ecf43f7..4d6862e 100644 --- a/src/core/graphics/webgl/utils/buildLine.js +++ b/src/core/graphics/webgl/utils/buildLine.js @@ -11,7 +11,29 @@ * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties * @param {object} webGLData - an object containing all the webGL-specific information to create this shape */ -export default function buildLine(graphicsData, webGLData) +export default function (graphicsData, webGLData) +{ + if (graphicsData.nativeLines) + { + buildNativeLine(graphicsData, webGLData); + } + else + { + buildLine(graphicsData, webGLData); + } +} + +/** + * Builds a line to draw using the poligon method. + * + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @private + * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties + * @param {object} webGLData - an object containing all the webGL-specific information to create this shape + */ +function buildLine(graphicsData, webGLData) { // TODO OPTIMISE! let points = graphicsData.points; @@ -224,3 +246,46 @@ indices.push(indexStart - 1); } + +/** + * Builds a line to draw using the gl.drawArrays(gl.LINES) method + * + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @private + * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties + * @param {object} webGLData - an object containing all the webGL-specific information to create this shape + */ +function buildNativeLine(graphicsData, webGLData) +{ + let i = 0; + const points = graphicsData.points; + + if (points.length === 0) return; + + const verts = webGLData.points; + const length = points.length / 2; + + // sort color + const color = hex2rgb(graphicsData.lineColor); + const alpha = graphicsData.lineAlpha; + const r = color[0] * alpha; + const g = color[1] * alpha; + const b = color[2] * alpha; + + for (i = 1; i < length; i++) + { + const p1x = points[(i - 1) * 2]; + const p1y = points[((i - 1) * 2) + 1]; + + const p2x = points[i * 2]; + const p2y = points[(i * 2) + 1]; + + verts.push(p1x, p1y); + verts.push(r, g, b, alpha); + + verts.push(p2x, p2y); + verts.push(r, g, b, alpha); + } +} diff --git a/src/core/graphics/webgl/utils/buildRoundedRectangle.js b/src/core/graphics/webgl/utils/buildRoundedRectangle.js index 9abc807..a6c8461 100644 --- a/src/core/graphics/webgl/utils/buildRoundedRectangle.js +++ b/src/core/graphics/webgl/utils/buildRoundedRectangle.js @@ -77,6 +77,26 @@ } /** + * Calculate a single point for a quadratic bezier curve. + * Utility function used by quadraticBezierCurve. + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @private + * @param {number} n1 - first number + * @param {number} n2 - second number + * @param {number} perc - percentage + * @return {number} the result + * + */ +function getPt(n1, n2, perc) +{ + const diff = n2 - n1; + + return n1 + (diff * perc); +} + +/** * Calculate the points for a quadratic bezier curve. (helper function..) * Based on: https://stackoverflow.com/questions/785097/how-do-i-implement-a-bezier-curve-in-c * @@ -105,13 +125,6 @@ let x = 0; let y = 0; - function getPt(n1, n2, perc) - { - const diff = n2 - n1; - - return n1 + (diff * perc); - } - for (let i = 0, j = 0; i <= n; ++i) { j = i / n; diff --git a/src/core/index.js b/src/core/index.js index f7a817c..4f414e8 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -30,6 +30,7 @@ export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; export { default as CanvasGraphicsRenderer } from './graphics/canvas/CanvasGraphicsRenderer'; +export { default as Spritesheet } from './textures/Spritesheet'; export { default as Texture } from './textures/Texture'; export { default as BaseTexture } from './textures/BaseTexture'; export { default as RenderTexture } from './textures/RenderTexture'; diff --git a/src/core/math/Matrix.js b/src/core/math/Matrix.js index 0b77477..3679860 100644 --- a/src/core/math/Matrix.js +++ b/src/core/math/Matrix.js @@ -13,45 +13,50 @@ export default class Matrix { /** - * + * @param {number} [a=1] - x scale + * @param {number} [b=0] - y skew + * @param {number} [c=0] - x skew + * @param {number} [d=1] - y scale + * @param {number} [tx=0] - x translation + * @param {number} [ty=0] - y translation */ - constructor() + constructor(a = 1, b = 0, c = 0, d = 1, tx = 0, ty = 0) { /** * @member {number} * @default 1 */ - this.a = 1; + this.a = a; /** * @member {number} * @default 0 */ - this.b = 0; + this.b = b; /** * @member {number} * @default 0 */ - this.c = 0; + this.c = c; /** * @member {number} * @default 1 */ - this.d = 1; + this.d = d; /** * @member {number} * @default 0 */ - this.tx = 0; + this.tx = tx; /** * @member {number} * @default 0 */ - this.ty = 0; + this.ty = ty; this.array = null; } diff --git a/src/core/renderers/SystemRenderer.js b/src/core/renderers/SystemRenderer.js index fa116a6..a24c775 100644 --- a/src/core/renderers/SystemRenderer.js +++ b/src/core/renderers/SystemRenderer.js @@ -1,5 +1,5 @@ import { sayHello, hex2string, hex2rgb } from '../utils'; -import { Matrix } from '../math'; +import { Matrix, Rectangle } from '../math'; import { RENDERER_TYPE } from '../const'; import settings from '../settings'; import Container from '../display/Container'; @@ -21,8 +21,8 @@ { /** * @param {string} system - The name of the system this renderer is for. - * @param {number} [width=800] - the width of the canvas view - * @param {number} [height=600] - the height of the canvas view + * @param {number} [screenWidth=800] - the width of the screen + * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false @@ -37,7 +37,7 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(system, width, height, options) + constructor(system, screenWidth, screenHeight, options) { super(); @@ -69,20 +69,13 @@ this.type = RENDERER_TYPE.UNKNOWN; /** - * The width of the canvas view + * Measurements of the screen. (0, 0, screenWidth, screenHeight) * - * @member {number} - * @default 800 - */ - this.width = width || 800; - - /** - * The height of the canvas view + * Its safe to use as filterArea or hitArea for whole stage * - * @member {number} - * @default 600 + * @member {PIXI.Rectangle} */ - this.height = height || 600; + this.screen = new Rectangle(0, 0, screenWidth || 800, screenHeight || 600); /** * The canvas element that everything is drawn to @@ -107,7 +100,7 @@ this.transparent = options.transparent; /** - * Whether the render view should be resized automatically + * Whether css dimensions of canvas view should be resized to screen dimensions automatically * * @member {boolean} */ @@ -192,23 +185,48 @@ } /** - * Resizes the canvas view to the specified width and height + * Same as view.width, actual number of pixels in the canvas by horizontal * - * @param {number} width - the new width of the canvas view - * @param {number} height - the new height of the canvas view + * @member {number} + * @readonly + * @default 800 */ - resize(width, height) + get width() { - this.width = width * this.resolution; - this.height = height * this.resolution; + return this.view.width; + } - this.view.width = this.width; - this.view.height = this.height; + /** + * Same as view.height, actual number of pixels in the canvas by vertical + * + * @member {number} + * @readonly + * @default 600 + */ + get height() + { + return this.view.height; + } + + /** + * Resizes the screen and canvas to the specified width and height + * Canvas dimensions are multiplied by resolution + * + * @param {number} screenWidth - the new width of the screen + * @param {number} screenHeight - the new height of the screen + */ + resize(screenWidth, screenHeight) + { + this.screen.width = screenWidth; + this.screen.height = screenHeight; + + this.view.width = screenWidth * this.resolution; + this.view.height = screenHeight * this.resolution; if (this.autoResize) { - this.view.style.width = `${this.width / this.resolution}px`; - this.view.style.height = `${this.height / this.resolution}px`; + this.view.style.width = `${screenWidth}px`; + this.view.style.height = `${screenHeight}px`; } } @@ -249,11 +267,10 @@ this.type = RENDERER_TYPE.UNKNOWN; - this.width = 0; - this.height = 0; - this.view = null; + this.screen = null; + this.resolution = 0; this.transparent = false; diff --git a/src/core/renderers/canvas/CanvasRenderer.js b/src/core/renderers/canvas/CanvasRenderer.js index 1bd2d5e..e84e49c 100644 --- a/src/core/renderers/canvas/CanvasRenderer.js +++ b/src/core/renderers/canvas/CanvasRenderer.js @@ -18,8 +18,8 @@ export default class CanvasRenderer extends SystemRenderer { /** - * @param {number} [width=800] - the width of the canvas view - * @param {number} [height=600] - the height of the canvas view + * @param {number} [screenWidth=800] - the width of the screen + * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false @@ -34,9 +34,9 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(width, height, options = {}) + constructor(screenWidth, screenHeight, options = {}) { - super('Canvas', width, height, options); + super('Canvas', screenWidth, screenHeight, options); this.type = RENDERER_TYPE.CANVAS; @@ -96,7 +96,7 @@ this.context = null; this.renderingToScreen = false; - this.resize(width, height); + this.resize(screenWidth, screenHeight); } /** @@ -283,12 +283,12 @@ * * @extends PIXI.SystemRenderer#resize * - * @param {number} width - The new width of the canvas view - * @param {number} height - The new height of the canvas view + * @param {number} screenWidth - the new width of the screen + * @param {number} screenHeight - the new height of the screen */ - resize(width, height) + resize(screenWidth, screenHeight) { - super.resize(width, height); + super.resize(screenWidth, screenHeight); // reset the scale mode.. oddly this seems to be reset when the canvas is resized. // surely a browser bug?? Let pixi fix that for you.. diff --git a/src/core/renderers/webgl/TextureManager.js b/src/core/renderers/webgl/TextureManager.js index b685c1a..8227289 100644 --- a/src/core/renderers/webgl/TextureManager.js +++ b/src/core/renderers/webgl/TextureManager.js @@ -61,7 +61,7 @@ * Updates and/or Creates a WebGL texture for the renderer's context. * * @param {PIXI.BaseTexture|PIXI.Texture} texture - the texture to update - * @param {Number} location - the location the texture will be bound to. + * @param {number} location - the location the texture will be bound to. * @return {GLTexture} The gl texture. */ updateTexture(texture, location) diff --git a/src/core/renderers/webgl/WebGLRenderer.js b/src/core/renderers/webgl/WebGLRenderer.js index e4cd8a0..830b382 100644 --- a/src/core/renderers/webgl/WebGLRenderer.js +++ b/src/core/renderers/webgl/WebGLRenderer.js @@ -31,8 +31,8 @@ { /** * - * @param {number} [width=0] - the width of the canvas view - * @param {number} [height=0] - the height of the canvas view + * @param {number} [screenWidth=800] - the width of the screen + * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false @@ -53,9 +53,9 @@ * @param {boolean} [options.legacy=false] - If true Pixi will aim to ensure compatibility * with older / less advanced devices. If you experiance unexplained flickering try setting this to true. */ - constructor(width, height, options = {}) + constructor(screenWidth, screenHeight, options = {}) { - super('WebGL', width, height, options); + super('WebGL', screenWidth, screenHeight, options); this.legacy = !!options.legacy; @@ -245,7 +245,7 @@ glCore._testingContext = gl; // setup the width/height properties and gl viewport - this.resize(this.width, this.height); + this.resize(this.screen.width, this.screen.height); } /** @@ -343,16 +343,16 @@ /** * Resizes the webGL view to the specified width and height. * - * @param {number} width - the new width of the webGL view - * @param {number} height - the new height of the webGL view + * @param {number} screenWidth - the new width of the screen + * @param {number} screenHeight - the new height of the screen */ - resize(width, height) + resize(screenWidth, screenHeight) { // if(width * this.resolution === this.width && height * this.resolution === this.height)return; - SystemRenderer.prototype.resize.call(this, width, height); + SystemRenderer.prototype.resize.call(this, screenWidth, screenHeight); - this.rootRenderTarget.resize(width, height); + this.rootRenderTarget.resize(screenWidth, screenHeight); if (this._activeRenderTarget === this.rootRenderTarget) { @@ -396,6 +396,26 @@ } /** + * Erases the render texture and fills the drawing area with a colour + * + * @param {PIXI.RenderTexture} renderTexture - The render texture to clear + * @param {number} [clearColor] - The colour + * @return {PIXI.WebGLRenderer} Returns itself. + */ + clearRenderTexture(renderTexture, clearColor) + { + const baseTexture = renderTexture.baseTexture; + const renderTarget = baseTexture._glRenderTargets[this.CONTEXT_UID]; + + if (renderTarget) + { + renderTarget.clear(clearColor); + } + + return this; + } + + /** * Binds a render texture for rendering * * @param {PIXI.RenderTexture} renderTexture - The render texture to render @@ -474,9 +494,10 @@ * Changes the current GLShader to the one given in parameter * * @param {PIXI.glCore.Shader} shader - the new glShader + * @param {boolean} [autoProject=true] - Whether automatically set the projection matrix * @return {PIXI.WebGLRenderer} Returns itself. */ - _bindGLShader(shader) + _bindGLShader(shader, autoProject) { // TODO cache if (this._activeShader !== shader) @@ -484,8 +505,14 @@ this._activeShader = shader; shader.bind(); - // automatically set the projection matrix - shader.uniforms.projectionMatrix = this._activeRenderTarget.projectionMatrix.toArray(true); + // `autoProject` normally would be a default parameter set to true + // but because of how Babel transpiles default parameters + // it hinders the performance of this method. + if (autoProject !== false) + { + // automatically set the projection matrix + shader.uniforms.projectionMatrix = this._activeRenderTarget.projectionMatrix.toArray(true); + } } return this; diff --git a/src/core/renderers/webgl/WebGLState.js b/src/core/renderers/webgl/WebGLState.js index ba6ecc5..6c846d5 100755 --- a/src/core/renderers/webgl/WebGLState.js +++ b/src/core/renderers/webgl/WebGLState.js @@ -82,18 +82,20 @@ push() { // next state.. - let state = this.stack[++this.stackIndex]; + let state = this.stack[this.stackIndex]; if (!state) { state = this.stack[this.stackIndex] = new Uint8Array(16); } + ++this.stackIndex; + // copy state.. // set active state so we can force overrides of gl state for (let i = 0; i < this.activeState.length; i++) { - this.activeState[i] = state[i]; + state[i] = this.activeState[i]; } } diff --git a/src/core/renderers/webgl/filters/Filter.js b/src/core/renderers/webgl/filters/Filter.js index 2c3eaad..4eb6128 100644 --- a/src/core/renderers/webgl/filters/Filter.js +++ b/src/core/renderers/webgl/filters/Filter.js @@ -9,7 +9,7 @@ * @memberof PIXI * @extends PIXI.Shader */ -class Filter extends Shader +export default class Filter extends Shader { /** * @param {string} [vertexSrc] - The source of the vertex shader. @@ -56,15 +56,16 @@ * @param {PIXI.RenderTarget} input - The input render target. * @param {PIXI.RenderTarget} output - The target to output to. * @param {boolean} clear - Should the output be cleared before rendering to it + * @param {object} [currentState] - It's current state of filter. + * There are some useful properties in the currentState : + * target, filters, sourceFrame, destinationFrame, renderTarget, resolution */ - apply(filterManager, input, output, clear) + apply(filterManager, input, output, clear, currentState) // eslint-disable-line no-unused-vars { // do as you please! - filterManager.applyFilter(this, input, output, clear); + filterManager.applyFilter(this, input, output, clear, currentState); // or just do a regular render.. } } - -export default Filter; diff --git a/src/core/renderers/webgl/filters/extractUniformsFromSrc.js b/src/core/renderers/webgl/filters/extractUniformsFromSrc.js index d78a0b1..4893214 100644 --- a/src/core/renderers/webgl/filters/extractUniformsFromSrc.js +++ b/src/core/renderers/webgl/filters/extractUniformsFromSrc.js @@ -12,7 +12,7 @@ function extractUniformsFromString(string) { - const maskRegex = new RegExp('^(projectionMatrix|uSampler|filterArea)$'); + const maskRegex = new RegExp('^(projectionMatrix|uSampler|filterArea|filterClamp)$'); const uniforms = {}; let nameSplit; diff --git a/src/core/renderers/webgl/managers/FilterManager.js b/src/core/renderers/webgl/managers/FilterManager.js index 43fc869..6c7422d 100644 --- a/src/core/renderers/webgl/managers/FilterManager.js +++ b/src/core/renderers/webgl/managers/FilterManager.js @@ -2,6 +2,7 @@ import RenderTarget from '../utils/RenderTarget'; import Quad from '../utils/Quad'; import { Rectangle } from '../../../math'; +import Shader from '../../../Shader'; import * as filterTransforms from '../filters/filterTransforms'; import bitTwiddle from 'bit-twiddle'; @@ -22,8 +23,6 @@ this.filters = []; this.target = null; this.resolution = 1; - - this.firstRun = true; } } @@ -155,7 +154,7 @@ if (filters.length === 1) { - filters[0].apply(this, currentState.renderTarget, lastState.renderTarget, false); + filters[0].apply(this, currentState.renderTarget, lastState.renderTarget, false, currentState); this.freePotRenderTarget(currentState.renderTarget); } else @@ -177,7 +176,7 @@ for (i = 0; i < filters.length - 1; ++i) { - filters[i].apply(this, flip, flop, true); + filters[i].apply(this, flip, flop, true, currentState); const t = flip; @@ -185,7 +184,7 @@ flop = t; } - filters[i].apply(this, flip, lastState.renderTarget, false); + filters[i].apply(this, flip, lastState.renderTarget, false, currentState); this.freePotRenderTarget(flip); this.freePotRenderTarget(flop); @@ -212,16 +211,28 @@ const renderer = this.renderer; const gl = renderer.gl; - renderer.bindShader(filter, true); + let shader = filter.glShaders[renderer.CONTEXT_UID]; - // TODO theres a better way! - // quad can be a mesh and we then should be able to pull this off without accessing the shader directly - const shader = renderer.shaderManager.getGLShader(); - - // TODO change quad to mesh.. - if (!this.firstRun) + // cacheing.. + if (!shader) { - this.firstRun = true; + if (filter.glShaderKey) + { + shader = this.shaderCache[filter.glShaderKey]; + + if (!shader) + { + shader = new Shader(this.gl, filter.vertexSrc, filter.fragmentSrc); + + filter.glShaders[renderer.CONTEXT_UID] = this.shaderCache[filter.glShaderKey] = shader; + } + } + else + { + shader = filter.glShaders[renderer.CONTEXT_UID] = new Shader(this.gl, filter.vertexSrc, filter.fragmentSrc); + } + + // TODO - this only needs to be done once? renderer.bindVao(null); this.quad.initVao(shader); @@ -244,58 +255,146 @@ renderer.maskManager.pushScissorMask(null, renderer.maskManager.scissorData); } - // renderer._bindGLShader(shader); + renderer.bindShader(shader); - let currentState; - const uniforms = filter.uniforms; + // free unit 0 for us, doesn't matter what was there + // don't try to restore it, because syncUniforms can upload it to another slot + // and it'll be a problem + const tex = this.renderer.emptyTextures[0]; - if (uniforms.filterArea) - { - currentState = this.filterData.stack[this.filterData.index]; - - const filterArea = uniforms.filterArea; - - filterArea[0] = currentState.renderTarget.size.width; - filterArea[1] = currentState.renderTarget.size.height; - filterArea[2] = currentState.sourceFrame.x; - filterArea[3] = currentState.sourceFrame.y; - - filter.uniforms.filterArea = filterArea; - } - - // use this to clamp displaced texture coords so they belong to filterArea - // see displacementFilter fragment shader for an example - if (uniforms.filterClamp) - { - currentState = this.filterData.stack[this.filterData.index]; - - const filterClamp = uniforms.filterClamp; - - filterClamp[0] = 0; - filterClamp[1] = 0; - filterClamp[2] = (currentState.sourceFrame.width - 1) / currentState.renderTarget.size.width; - filterClamp[3] = (currentState.sourceFrame.height - 1) / currentState.renderTarget.size.height; - - filter.uniforms.filterClamp = filterClamp; - } - - renderer.shaderManager.setUniforms(filter.uniforms); + this.renderer.boundTextures[0] = tex; + // this syncs the pixi filters uniforms with glsl uniforms + this.syncUniforms(shader, filter); renderer.state.setBlendMode(filter.blendMode); - // temporary bypass cache.. - const tex = this.renderer.boundTextures[0]; - gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, input.texture.texture); this.quad.vao.draw(this.renderer.gl.TRIANGLES, 6, 0); - // restore cache. gl.bindTexture(gl.TEXTURE_2D, tex._glTextures[this.renderer.CONTEXT_UID].texture); } /** + * Uploads the uniforms of the filter. + * + * @param {GLShader} shader - The underlying gl shader. + * @param {PIXI.Filter} filter - The filter we are synchronizing. + */ + syncUniforms(shader, filter) + { + const uniformData = filter.uniformData; + const uniforms = filter.uniforms; + + // 0 is reserved for the pixi texture so we start at 1! + let textureCount = 1; + let currentState; + + // filterArea and filterClamp that are handled by FilterManager directly + // they must not appear in uniformData + + if (shader.uniforms.filterArea) + { + currentState = this.filterData.stack[this.filterData.index]; + + const filterArea = shader.uniforms.filterArea; + + filterArea[0] = currentState.renderTarget.size.width; + filterArea[1] = currentState.renderTarget.size.height; + filterArea[2] = currentState.sourceFrame.x; + filterArea[3] = currentState.sourceFrame.y; + + shader.uniforms.filterArea = filterArea; + } + + // use this to clamp displaced texture coords so they belong to filterArea + // see displacementFilter fragment shader for an example + if (shader.uniforms.filterClamp) + { + currentState = currentState || this.filterData.stack[this.filterData.index]; + + const filterClamp = shader.uniforms.filterClamp; + + filterClamp[0] = 0; + filterClamp[1] = 0; + filterClamp[2] = (currentState.sourceFrame.width - 1) / currentState.renderTarget.size.width; + filterClamp[3] = (currentState.sourceFrame.height - 1) / currentState.renderTarget.size.height; + + shader.uniforms.filterClamp = filterClamp; + } + + // TODO Cacheing layer.. + for (const i in uniformData) + { + if (uniformData[i].type === 'sampler2D' && uniforms[i] !== 0) + { + if (uniforms[i].baseTexture) + { + shader.uniforms[i] = this.renderer.bindTexture(uniforms[i].baseTexture, textureCount); + } + else + { + shader.uniforms[i] = textureCount; + + // TODO + // this is helpful as renderTargets can also be set. + // Although thinking about it, we could probably + // make the filter texture cache return a RenderTexture + // rather than a renderTarget + const gl = this.renderer.gl; + + this.renderer.boundTextures[textureCount] = this.renderer.emptyTextures[textureCount]; + gl.activeTexture(gl.TEXTURE0 + textureCount); + + uniforms[i].texture.bind(); + } + + textureCount++; + } + else if (uniformData[i].type === 'mat3') + { + // check if its pixi matrix.. + if (uniforms[i].a !== undefined) + { + shader.uniforms[i] = uniforms[i].toArray(true); + } + else + { + shader.uniforms[i] = uniforms[i]; + } + } + else if (uniformData[i].type === 'vec2') + { + // check if its a point.. + if (uniforms[i].x !== undefined) + { + const val = shader.uniforms[i] || new Float32Array(2); + + val[0] = uniforms[i].x; + val[1] = uniforms[i].y; + shader.uniforms[i] = val; + } + else + { + shader.uniforms[i] = uniforms[i]; + } + } + else if (uniformData[i].type === 'float') + { + if (shader.uniforms.data[i].value !== uniformData[i]) + { + shader.uniforms[i] = uniforms[i]; + } + } + else + { + shader.uniforms[i] = uniforms[i]; + } + } + } + + /** * Gets a render target from the pool, or creates a new one. * * @param {boolean} clear - Should we clear the render texture when we get it? @@ -442,7 +541,6 @@ renderTarget.resolution = resolution; renderTarget.defaultFrame.width = renderTarget.size.width = minWidth / resolution; renderTarget.defaultFrame.height = renderTarget.size.height = minHeight / resolution; - renderTarget.attachStencilBuffer(); return renderTarget; } diff --git a/src/core/settings.js b/src/core/settings.js index bb08cb0..f4dd087 100644 --- a/src/core/settings.js +++ b/src/core/settings.js @@ -77,11 +77,11 @@ * * @static * @memberof PIXI.settings - * @type {RegExp|string} + * @type {RegExp} * @example `@2x` - * @default /@(.+)x/ + * @default /@([0-9\.]+)x/ */ - RETINA_PREFIX: /@(.+)x/, + RETINA_PREFIX: /@([0-9\.]+)x/, /** * The default render options if none are supplied to {@link PIXI.WebGLRenderer} @@ -175,14 +175,24 @@ SCALE_MODE: 0, /** - * Default specify float precision in shaders. + * Default specify float precision in vertex shader. + * + * @static + * @memberof PIXI.settings + * @type {PIXI.PRECISION} + * @default PIXI.PRECISION.HIGH + */ + PRECISION_VERTEX: 'highp', + + /** + * Default specify float precision in fragment shader. * * @static * @memberof PIXI.settings * @type {PIXI.PRECISION} * @default PIXI.PRECISION.MEDIUM */ - PRECISION: 'mediump', + PRECISION_FRAGMENT: 'mediump', /** * Can we upload the same buffer in a single frame? diff --git a/src/core/sprites/Sprite.js b/src/core/sprites/Sprite.js index 28b0b18..121bf58 100644 --- a/src/core/sprites/Sprite.js +++ b/src/core/sprites/Sprite.js @@ -212,11 +212,11 @@ } else { - w0 = orig.width * (1 - anchor._x); - w1 = orig.width * -anchor._x; + w1 = -anchor._x * orig.width; + w0 = w1 + orig.width; - h0 = orig.height * (1 - anchor._y); - h1 = orig.height * -anchor._y; + h1 = -anchor._y * orig.height; + h0 = h1 + orig.height; } // xy @@ -269,11 +269,11 @@ const tx = wt.tx; const ty = wt.ty; - const w0 = (orig.width) * (1 - anchor._x); - const w1 = (orig.width) * -anchor._x; + const w1 = -anchor._x * orig.width; + const w0 = w1 + orig.width; - const h0 = orig.height * (1 - anchor._y); - const h1 = orig.height * -anchor._y; + const h1 = -anchor._y * orig.height; + const h0 = h1 + orig.height; // xy vertexData[0] = (a * w1) + (c * h1) + tx; @@ -440,7 +440,7 @@ * * @static * @param {number|string|PIXI.BaseTexture|HTMLCanvasElement|HTMLVideoElement} source Source to create texture from - * @return {PIXI.Sprite} The newly created texture + * @return {PIXI.Sprite} The newly created sprite */ static from(source) { @@ -538,8 +538,8 @@ } /** - * The tint applied to the sprite. This is a hex value. A value of - * 0xFFFFFF will remove any tint effect. + * The tint applied to the sprite. This is a hex value. + * A value of 0xFFFFFF will remove any tint effect. * * @member {number} * @default 0xFFFFFF diff --git a/src/core/sprites/canvas/CanvasTinter.js b/src/core/sprites/canvas/CanvasTinter.js index 1cf316b..27e5e0c 100644 --- a/src/core/sprites/canvas/CanvasTinter.js +++ b/src/core/sprites/canvas/CanvasTinter.js @@ -75,8 +75,8 @@ crop.width *= resolution; crop.height *= resolution; - canvas.width = crop.width; - canvas.height = crop.height; + canvas.width = Math.ceil(crop.width); + canvas.height = Math.ceil(crop.height); context.fillStyle = `#${(`00000${(color | 0).toString(16)}`).substr(-6)}`; @@ -130,8 +130,8 @@ crop.width *= resolution; crop.height *= resolution; - canvas.width = crop.width; - canvas.height = crop.height; + canvas.width = Math.ceil(crop.width); + canvas.height = Math.ceil(crop.height); context.globalCompositeOperation = 'copy'; context.fillStyle = `#${(`00000${(color | 0).toString(16)}`).substr(-6)}`; @@ -172,8 +172,8 @@ crop.width *= resolution; crop.height *= resolution; - canvas.width = crop.width; - canvas.height = crop.height; + canvas.width = Math.ceil(crop.width); + canvas.height = Math.ceil(crop.height); context.globalCompositeOperation = 'copy'; context.drawImage( diff --git a/src/core/sprites/webgl/SpriteRenderer.js b/src/core/sprites/webgl/SpriteRenderer.js index 2affcd5..7c352e2 100644 --- a/src/core/sprites/webgl/SpriteRenderer.js +++ b/src/core/sprites/webgl/SpriteRenderer.js @@ -30,7 +30,7 @@ /** * Number of values sent in the vertex buffer. - * positionX, positionY, colorR, colorG, colorB = 5 + * aVertexPosition(2), aTextureCoord(1), aColor(1), aTextureId(1) = 5 * * @member {number} */ @@ -75,7 +75,6 @@ this.shader = null; this.currentIndex = 0; - TICK = 0; this.groups = []; for (let k = 0; k < this.size; k++) diff --git a/src/core/textures/BaseTexture.js b/src/core/textures/BaseTexture.js index 853de7e..aeccf39 100644 --- a/src/core/textures/BaseTexture.js +++ b/src/core/textures/BaseTexture.js @@ -232,16 +232,24 @@ this.realWidth = this.source.naturalWidth || this.source.videoWidth || this.source.width; this.realHeight = this.source.naturalHeight || this.source.videoHeight || this.source.height; - this.width = this.realWidth / this.resolution; - this.height = this.realHeight / this.resolution; - - this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); + this._updateDimensions(); } this.emit('update', this); } /** + * Update dimensions from real values + */ + _updateDimensions() + { + this.width = this.realWidth / this.resolution; + this.height = this.realHeight / this.resolution; + + this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); + } + + /** * Load a source. * * If the source is not-immediately-available, such as an image that needs to be @@ -525,11 +533,7 @@ this.realWidth = Math.round(svgWidth * this.sourceScale); this.realHeight = Math.round(svgHeight * this.sourceScale); - this.width = this.realWidth / this.resolution; - this.height = this.realHeight / this.resolution; - - // Check pow2 after scale - this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); + this._updateDimensions(); // Create a canvas element const canvas = document.createElement('canvas'); @@ -690,4 +694,52 @@ return baseTexture; } + + /** + * Helper function that creates a base texture based on the source you provide. + * The source can be - image url, image element, canvas element. + * + * @static + * @param {string|HTMLImageElement|HTMLCanvasElement} source - The source to create base texture from. + * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values + * @param {number} [sourceScale=(auto)] - Scale for the original image, used with Svg images. + * @return {PIXI.BaseTexture} The new base texture. + */ + static from(source, scaleMode, sourceScale) + { + if (typeof source === 'string') + { + return BaseTexture.fromImage(source, undefined, scaleMode, sourceScale); + } + else if (source instanceof HTMLImageElement) + { + const imageUrl = source.src; + let baseTexture = BaseTextureCache[imageUrl]; + + if (!baseTexture) + { + baseTexture = new BaseTexture(source, scaleMode); + baseTexture.imageUrl = imageUrl; + + if (sourceScale) + { + baseTexture.sourceScale = sourceScale; + } + + // if there is an @2x at the end of the url we are going to assume its a highres image + baseTexture.resolution = getResolutionOfUrl(imageUrl); + + BaseTextureCache[imageUrl] = baseTexture; + } + + return baseTexture; + } + else if (source instanceof HTMLCanvasElement) + { + return BaseTexture.fromCanvas(source, scaleMode); + } + + // lets assume its a base texture! + return source; + } } diff --git a/src/core/textures/Spritesheet.js b/src/core/textures/Spritesheet.js new file mode 100644 index 0000000..d469c62 --- /dev/null +++ b/src/core/textures/Spritesheet.js @@ -0,0 +1,274 @@ +import { Rectangle, Texture } from '../'; +import { getResolutionOfUrl, TextureCache } from '../utils'; + +/** + * Utility class for maintaining reference to a collection + * of Textures on a single Spritesheet. + * + * @class + * @memberof PIXI + */ +export default class Spritesheet +{ + /** + * The maximum number of Textures to build per process. + * + * @type {number} + * @default 1000 + */ + static get BATCH_SIZE() + { + return 1000; + } + + /** + * @param {PIXI.BaseTexture} baseTexture Reference to the source BaseTexture object. + * @param {Object} data - Spritesheet image data. + * @param {string} [resolutionFilename] - The filename to consider when determining + * the resolution of the spritesheet. If not provided, the imageUrl will + * be used on the BaseTexture. + */ + constructor(baseTexture, data, resolutionFilename = null) + { + /** + * Reference to ths source texture + * @type {PIXI.BaseTexture} + */ + this.baseTexture = baseTexture; + + /** + * Map of spritesheet textures. + * @type {Object} + */ + this.textures = {}; + + /** + * Reference to the original JSON data. + * @type {Object} + */ + this.data = data; + + /** + * The resolution of the spritesheet. + * @type {number} + */ + this.resolution = this._updateResolution( + resolutionFilename || this.baseTexture.imageUrl + ); + + /** + * Map of spritesheet frames. + * @type {Object} + * @private + */ + this._frames = this.data.frames; + + /** + * Collection of frame names. + * @type {string[]} + * @private + */ + this._frameKeys = Object.keys(this._frames); + + /** + * Current batch index being processed. + * @type {number} + * @private + */ + this._batchIndex = 0; + + /** + * Callback when parse is completed. + * @type {Function} + * @private + */ + this._callback = null; + } + + /** + * Generate the resolution from the filename or fallback + * to the meta.scale field of the JSON data. + * + * @private + * @param {string} resolutionFilename - The filename to use for resolving + * the default resolution. + * @return {number} Resolution to use for spritesheet. + */ + _updateResolution(resolutionFilename) + { + const scale = this.data.meta.scale; + + // Use a defaultValue of `null` to check if a url-based resolution is set + let resolution = getResolutionOfUrl(resolutionFilename, null); + + // No resolution found via URL + if (resolution === null) + { + // Use the scale value or default to 1 + resolution = scale !== undefined ? parseFloat(scale) : 1; + } + + // For non-1 resolutions, update baseTexture + if (resolution !== 1) + { + this.baseTexture.resolution = resolution; + this.baseTexture.update(); + } + + return resolution; + } + + /** + * Parser spritesheet from loaded data. This is done asynchronously + * to prevent creating too many Texture within a single process. + * + * @param {Function} callback - Callback when complete returns + * a map of the Textures for this spritesheet. + */ + parse(callback) + { + this._batchIndex = 0; + this._callback = callback; + + if (this._frameKeys.length <= Spritesheet.BATCH_SIZE) + { + this._processFrames(0); + this._parseComplete(); + } + else + { + this._nextBatch(); + } + } + + /** + * Process a batch of frames + * + * @private + * @param {number} initialFrameIndex - The index of frame to start. + */ + _processFrames(initialFrameIndex) + { + let frameIndex = initialFrameIndex; + const maxFrames = Spritesheet.BATCH_SIZE; + + while (frameIndex - initialFrameIndex < maxFrames && frameIndex < this._frameKeys.length) + { + const i = this._frameKeys[frameIndex]; + const rect = this._frames[i].frame; + + if (rect) + { + let frame = null; + let trim = null; + const orig = new Rectangle( + 0, + 0, + this._frames[i].sourceSize.w / this.resolution, + this._frames[i].sourceSize.h / this.resolution + ); + + if (this._frames[i].rotated) + { + frame = new Rectangle( + rect.x / this.resolution, + rect.y / this.resolution, + rect.h / this.resolution, + rect.w / this.resolution + ); + } + else + { + frame = new Rectangle( + rect.x / this.resolution, + rect.y / this.resolution, + rect.w / this.resolution, + rect.h / this.resolution + ); + } + + // Check to see if the sprite is trimmed + if (this._frames[i].trimmed) + { + trim = new Rectangle( + this._frames[i].spriteSourceSize.x / this.resolution, + this._frames[i].spriteSourceSize.y / this.resolution, + rect.w / this.resolution, + rect.h / this.resolution + ); + } + + this.textures[i] = new Texture( + this.baseTexture, + frame, + orig, + trim, + this._frames[i].rotated ? 2 : 0 + ); + + // lets also add the frame to pixi's global cache for fromFrame and fromImage functions + TextureCache[i] = this.textures[i]; + } + + frameIndex++; + } + } + + /** + * The parse has completed. + * + * @private + */ + _parseComplete() + { + const callback = this._callback; + + this._callback = null; + this._batchIndex = 0; + callback.call(this, this.textures); + } + + /** + * Begin the next batch of textures. + * + * @private + */ + _nextBatch() + { + this._processFrames(this._batchIndex * Spritesheet.BATCH_SIZE); + this._batchIndex++; + setTimeout(() => + { + if (this._batchIndex * Spritesheet.BATCH_SIZE < this._frameKeys.length) + { + this._nextBatch(); + } + else + { + this._parseComplete(); + } + }, 0); + } + + /** + * Destroy Spritesheet and don't use after this. + * + * @param {boolean} [destroyBase=false] Whether to destroy the base texture as well + */ + destroy(destroyBase = false) + { + for (const i in this.textures) + { + this.textures[i].destroy(); + } + this._frames = null; + this._frameKeys = null; + this.data = null; + this.textures = null; + if (destroyBase) + { + this.baseTexture.destroy(); + } + this.baseTexture = null; + } +} diff --git a/src/core/textures/Texture.js b/src/core/textures/Texture.js index 6403d4f..afe4254 100644 --- a/src/core/textures/Texture.js +++ b/src/core/textures/Texture.js @@ -3,7 +3,7 @@ import TextureUvs from './TextureUvs'; import EventEmitter from 'eventemitter3'; import { Rectangle } from '../math'; -import { TextureCache, BaseTextureCache } from '../utils'; +import { TextureCache, BaseTextureCache, getResolutionOfUrl } from '../utils'; /** * A texture stores the information that represents an image or part of an image. It cannot be added @@ -365,7 +365,8 @@ * The source can be - frame id, image url, video url, canvas element, video element, base texture * * @static - * @param {number|string|PIXI.BaseTexture|HTMLCanvasElement|HTMLVideoElement} source - Source to create texture from + * @param {number|string|HTMLImageElement|HTMLCanvasElement|HTMLVideoElement|PIXI.BaseTexture} + * source - Source to create texture from * @return {PIXI.Texture} The newly created texture */ static from(source) @@ -393,7 +394,7 @@ } else if (source instanceof HTMLImageElement) { - return new Texture(new BaseTexture(source)); + return new Texture(BaseTexture.from(source)); } else if (source instanceof HTMLCanvasElement) { @@ -413,6 +414,43 @@ } /** + * Create a texture from a source and add to the cache. + * + * @static + * @param {HTMLImageElement|HTMLCanvasElement} source - The input source. + * @param {String} imageUrl - File name of texture, for cache and resolving resolution. + * @param {String} [name] - Human readible name for the texture cache. If no name is + * specified, only `imageUrl` will be used as the cache ID. + * @return {PIXI.Texture} Output texture + */ + static fromLoader(source, imageUrl, name) + { + const baseTexture = new BaseTexture(source, null, getResolutionOfUrl(imageUrl)); + const texture = new Texture(baseTexture); + + baseTexture.imageUrl = imageUrl; + + // No name, use imageUrl instead + if (!name) + { + name = imageUrl; + } + + // lets also add the frame to pixi's global cache for fromFrame and fromImage fucntions + BaseTextureCache[name] = baseTexture; + TextureCache[name] = texture; + + // also add references by url if they are different. + if (name !== imageUrl) + { + BaseTextureCache[imageUrl] = baseTexture; + TextureCache[imageUrl] = texture; + } + + return texture; + } + + /** * Adds a texture to the global TextureCache. This cache is shared across the whole PIXI object. * * @static @@ -522,6 +560,29 @@ } } +function createWhiteTexture() +{ + const canvas = document.createElement('canvas'); + + canvas.width = 10; + canvas.height = 10; + + const context = canvas.getContext('2d'); + + context.fillStyle = 'white'; + context.fillRect(0, 0, 10, 10); + + return new Texture(new BaseTexture(canvas)); +} + +function removeAllHandlers(tex) +{ + tex.destroy = function _emptyDestroy() { /* empty */ }; + tex.on = function _emptyOn() { /* empty */ }; + tex.once = function _emptyOnce() { /* empty */ }; + tex.emit = function _emptyEmit() { /* empty */ }; +} + /** * An empty texture, used often to not have to create multiple empty textures. * Can not be destroyed. @@ -530,7 +591,14 @@ * @constant */ Texture.EMPTY = new Texture(new BaseTexture()); -Texture.EMPTY.destroy = function _emptyDestroy() { /* empty */ }; -Texture.EMPTY.on = function _emptyOn() { /* empty */ }; -Texture.EMPTY.once = function _emptyOnce() { /* empty */ }; -Texture.EMPTY.emit = function _emptyEmit() { /* empty */ }; +removeAllHandlers(Texture.EMPTY); + +/** + * A white texture of 10x10 size, used for graphics and other things + * Can not be destroyed. + * + * @static + * @constant + */ +Texture.WHITE = createWhiteTexture(); +removeAllHandlers(Texture.WHITE); diff --git a/src/core/ticker/Ticker.js b/src/core/ticker/Ticker.js index d98a1f0..17a1517 100644 --- a/src/core/ticker/Ticker.js +++ b/src/core/ticker/Ticker.js @@ -67,11 +67,12 @@ * is based, this value is neither capped nor scaled. * If the platform supports DOMHighResTimeStamp, * this value will have a precision of 1 µs. + * Defaults to target frame time * * @member {number} - * @default 1 / TARGET_FPMS + * @default 16.66 */ - this.elapsedMS = 1 / settings.TARGET_FPMS; // default to target frame time + this.elapsedMS = 1 / settings.TARGET_FPMS; /** * The last time {@link PIXI.ticker.Ticker#update} was invoked. diff --git a/src/core/utils/index.js b/src/core/utils/index.js index 6ff609f..be44750 100644 --- a/src/core/utils/index.js +++ b/src/core/utils/index.js @@ -2,6 +2,7 @@ import settings from '../settings'; import EventEmitter from 'eventemitter3'; import pluginTarget from './pluginTarget'; +import * as mixins from './mixin'; import * as isMobile from 'ismobilejs'; let nextUid = 0; @@ -33,6 +34,7 @@ * @type {mixin} */ pluginTarget, + mixins, }; /** @@ -93,7 +95,7 @@ */ export function rgb2hex(rgb) { - return (((rgb[0] * 255) << 16) + ((rgb[1] * 255) << 8) + (rgb[2] * 255)); + return (((rgb[0] * 255) << 16) + ((rgb[1] * 255) << 8) + (rgb[2] * 255 | 0)); } /** @@ -368,3 +370,43 @@ * @private */ export const ProgramCache = {}; + +/** + * Destroys all texture in the cache + * + * @memberof PIXI.utils + * @function destroyTextureCache + */ +export function destroyTextureCache() +{ + let key; + + for (key in TextureCache) + { + TextureCache[key].destroy(); + } + for (key in BaseTextureCache) + { + BaseTextureCache[key].destroy(); + } +} + +/** + * Removes all textures from cache, but does not destroy them + * + * @memberof PIXI.utils + * @function clearTextureCache + */ +export function clearTextureCache() +{ + let key; + + for (key in TextureCache) + { + delete TextureCache[key]; + } + for (key in BaseTextureCache) + { + delete BaseTextureCache[key]; + } +} diff --git a/src/core/utils/mixin.js b/src/core/utils/mixin.js new file mode 100644 index 0000000..114de2a --- /dev/null +++ b/src/core/utils/mixin.js @@ -0,0 +1,59 @@ +/** + * Mixes all enumerable properties and methods from a source object to a target object. + * + * @memberof PIXI.utils.mixins + * @function mixin + * @param {object} target The prototype or instance that properties and methods should be added to. + * @param {object} source The source of properties and methods to mix in. + */ +export function mixin(target, source) +{ + if (!target || !source) return; + // in ES8/ES2017, this would be really easy: + // Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); + + // get all the enumerable property keys + const keys = Object.keys(source); + + // loop through properties + for (let i = 0; i < keys.length; ++i) + { + const propertyName = keys[i]; + + // Set the property using the property descriptor - this works for accessors and normal value properties + Object.defineProperty(target, propertyName, Object.getOwnPropertyDescriptor(source, propertyName)); + } +} + +const mixins = []; + +/** + * Queues a mixin to be handled towards the end of the initialization of PIXI, so that deprecation + * can take effect. + * + * @memberof PIXI.utils.mixins + * @function delayMixin + * @private + * @param {object} target The prototype or instance that properties and methods should be added to. + * @param {object} source The source of properties and methods to mix in. + */ +export function delayMixin(target, source) +{ + mixins.push(target, source); +} + +/** + * Handles all mixins queued via delayMixin(). + * + * @memberof PIXI.utils.mixins + * @function performMixins + * @private + */ +export function performMixins() +{ + for (let i = 0; i < mixins.length; i += 2) + { + mixin(mixins[i], mixins[i + 1]); + } + mixins.length = 0; +} diff --git a/src/deprecation.js b/src/deprecation.js index 045a19f..b9c73b3 100644 --- a/src/deprecation.js +++ b/src/deprecation.js @@ -5,6 +5,7 @@ import * as filters from './filters'; import * as prepare from './prepare'; import * as loaders from './loaders'; +import * as interaction from './interaction'; // provide method to give a stack track for warnings // useful for tracking-down where deprecated methods/properties/classes @@ -534,7 +535,7 @@ { parent: 'GC_MODES', target: 'GC_MODE' }, { parent: 'WRAP_MODES', target: 'WRAP_MODE' }, { parent: 'SCALE_MODES', target: 'SCALE_MODE' }, - { parent: 'PRECISION', target: 'PRECISION' }, + { parent: 'PRECISION', target: 'PRECISION_FRAGMENT' }, ]; for (let i = 0; i < defaults.length; i++) @@ -558,6 +559,32 @@ }); } +Object.defineProperties(core.settings, { + + /** + * @static + * @name PRECISION + * @memberof PIXI.settings + * @see PIXI.PRECISION + * @deprecated since version 4.4.0 + */ + PRECISION: { + enumerable: true, + get() + { + warn('PIXI.settings.PRECISION has been deprecated, please use PIXI.settings.PRECISION_FRAGMENT'); + + return core.settings.PRECISION_FRAGMENT; + }, + set(value) + { + warn('PIXI.settings.PRECISION has been deprecated, please use PIXI.settings.PRECISION_FRAGMENT'); + + core.settings.PRECISION_FRAGMENT = value; + }, + }, +}); + Object.defineProperties(extras, { /** @@ -694,7 +721,7 @@ warn('determineFontProperties is now deprecated, please use the static calculateFontProperties method, ' + 'e.g : Text.calculateFontProperties(fontStyle);'); - return Text.calculateFontProperties(fontStyle); + return core.Text.calculateFontProperties(fontStyle); }; Object.defineProperties(core.TextStyle.prototype, { @@ -1003,3 +1030,69 @@ }, }, }); + +/** + * @name PIXI.interaction.interactiveTarget#defaultCursor + * @static + * @type {number} + * @see PIXI.interaction.interactiveTarget#cursor + * @deprecated since 4.3.0 + */ +Object.defineProperty(interaction.interactiveTarget, 'defaultCursor', { + set(value) + { + warn('Property defaultCursor has been replaced with \'cursor\'. '); + this.cursor = value; + }, + get() + { + warn('Property defaultCursor has been replaced with \'cursor\'. '); + + return this.cursor; + }, + enumerable: true, +}); + +/** + * @name PIXI.interaction.InteractionManager#defaultCursorStyle + * @static + * @type {string} + * @see PIXI.interaction.InteractionManager#cursorStyles + * @deprecated since 4.3.0 + */ +Object.defineProperty(interaction.InteractionManager, 'defaultCursorStyle', { + set(value) + { + warn('Property defaultCursorStyle has been replaced with \'cursorStyles.default\'. '); + this.cursorStyles.default = value; + }, + get() + { + warn('Property defaultCursorStyle has been replaced with \'cursorStyles.default\'. '); + + return this.cursorStyles.default; + }, +}); + +/** + * @name PIXI.interaction.InteractionManager#currentCursorStyle + * @static + * @type {string} + * @see PIXI.interaction.InteractionManager#cursorStyles + * @deprecated since 4.3.0 + */ +Object.defineProperty(interaction.InteractionManager, 'currentCursorStyle', { + set(value) + { + warn('Property currentCursorStyle has been removed.' + + 'See the currentCursorMode property, which works differently.'); + this.currentCursorMode = value; + }, + get() + { + warn('Property currentCursorStyle has been removed.' + + 'See the currentCursorMode property, which works differently.'); + + return this.currentCursorMode; + }, +}); diff --git a/src/extras/BitmapText.js b/src/extras/BitmapText.js index fb6c343..0a0e150 100644 --- a/src/extras/BitmapText.js +++ b/src/extras/BitmapText.js @@ -455,6 +455,73 @@ return this._textHeight; } + + /** + * Register a bitmap font with data and a texture. + * + * @static + * @param {XMLDocument} xml - The XML document data. + * @param {PIXI.Texture} texture - Texture with all symbols. + * @return {Object} Result font object with font, size, lineHeight and char fields. + */ + static registerFont(xml, texture) + { + const data = {}; + const info = xml.getElementsByTagName('info')[0]; + const common = xml.getElementsByTagName('common')[0]; + + data.font = info.getAttribute('face'); + data.size = parseInt(info.getAttribute('size'), 10); + data.lineHeight = parseInt(common.getAttribute('lineHeight'), 10); + data.chars = {}; + + // parse letters + const letters = xml.getElementsByTagName('char'); + + for (let i = 0; i < letters.length; i++) + { + const letter = letters[i]; + const charCode = parseInt(letter.getAttribute('id'), 10); + + const textureRect = new core.Rectangle( + parseInt(letter.getAttribute('x'), 10) + texture.frame.x, + parseInt(letter.getAttribute('y'), 10) + texture.frame.y, + parseInt(letter.getAttribute('width'), 10), + parseInt(letter.getAttribute('height'), 10) + ); + + data.chars[charCode] = { + xOffset: parseInt(letter.getAttribute('xoffset'), 10), + yOffset: parseInt(letter.getAttribute('yoffset'), 10), + xAdvance: parseInt(letter.getAttribute('xadvance'), 10), + kerning: {}, + texture: new core.Texture(texture.baseTexture, textureRect), + + }; + } + + // parse kernings + const kernings = xml.getElementsByTagName('kerning'); + + for (let i = 0; i < kernings.length; i++) + { + const kerning = kernings[i]; + const first = parseInt(kerning.getAttribute('first'), 10); + const second = parseInt(kerning.getAttribute('second'), 10); + const amount = parseInt(kerning.getAttribute('amount'), 10); + + if (data.chars[second]) + { + data.chars[second].kerning[first] = amount; + } + } + + // I'm leaving this as a temporary fix so we can test the bitmap fonts in v3 + // but it's very likely to change + BitmapText.fonts[data.font] = data; + + return data; + } } BitmapText.fonts = {}; diff --git a/src/index.js b/src/index.js index 59356cf..c553427 100644 --- a/src/index.js +++ b/src/index.js @@ -16,6 +16,10 @@ import * as particles from './particles'; import * as prepare from './prepare'; +// handle mixins now, after all code has been added, including deprecation +import { utils } from './core'; +utils.mixins.performMixins(); + export { accessibility, extract, diff --git a/src/interaction/InteractionManager.js b/src/interaction/InteractionManager.js index 7d51134..8baf13a 100644 --- a/src/interaction/InteractionManager.js +++ b/src/interaction/InteractionManager.js @@ -1,18 +1,20 @@ 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'; -// Mix interactiveTarget into core.DisplayObject.prototype -Object.assign( +// Mix interactiveTarget into core.DisplayObject.prototype, after deprecation has been handled +core.utils.mixins.delayMixin( core.DisplayObject.prototype, 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 +70,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 +101,6 @@ this.eventData = new InteractionEvent(); /** - * Tiny little interactiveData pool ! - * - * @member {PIXI.interaction.InteractionData[]} - */ - this.interactiveDataPool = []; - - /** * The DOM element to bind to. * * @private @@ -153,66 +155,12 @@ */ this.supportsPointerEvents = !!window.PointerEvent; - /** - * Are touch events being 'normalized' and converted into pointer events if pointer events are not supported - * For example, on a touch screen mobile device, a touchstart would also be emitted as a pointerdown - * - * @private - * @readonly - * @member {boolean} - */ - this.normalizeTouchEvents = !this.supportsPointerEvents && this.supportsTouchEvents; - - /** - * Are mouse events being 'normalized' and converted into pointer events if pointer events are not supported - * For example, on a desktop pc, a mousedown would also be emitted as a pointerdown - * - * @private - * @readonly - * @member {boolean} - */ - this.normalizeMouseEvents = !this.supportsPointerEvents && !MobileDevice.any; - // this will make it so that you don't have to call bind all the time /** * @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 +168,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,41 +199,32 @@ this.onPointerOver = this.onPointerOver.bind(this); /** - * @private - * @member {Function} + * Dictionary of how different cursor modes are handled. Strings are handled as CSS cursor + * values, objects are handled as dictionaries of CSS values for interactionDOMElement, + * and functions are called instead of changing the CSS. + * Default CSS cursor values are provided for 'default' and 'pointer' modes. + * @member {Object.)>} */ - this.onTouchStart = this.onTouchStart.bind(this); - this.processTouchStart = this.processTouchStart.bind(this); + this.cursorStyles = { + default: 'inherit', + pointer: 'pointer', + }; /** - * @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. - * - * @member {string} - * @default 'inherit' - */ - this.defaultCursorStyle = 'inherit'; - - /** - * The css style of the cursor that is being used. + * The mode of the cursor that is being used. + * The value of this is a key from the cursorStyles dictionary. * * @member {string} */ - this.currentCursorStyle = 'inherit'; + this.currentCursorMode = null; + + /** + * Internal cached let. + * + * @private + * @member {string} + */ + this.cursor = null; /** * Internal cached let. @@ -303,6 +249,7 @@ * object. * * @event mousedown + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -311,6 +258,7 @@ * on the display object. * * @event rightdown + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -319,6 +267,7 @@ * object. * * @event mouseup + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -327,6 +276,7 @@ * over the display object. * * @event rightup + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -335,6 +285,7 @@ * the display object. * * @event click + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -343,6 +294,7 @@ * and released on the display object. * * @event rightclick + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -352,6 +304,7 @@ * [mousedown]{@link PIXI.interaction.InteractionManager#event:mousedown}. * * @event mouseupoutside + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -361,6 +314,7 @@ * [rightdown]{@link PIXI.interaction.InteractionManager#event:rightdown}. * * @event rightupoutside + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -368,6 +322,7 @@ * Fired when a pointer device (usually a mouse) is moved while over the display object * * @event mousemove + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -375,6 +330,7 @@ * Fired when a pointer device (usually a mouse) is moved onto the display object * * @event mouseover + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -382,6 +338,7 @@ * Fired when a pointer device (usually a mouse) is moved off the display object * * @event mouseout + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -389,6 +346,7 @@ * Fired when a pointer device button is pressed on the display object. * * @event pointerdown + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -396,6 +354,14 @@ * Fired when a pointer device button is released over the display object. * * @event pointerup + * @type {PIXI.interaction.InteractionData} + * @memberof PIXI.interaction.InteractionManager# + */ + + /** + * Fired when the operating system cancels a pointer event + * + * @event pointercancel * @memberof PIXI.interaction.InteractionManager# */ @@ -403,6 +369,7 @@ * Fired when a pointer device button is pressed and released on the display object. * * @event pointertap + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -411,6 +378,7 @@ * registered a [pointerdown]{@link PIXI.interaction.InteractionManager#event:pointerdown}. * * @event pointerupoutside + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -418,6 +386,7 @@ * Fired when a pointer device is moved while over the display object * * @event pointermove + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -425,6 +394,7 @@ * Fired when a pointer device is moved onto the display object * * @event pointerover + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -432,6 +402,7 @@ * Fired when a pointer device is moved off the display object * * @event pointerout + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -439,6 +410,7 @@ * Fired when a touch point is placed on the display object. * * @event touchstart + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -446,6 +418,14 @@ * Fired when a touch point is removed from the display object. * * @event touchend + * @type {PIXI.interaction.InteractionData} + * @memberof PIXI.interaction.InteractionManager# + */ + + /** + * Fired when the operating system cancels a touch + * + * @event touchcancel * @memberof PIXI.interaction.InteractionManager# */ @@ -453,6 +433,7 @@ * Fired when a touch point is placed and removed from the display object. * * @event tap + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -461,6 +442,7 @@ * registered a [touchstart]{@link PIXI.interaction.InteractionManager#event:touchstart}. * * @event touchendoutside + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -468,6 +450,7 @@ * Fired when a touch point is moved along the display object. * * @event touchmove + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ } @@ -524,45 +507,30 @@ { window.document.addEventListener('pointermove', this.onPointerMove, true); this.interactionDOMElement.addEventListener('pointerdown', this.onPointerDown, true); - this.interactionDOMElement.addEventListener('pointerout', this.onPointerOut, true); + // pointerout is fired in addition to pointerup (for touch events) and pointercancel + // we already handle those, so for the purposes of what we do in onPointerOut, we only + // care about the pointerleave event + this.interactionDOMElement.addEventListener('pointerleave', 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; @@ -596,45 +564,26 @@ { window.document.removeEventListener('pointermove', this.onPointerMove, true); this.interactionDOMElement.removeEventListener('pointerdown', this.onPointerDown, true); - this.interactionDOMElement.removeEventListener('pointerout', this.onPointerOut, true); + this.interactionDOMElement.removeEventListener('pointerleave', 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) + 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.onPointerDown, true); + this.interactionDOMElement.removeEventListener('touchcancel', this.onPointerCancel, 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); - - 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 = null; @@ -664,7 +613,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; @@ -672,25 +621,80 @@ return; } - this.cursor = this.defaultCursorStyle; + this.cursor = null; // 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(); - - this.processInteractive(this.mouse.global, this.renderer._lastObjectRendered, this.processMouseOverOut, true); - - if (this.currentCursorStyle !== this.cursor) + for (const k in this.activeInteractionData) { - this.currentCursorStyle = this.cursor; - this.interactionDOMElement.style.cursor = this.cursor; + // eslint-disable-next-line no-prototype-builtins + if (this.activeInteractionData.hasOwnProperty(k)) + { + const interactionData = this.activeInteractionData[k]; + + if (interactionData.originalEvent && interactionData.pointerType !== 'touch') + { + const interactionEvent = this.configureInteractionEventForDOMEvent( + this.eventData, + interactionData.originalEvent, + interactionData + ); + + this.processInteractive( + interactionEvent, + this.renderer._lastObjectRendered, + this.processPointerOverOut, + true + ); + } + } } + this.setCursorMode(this.cursor); + // TODO } /** + * Sets the current cursor mode, handling any callbacks or CSS style changes. + * + * @param {string} mode - cursor mode, a key from the cursorStyles dictionary + */ + setCursorMode(mode) + { + mode = mode || 'default'; + // if the mode didn't actually change, bail early + if (this.currentCursorMode === mode) + { + return; + } + this.currentCursorMode = mode; + const style = this.cursorStyles[mode]; + + // only do things if there is a cursor style for it + if (style) + { + switch (typeof style) + { + case 'string': + // string styles are handled as cursor CSS + this.interactionDOMElement.style.cursor = style; + break; + case 'function': + // functions are just called, and passed the cursor mode + style(mode); + break; + case 'object': + // if it is an object, assume that it is a dictionary of CSS styles, + // apply it to the interactionDOMElement + Object.assign(this.interactionDOMElement.style, style); + break; + } + } + } + + /** * Dispatches an event on the display object that was interacted with * * @param {PIXI.Container|PIXI.Sprite|PIXI.extras.TilingSprite} displayObject - the display object in question @@ -748,22 +752,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 @@ -787,9 +795,8 @@ { interactiveParent = false; } - - // it has a mask! Then lets hit test that before continuing.. - if (hitTest && displayObject._mask) + // it has a mask! Then lets hit test that before continuing + else if (hitTest && displayObject._mask) { if (!displayObject._mask.containsPoint(point)) { @@ -797,14 +804,7 @@ } } - // it has a filterArea! Same as mask but easier, its a rectangle - if (hitTest && displayObject.filterArea) - { - if (!displayObject.filterArea.contains(point.x, point.y)) - { - hitTest = false; - } - } + let keepHitTestingAfterChildren = hitTest; // ** FREE TIP **! If an object is not interactive or has no buttons in it // (such as a game scene!) set interactiveChildren to false for that displayObject. @@ -818,7 +818,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 @@ -838,15 +838,20 @@ // This means we no longer need to hit test anything else. We still need to run // through all objects, but we don't need to perform any hit tests. - // { - hitTest = false; - // } + keepHitTestingAfterChildren = false; + + if (child.interactive) + { + hitTest = false; + } // we can break now as we have hit an object. } } } + hitTest = keepHitTestingAfterChildren; + // no point running this if the item is not interactive or does not have an interactive parent. if (interactive) { @@ -868,14 +873,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 +886,175 @@ } /** - * 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].isNormalized) { - 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); + if (!displayObject.trackedPointers[id]) + { + displayObject.trackedPointers[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.trackedPointers[id].rightDown = true; + } + else + { + displayObject.trackedPointers[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.trackedPointers[id] !== undefined) + { + delete displayObject.trackedPointers[id]; + this.dispatchEvent(displayObject, 'pointercancel', interactionEvent); + + if (e.type === 'touchcancel' || e.pointerType === 'touch') + { + this.dispatchEvent(displayObject, 'touchcancel', interactionEvent); + } } } @@ -1159,42 +1066,94 @@ */ 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) { - if (hit) - { - this.dispatchEvent(displayObject, 'pointerup', this.eventData); + const e = interactionEvent.data.originalEvent; - if (displayObject._pointerDown) + const id = interactionEvent.data.identifier; + + const trackingData = displayObject.trackedPointers[id]; + + const isTouch = (e.type === 'touchend' || e.pointerType === 'touch'); + + const isMouse = (e.type.indexOf('mouse') === 0 || e.pointerType === 'mouse'); + + // 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) { - displayObject._pointerDown = false; - this.dispatchEvent(displayObject, 'pointertap', this.eventData); + 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); + } + // update the down state of the tracking data + if (trackingData) + { + if (isRightButton) + { + trackingData.rightDown = hit; + } + else + { + trackingData.leftDown = hit; + } } } - else if (displayObject._pointerDown) + + // Pointers and Touches, and Mouse + if (hit) { - displayObject._pointerDown = false; - this.dispatchEvent(displayObject, 'pointerupoutside', this.eventData); + this.dispatchEvent(displayObject, 'pointerup', interactionEvent); + if (isTouch) this.dispatchEvent(displayObject, 'touchend', interactionEvent); + + if (trackingData) + { + this.dispatchEvent(displayObject, 'pointertap', interactionEvent); + if (isTouch) + { + this.dispatchEvent(displayObject, 'tap', interactionEvent); + // touches are no longer over (if they ever were) when we get the touchend + // so we should ensure that we don't keep pretending that they are + trackingData.over = false; + } + } + } + else if (trackingData) + { + this.dispatchEvent(displayObject, 'pointerupoutside', interactionEvent); + if (isTouch) this.dispatchEvent(displayObject, 'touchendoutside', interactionEvent); + } + // Only remove the tracking data if there is no over/down state still associated with it + if (trackingData && trackingData.none) + { + delete displayObject.trackedPointers[id]; } } @@ -1202,39 +1161,78 @@ * 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 = null; + } - 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') + { + this.setCursorMode(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 (isMouse) { - 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 +1240,94 @@ * 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.setCursorMode(null); + } - 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'); + + let trackingData = displayObject.trackedPointers[id]; + + // if we just moused over the display object, then we need to track that state + if (hit && !trackingData) + { + trackingData = displayObject.trackedPointers[id] = new InteractionTrackingData(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); + } + } + + // only change the cursor if it has not already been changed (by something deeper in the + // display tree) + if (isMouse && this.cursor === null) + { + this.cursor = displayObject.cursor; } } - 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); + } + // if there is no mouse down information for the pointer, then it is safe to delete + if (trackingData.none) + { + delete displayObject.trackedPointers[id]; + } } } @@ -1287,253 +1335,154 @@ * 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 && pointerEvent.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; + + // mark the touch as normalized, just so that we know we did it + touch.isNormalized = true; + + normalizedEvents.push(touch); + } } - else if (this.normalizeMouseEvents) + // apparently PointerEvent subclasses MouseEvent, so yay + else if (event instanceof MouseEvent && (!this.supportsPointerEvents || !(event instanceof window.PointerEvent))) { if (typeof event.isPrimary === 'undefined') event.isPrimary = true; if (typeof event.width === 'undefined') event.width = 1; @@ -1541,10 +1490,21 @@ 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; + + // mark the mouse event as normalized, just so that we know we did it + event.isNormalized = true; + + normalizedEvents.push(event); } + else + { + normalizedEvents.push(event); + } + + return normalizedEvents; } /** @@ -1563,30 +1523,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 +1542,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..6a1f69e --- /dev/null +++ b/src/interaction/InteractionTrackingData.js @@ -0,0 +1,147 @@ +/** + * 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 inactive (not over or down)? + * + * @member {number} + * @memberof PIXI.interaction.InteractionTrackingData# + */ + get none() + { + return this._flags === this.constructor.FLAGS.NONE; + } + + /** + * 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..d252fba 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 * ); */ @@ -38,68 +38,56 @@ hitArea: null, /** - * If enabled, the mouse cursor will change when hovered over the displayObject if it is interactive + * If enabled, the mouse cursor use the pointer behavior when hovered over the displayObject if it is interactive + * Setting this changes the 'cursor' property to `'pointer'`. * - * @inner {boolean} + * @member {boolean} + * @memberof PIXI.interaction.interactiveTarget# */ - buttonMode: false, + get buttonMode() + { + return this.cursor === 'pointer'; + }, + set buttonMode(value) + { + if (value) + { + this.cursor = 'pointer'; + } + else if (this.cursor === 'pointer') + { + this.cursor = null; + } + }, /** - * If buttonMode is enabled, this defines what CSS cursor property is used when the mouse cursor - * is hovered over the displayObject + * This defines what cursor mode is used when the mouse cursor + * is hovered over the displayObject. * * @see https://developer.mozilla.org/en/docs/Web/CSS/cursor * * @inner {string} */ - defaultCursor: 'pointer', - - // some internal checks.. - /** - * Internal check to detect if the mouse cursor is hovered over the displayObject - * - * @inner {boolean} - * @private - */ - _over: false, + cursor: null, /** - * Internal check to detect if the left mouse button is pressed on the displayObject + * Internal set of all active pointers, by identifier * - * @inner {boolean} + * @member {Map} + * @memberof PIXI.interaction.interactiveTarget# * @private */ - _isLeftDown: false, + get trackedPointers() + { + if (this._trackedPointers === undefined) this._trackedPointers = {}; + + return this._trackedPointers; + }, /** - * Internal check to detect if the right mouse button is pressed on the displayObject + * Map of all tracked pointers, by identifier. Use trackedPointers to access. * - * @inner {boolean} - * @private + * @private {Map} */ - _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, + _trackedPointers: undefined, }; diff --git a/src/loaders/bitmapFontParser.js b/src/loaders/bitmapFontParser.js index cc2618c..f4cf996 100644 --- a/src/loaders/bitmapFontParser.js +++ b/src/loaders/bitmapFontParser.js @@ -1,63 +1,19 @@ import * as path from 'path'; -import { Rectangle, Texture, utils } from '../core'; +import { utils } from '../core'; import { Resource } from 'resource-loader'; import { BitmapText } from '../extras'; +/** + * Register a BitmapText font from loader resource. + * + * @function parseBitmapFontData + * @memberof PIXI.loaders + * @param {PIXI.loaders.Resource} resource - Loader resource. + * @param {PIXI.Texture} texture - Reference to texture. + */ export function parse(resource, texture) { - const data = {}; - const info = resource.data.getElementsByTagName('info')[0]; - const common = resource.data.getElementsByTagName('common')[0]; - - data.font = info.getAttribute('face'); - data.size = parseInt(info.getAttribute('size'), 10); - data.lineHeight = parseInt(common.getAttribute('lineHeight'), 10); - data.chars = {}; - - // parse letters - const letters = resource.data.getElementsByTagName('char'); - - for (let i = 0; i < letters.length; i++) - { - const charCode = parseInt(letters[i].getAttribute('id'), 10); - - const textureRect = new Rectangle( - parseInt(letters[i].getAttribute('x'), 10) + texture.frame.x, - parseInt(letters[i].getAttribute('y'), 10) + texture.frame.y, - parseInt(letters[i].getAttribute('width'), 10), - parseInt(letters[i].getAttribute('height'), 10) - ); - - data.chars[charCode] = { - xOffset: parseInt(letters[i].getAttribute('xoffset'), 10), - yOffset: parseInt(letters[i].getAttribute('yoffset'), 10), - xAdvance: parseInt(letters[i].getAttribute('xadvance'), 10), - kerning: {}, - texture: new Texture(texture.baseTexture, textureRect), - - }; - } - - // parse kernings - const kernings = resource.data.getElementsByTagName('kerning'); - - for (let i = 0; i < kernings.length; i++) - { - const first = parseInt(kernings[i].getAttribute('first'), 10); - const second = parseInt(kernings[i].getAttribute('second'), 10); - const amount = parseInt(kernings[i].getAttribute('amount'), 10); - - if (data.chars[second]) - { - data.chars[second].kerning[first] = amount; - } - } - - resource.bitmapFont = data; - - // I'm leaving this as a temporary fix so we can test the bitmap fonts in v3 - // but it's very likely to change - BitmapText.fonts[data.font] = data; + resource.bitmapFont = BitmapText.registerFont(resource.data, texture); } export default function () diff --git a/src/loaders/index.js b/src/loaders/index.js index 8942436..5189793 100644 --- a/src/loaders/index.js +++ b/src/loaders/index.js @@ -5,4 +5,11 @@ export { default as bitmapFontParser, parse as parseBitmapFontData } from './bitmapFontParser'; export { default as spritesheetParser } from './spritesheetParser'; export { default as textureParser } from './textureParser'; + +/** + * Reference to **resource-loader**'s Resource class. + * See https://github.com/englercj/resource-loader + * @class Resource + * @memberof PIXI.loaders + */ export { Resource } from 'resource-loader'; diff --git a/src/loaders/loader.js b/src/loaders/loader.js index 0a670cb..767a411 100644 --- a/src/loaders/loader.js +++ b/src/loaders/loader.js @@ -7,20 +7,47 @@ /** * - * The new loader, extends Resource Loader by Chad Engler : https://github.com/englercj/resource-loader + * The new loader, extends Resource Loader by Chad Engler: https://github.com/englercj/resource-loader * * ```js - * let loader = PIXI.loader; // pixi exposes a premade instance for you to use. + * const loader = PIXI.loader; // pixi exposes a premade instance for you to use. * //or - * let loader = new PIXI.loaders.Loader(); // you can also create your own if you want + * const loader = new PIXI.loaders.Loader(); // you can also create your own if you want * - * loader.add('bunny', 'data/bunny.png'); - * loader.add('spaceship', 'assets/spritesheet.json'); + * const sprites = {}; + * + * // Chainable `add` to enqueue a resource + * loader.add('bunny', 'data/bunny.png') + * .add('spaceship', 'assets/spritesheet.json'); * loader.add('scoreFont', 'assets/score.fnt'); * - * loader.once('complete',onAssetsLoaded); + * // Chainable `pre` to add a middleware that runs for each resource, *before* loading that resource. + * // This is useful to implement custom caching modules (using filesystem, indexeddb, memory, etc). + * loader.pre(cachingMiddleware); * - * loader.load(); + * // Chainable `use` to add a middleware that runs for each resource, *after* loading that resource. + * // This is useful to implement custom parsing modules (like spritesheet parsers, spine parser, etc). + * loader.use(parsingMiddleware); + * + * // The `load` method loads the queue of resources, and calls the passed in callback called once all + * // resources have loaded. + * loader.load((loader, resources) => { + * // resources is an object where the key is the name of the resource loaded and the value is the resource object. + * // They have a couple default properties: + * // - `url`: The URL that the resource was loaded from + * // - `error`: The error that happened when trying to load (if any) + * // - `data`: The raw data that was loaded + * // also may contain other properties based on the middleware that runs. + * sprites.bunny = new PIXI.TilingSprite(resources.bunny.texture); + * sprites.spaceship = new PIXI.TilingSprite(resources.spaceship.texture); + * sprites.scoreFont = new PIXI.TilingSprite(resources.scoreFont.texture); + * }); + * + * // throughout the process multiple signals can be dispatched. + * loader.onProgress.add(() => {}); // called once per loaded/errored file + * loader.onError.add(() => {}); // called once per errored file + * loader.onLoad.add(() => {}); // called once per loaded file + * loader.onComplete.add(() => {}); // called once when the queued resources all load. * ``` * * @see https://github.com/englercj/resource-loader diff --git a/src/loaders/spritesheetParser.js b/src/loaders/spritesheetParser.js index 7570332..e57dfb2 100644 --- a/src/loaders/spritesheetParser.js +++ b/src/loaders/spritesheetParser.js @@ -1,8 +1,6 @@ import { Resource } from 'resource-loader'; import path from 'path'; -import * as core from '../core'; - -const BATCH_SIZE = 1000; +import { Spritesheet } from '../core'; export default function () { @@ -43,134 +41,18 @@ // load the image for this sheet this.add(imageResourceName, resourcePath, loadOptions, function onImageLoad(res) { - resource.textures = {}; + const spritesheet = new Spritesheet( + res.texture.baseTexture, + resource.data, + resource.url + ); - const frames = resource.data.frames; - const frameKeys = Object.keys(frames); - const baseTexture = res.texture.baseTexture; - const scale = resource.data.meta.scale; - - // Use a defaultValue of `null` to check if a url-based resolution is set - let resolution = core.utils.getResolutionOfUrl(resource.url, null); - - // No resolution found via URL - if (resolution === null) + spritesheet.parse(() => { - // Use the scale value or default to 1 - resolution = scale !== undefined ? scale : 1; - } - - // For non-1 resolutions, update baseTexture - if (resolution !== 1) - { - baseTexture.resolution = resolution; - baseTexture.update(); - } - - let batchIndex = 0; - - function processFrames(initialFrameIndex, maxFrames) - { - let frameIndex = initialFrameIndex; - - while (frameIndex - initialFrameIndex < maxFrames && frameIndex < frameKeys.length) - { - const i = frameKeys[frameIndex]; - const rect = frames[i].frame; - - if (rect) - { - let frame = null; - let trim = null; - const orig = new core.Rectangle( - 0, - 0, - frames[i].sourceSize.w / resolution, - frames[i].sourceSize.h / resolution - ); - - if (frames[i].rotated) - { - frame = new core.Rectangle( - rect.x / resolution, - rect.y / resolution, - rect.h / resolution, - rect.w / resolution - ); - } - else - { - frame = new core.Rectangle( - rect.x / resolution, - rect.y / resolution, - rect.w / resolution, - rect.h / resolution - ); - } - - // Check to see if the sprite is trimmed - if (frames[i].trimmed) - { - trim = new core.Rectangle( - frames[i].spriteSourceSize.x / resolution, - frames[i].spriteSourceSize.y / resolution, - rect.w / resolution, - rect.h / resolution - ); - } - - resource.textures[i] = new core.Texture( - baseTexture, - frame, - orig, - trim, - frames[i].rotated ? 2 : 0 - ); - - // lets also add the frame to pixi's global cache for fromFrame and fromImage functions - core.utils.TextureCache[i] = resource.textures[i]; - } - - frameIndex++; - } - } - - function shouldProcessNextBatch() - { - return batchIndex * BATCH_SIZE < frameKeys.length; - } - - function processNextBatch(done) - { - processFrames(batchIndex * BATCH_SIZE, BATCH_SIZE); - batchIndex++; - setTimeout(done, 0); - } - - function iteration() - { - processNextBatch(() => - { - if (shouldProcessNextBatch()) - { - iteration(); - } - else - { - next(); - } - }); - } - - if (frameKeys.length <= BATCH_SIZE) - { - processFrames(0, BATCH_SIZE); + resource.spritesheet = spritesheet; + resource.textures = spritesheet.textures; next(); - } - else - { - iteration(); - } + }); }); }; } diff --git a/src/loaders/textureParser.js b/src/loaders/textureParser.js index 5398a7f..480edc1 100644 --- a/src/loaders/textureParser.js +++ b/src/loaders/textureParser.js @@ -1,5 +1,5 @@ -import * as core from '../core'; import { Resource } from 'resource-loader'; +import Texture from '../core/textures/Texture'; export default function () { @@ -8,23 +8,12 @@ // create a new texture if the data is an Image object if (resource.data && resource.type === Resource.TYPE.IMAGE) { - const baseTexture = new core.BaseTexture(resource.data, null, core.utils.getResolutionOfUrl(resource.url)); - - baseTexture.imageUrl = resource.url; - resource.texture = new core.Texture(baseTexture); - - // lets also add the frame to pixi's global cache for fromFrame and fromImage fucntions - core.utils.BaseTextureCache[resource.name] = baseTexture; - core.utils.TextureCache[resource.name] = resource.texture; - - // also add references by url if they are different. - if (resource.name !== resource.url) - { - core.utils.BaseTextureCache[resource.url] = baseTexture; - core.utils.TextureCache[resource.url] = resource.texture; - } + resource.texture = Texture.fromLoader( + resource.data, + resource.url, + resource.name + ); } - next(); }; } diff --git a/test/core/Container.js b/test/core/Container.js index 8a10b40..d11be33 100644 --- a/test/core/Container.js +++ b/test/core/Container.js @@ -1,5 +1,33 @@ 'use strict'; +function testAddChild(fn) +{ + fn(function (container, obj) + { + container.addChild(obj); + }); + fn(function (container, obj) + { + container.addChildAt(obj); + }); +} + +function testRemoveChild(fn) +{ + fn(function (container, obj) + { + container.removeChild(obj); + }); + fn(function (container, obj) + { + container.removeChildAt(container.children.indexOf(obj)); + }); + fn(function (container, obj) + { + container.removeChildren(container.children.indexOf(obj), container.children.indexOf(obj) + 1); + }); +} + describe('PIXI.Container', function () { describe('parent', function () @@ -70,6 +98,49 @@ expect(spy).to.have.been.called; expect(spy).to.have.been.calledWith(0); }); + + it('should flag child transform and container bounds for recalculation', testAddChild(function (mockAddChild) + { + const container = new PIXI.Container(); + const child = new PIXI.Container(); + + container.getBounds(); + child.getBounds(); + + const boundsID = container._boundsID; + const childParentID = child.transform._parentID; + + mockAddChild(container, child); + + expect(boundsID).to.not.be.equals(container._boundsID); + expect(childParentID).to.not.be.equals(child.transform._parentID); + })); + + it('should recalculate added child correctly', testAddChild(function (mockAddChild) + { + const parent = new PIXI.Container(); + const container = new PIXI.Container(); + const graphics = new PIXI.Graphics(); + + parent.addChild(container); + + graphics.drawRect(0, 0, 10, 10); + container.position.set(100, 200); + container.updateTransform(); + + graphics.getBounds(); + // Oops, that can happen sometimes! + graphics.transform._parentID = container.transform._worldID + 1; + + mockAddChild(container, graphics); + + const bounds = graphics.getBounds(); + + expect(bounds.x).to.be.equal(100); + expect(bounds.y).to.be.equal(200); + expect(bounds.width).to.be.equal(10); + expect(bounds.height).to.be.equal(10); + })); }); describe('removeChildAt', function () @@ -202,6 +273,44 @@ expect(spy).to.have.been.called; expect(spy).to.have.been.calledWith(0); }); + + it('should flag transform for recalculation', testRemoveChild(function (mockRemoveChild) + { + const container = new PIXI.Container(); + const child = new PIXI.Container(); + + container.addChild(child); + container.getBounds(); + + const childParentID = child.transform._parentID; + const boundsID = container._boundsID; + + mockRemoveChild(container, child); + + expect(childParentID).to.not.be.equals(child.transform._parentID); + expect(boundsID).to.not.be.equals(container._boundsID); + })); + + it('should recalculate removed child correctly', testRemoveChild(function (mockRemoveChild) + { + const parent = new PIXI.Container(); + const container = new PIXI.Container(); + const graphics = new PIXI.Graphics(); + + parent.addChild(container); + + graphics.drawRect(0, 0, 10, 10); + container.position.set(100, 200); + container.addChild(graphics); + graphics.getBounds(); + + mockRemoveChild(container, graphics); + + const bounds = graphics.getBounds(); + + expect(bounds.x).to.be.equal(0); + expect(bounds.y).to.be.equal(0); + })); }); describe('getChildIndex', function () diff --git a/test/core/Graphics.js b/test/core/Graphics.js index 98c7584..11e47a0 100644 --- a/test/core/Graphics.js +++ b/test/core/Graphics.js @@ -1,5 +1,8 @@ 'use strict'; +const MockPointer = require('../interaction/MockPointer'); +const withGL = require('../withGL'); + describe('PIXI.Graphics', function () { describe('constructor', function () @@ -218,4 +221,153 @@ expect(spy).to.have.been.calledOnce; }); }); + + describe('mask', function () + { + it('should trigger interaction callback when no mask present', function () + { + const stage = new PIXI.Container(); + const pointer = new MockPointer(stage); + const graphics = new PIXI.Graphics(); + const mask = new PIXI.Graphics(); + const spy = sinon.spy(); + + graphics.interactive = true; + graphics.beginFill(0xFF0000); + graphics.drawRect(0, 0, 50, 50); + graphics.on('click', spy); + stage.addChild(graphics); + mask.beginFill(); + mask.drawRect(0, 0, 50, 50); + graphics.mask = mask; + + pointer.click(10, 10); + + expect(spy).to.have.been.calledOnce; + }); + it('should trigger interaction callback when mask uses beginFill', function () + { + const stage = new PIXI.Container(); + const pointer = new MockPointer(stage); + const graphics = new PIXI.Graphics(); + const mask = new PIXI.Graphics(); + const spy = sinon.spy(); + + graphics.interactive = true; + graphics.beginFill(0xFF0000); + graphics.drawRect(0, 0, 50, 50); + graphics.on('click', spy); + stage.addChild(graphics); + mask.beginFill(); + mask.drawRect(0, 0, 50, 50); + graphics.mask = mask; + + pointer.click(10, 10); + + expect(spy).to.have.been.calledOnce; + }); + + it('should not trigger interaction callback when mask doesn\'t use beginFill', function () + { + const stage = new PIXI.Container(); + const pointer = new MockPointer(stage); + const graphics = new PIXI.Graphics(); + const mask = new PIXI.Graphics(); + const spy = sinon.spy(); + + graphics.interactive = true; + graphics.beginFill(0xFF0000); + graphics.drawRect(0, 0, 50, 50); + graphics.on('click', spy); + stage.addChild(graphics); + mask.drawRect(0, 0, 50, 50); + graphics.mask = mask; + + pointer.click(10, 10); + + expect(spy).to.have.not.been.called; + }); + + it('should trigger interaction callback when mask doesn\'t use beginFill but hitArea is defined', function () + { + const stage = new PIXI.Container(); + const pointer = new MockPointer(stage); + const graphics = new PIXI.Graphics(); + const mask = new PIXI.Graphics(); + const spy = sinon.spy(); + + graphics.interactive = true; + graphics.beginFill(0xFF0000); + graphics.hitArea = new PIXI.Rectangle(0, 0, 50, 50); + graphics.drawRect(0, 0, 50, 50); + graphics.on('click', spy); + stage.addChild(graphics); + mask.drawRect(0, 0, 50, 50); + graphics.mask = mask; + + pointer.click(10, 10); + + expect(spy).to.have.been.calledOnce; + }); + + it('should trigger interaction callback when mask is a sprite', function () + { + const stage = new PIXI.Container(); + const pointer = new MockPointer(stage); + const graphics = new PIXI.Graphics(); + const mask = new PIXI.Graphics(); + const spy = sinon.spy(); + + graphics.interactive = true; + graphics.beginFill(0xFF0000); + graphics.drawRect(0, 0, 50, 50); + graphics.on('click', spy); + stage.addChild(graphics); + mask.drawRect(0, 0, 50, 50); + graphics.mask = new PIXI.Sprite(mask.generateCanvasTexture()); + + pointer.click(10, 10); + + expect(spy).to.have.been.calledOnce; + }); + + it('should calculate tint, alpha and blendMode of fastRect correctly', withGL(function () + { + const renderer = new PIXI.WebGLRenderer(200, 200, {}); + + try + { + const graphics = new PIXI.Graphics(); + + graphics.beginFill(0x102030, 0.6); + graphics.drawRect(2, 3, 100, 100); + graphics.endFill(); + graphics.tint = 0x101010; + graphics.blendMode = 2; + graphics.alpha = 0.3; + + renderer.render(graphics); + + expect(graphics.isFastRect()).to.be.true; + + const sprite = graphics._spriteRect; + + expect(sprite).to.not.be.equals(null); + expect(sprite.worldAlpha).to.equals(0.18); + expect(sprite.blendMode).to.equals(2); + expect(sprite.tint).to.equals(0x010203); + + const bounds = sprite.getBounds(); + + expect(bounds.x).to.equals(2); + expect(bounds.y).to.equals(3); + expect(bounds.width).to.equals(100); + expect(bounds.height).to.equals(100); + } + finally + { + renderer.destroy(); + } + })); + }); }); diff --git a/test/core/Spritesheet.js b/test/core/Spritesheet.js new file mode 100644 index 0000000..283adf5 --- /dev/null +++ b/test/core/Spritesheet.js @@ -0,0 +1,89 @@ +'use strict'; + +const path = require('path'); + +describe('PIXI.Spritesheet', function () +{ + before(function () + { + this.resources = path.join(__dirname, 'resources'); + this.validate = function (spritesheet, done) + { + spritesheet.parse(function (textures) + { + const id = 'goldmine_10_5.png'; + + expect(Object.keys(textures).length).to.equal(1); + expect(Object.keys(spritesheet.textures).length).to.equal(1); + expect(textures[id]).to.be.an.instanceof(PIXI.Texture); + expect(textures[id].width).to.equal(spritesheet.data.frames[id].frame.w / spritesheet.resolution); + expect(textures[id].height).to.equal(spritesheet.data.frames[id].frame.h / spritesheet.resolution); + spritesheet.destroy(true); + expect(spritesheet.textures).to.be.null; + expect(spritesheet.baseTexture).to.be.null; + done(); + }); + }; + }); + + it('should exist on PIXI', function () + { + expect(PIXI.Spritesheet).to.be.a.function; + expect(PIXI.Spritesheet.BATCH_SIZE).to.be.a.number; + }); + + it('should create an instance', function () + { + const baseTexture = new PIXI.BaseTexture(); + const data = { + frames: {}, + meta: {}, + }; + const spritesheet = new PIXI.Spritesheet(baseTexture, data); + + expect(spritesheet.data).to.equal(data); + expect(spritesheet.baseTexture).to.equal(baseTexture); + expect(spritesheet.resolution).to.equal(1); + + spritesheet.destroy(true); + }); + + it('should create instance with scale resolution', function (done) + { + const data = require(path.resolve(this.resources, 'building1.json')); // eslint-disable-line global-require + const image = new Image(); + + image.src = path.join(this.resources, data.meta.image); + image.onload = () => + { + const baseTexture = new PIXI.BaseTexture(image, null, 1); + const spritesheet = new PIXI.Spritesheet(baseTexture, data); + + expect(data).to.be.an.object; + expect(data.meta.image).to.equal('building1.png'); + expect(spritesheet.resolution).to.equal(0.5); + + this.validate(spritesheet, done); + }; + }); + + it('should create instance with filename resolution', function (done) + { + const uri = path.resolve(this.resources, 'building1@2x.json'); + const data = require(uri); // eslint-disable-line global-require + const image = new Image(); + + image.src = path.join(this.resources, data.meta.image); + image.onload = () => + { + const baseTexture = new PIXI.BaseTexture(image, null, 1); + const spritesheet = new PIXI.Spritesheet(baseTexture, data, uri); + + expect(data).to.be.an.object; + expect(data.meta.image).to.equal('building1@2x.png'); + expect(spritesheet.resolution).to.equal(2); + + this.validate(spritesheet, done); + }; + }); +}); diff --git a/test/core/Texture.js b/test/core/Texture.js new file mode 100644 index 0000000..8c4ef98 --- /dev/null +++ b/test/core/Texture.js @@ -0,0 +1,19 @@ +'use strict'; + +describe('PIXI.Texture', function () +{ + it('should register Texture from Loader', function () + { + const URL = 'foo.png'; + const NAME = 'bar'; + const image = new Image(); + + const texture = PIXI.Texture.fromLoader(image, URL, NAME); + + expect(texture.baseTexture.imageUrl).to.equal('foo.png'); + expect(PIXI.utils.TextureCache[NAME]).to.equal(texture); + expect(PIXI.utils.BaseTextureCache[NAME]).to.equal(texture.baseTexture); + expect(PIXI.utils.TextureCache[URL]).to.equal(texture); + expect(PIXI.utils.BaseTextureCache[URL]).to.equal(texture.baseTexture); + }); +}); diff --git a/test/core/WebGLRenderer.js b/test/core/WebGLRenderer.js index 9bc3bc2..3d6cf98 100644 --- a/test/core/WebGLRenderer.js +++ b/test/core/WebGLRenderer.js @@ -1,21 +1,21 @@ 'use strict'; -function isWebGLSupported(fn) -{ - return PIXI.utils.isWebGLSupported() ? fn : function () {}; // eslint-disable-line no-empty-function -} +const withGL = require('../withGL'); -describe('PIXI.WebGLRenderer', isWebGLSupported(function () +describe('PIXI.WebGLRenderer', function () { - it('setting option legacy should disable VAOs and set minimum SPRITE_MAX_TEXTURES to 1', function () + it('setting option legacy should disable VAOs and SPRITE_MAX_TEXTURES', withGL(function () { const renderer = new PIXI.WebGLRenderer(1, 1, { legacy: true }); - - expect(PIXI.glCore.VertexArrayObject.FORCE_NATIVE).to.equal(true); - expect(renderer.plugins.sprite.MAX_TEXTURES).to.equal(1); - - renderer.destroy(); - }); -})); - + try + { + expect(PIXI.glCore.VertexArrayObject.FORCE_NATIVE).to.equal(true); + expect(renderer.plugins.sprite.MAX_TEXTURES).to.equal(1); + } + finally + { + renderer.destroy(); + } + })); +}); diff --git a/test/core/filters.js b/test/core/filters.js new file mode 100644 index 0000000..df459f6 --- /dev/null +++ b/test/core/filters.js @@ -0,0 +1,21 @@ +'use strict'; + +describe('PIXI.filters', function () +{ + it('should correctly form uniformData', function () + { + const sprite = new PIXI.Sprite(PIXI.Texture.EMPTY); + const displ = new PIXI.filters.DisplacementFilter(sprite); + + expect(!!displ.uniformData.scale).to.be.true; + expect(!!displ.uniformData.filterMatrix).to.be.true; + expect(!!displ.uniformData.mapSampler).to.be.true; + // it does have filterClamp, but it is handled by FilterManager + expect(!!displ.uniformData.filterClamp).to.be.false; + + const fxaa = new PIXI.filters.FXAAFilter(); + + // it does have filterArea, but it is handled by FilterManager + expect(!!fxaa.uniformData.filterArea).to.be.false; + }); +}); diff --git a/test/core/index.js b/test/core/index.js index 63a8f21..8530131 100755 --- a/test/core/index.js +++ b/test/core/index.js @@ -7,6 +7,7 @@ require('./DisplayObject'); require('./getLocalBounds'); require('./Sprite'); +require('./Spritesheet'); require('./TilingSprite'); require('./TextStyle'); require('./Text'); @@ -25,3 +26,5 @@ require('./SpriteRenderer'); require('./WebGLRenderer'); require('./Ellipse'); +require('./Texture'); +require('./filters'); diff --git a/test/core/resources/building1.json b/test/core/resources/building1.json new file mode 100755 index 0000000..03fa5c1 --- /dev/null +++ b/test/core/resources/building1.json @@ -0,0 +1,21 @@ +{"frames": { + +"goldmine_10_5.png": +{ + "frame": {"x":1,"y":1,"w":95,"h":115}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":95,"h":115}, + "sourceSize": {"w":95,"h":115}, + "pivot": {"x":0.5,"y":0.5} +}}, +"meta": { + "app": "http://www.codeandweb.com/texturepacker", + "version": "1.0", + "image": "building1.png", + "format": "RGBA8888", + "size": {"w":128,"h":128}, + "scale": "0.5", + "smartupdate": "$TexturePacker:SmartUpdate:d7a5e54c8f8a3fecd508baf190c44807:99a0e3a4dc0f441e4aad77614191ab38:6046b8eb706ddefaa771c33ceb7cb6d5$" +} +} diff --git a/test/core/resources/building1.png b/test/core/resources/building1.png new file mode 100755 index 0000000..7e1e114 --- /dev/null +++ b/test/core/resources/building1.png Binary files differ diff --git a/test/core/resources/building1@2x.json b/test/core/resources/building1@2x.json new file mode 100755 index 0000000..24e25ff --- /dev/null +++ b/test/core/resources/building1@2x.json @@ -0,0 +1,21 @@ +{"frames": { + +"goldmine_10_5.png": +{ + "frame": {"x":1,"y":1,"w":190,"h":229}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":190,"h":229}, + "sourceSize": {"w":190,"h":229}, + "pivot": {"x":0.5,"y":0.5} +}}, +"meta": { + "app": "http://www.codeandweb.com/texturepacker", + "version": "1.0", + "image": "building1@2x.png", + "format": "RGBA8888", + "size": {"w":256,"h":256}, + "scale": "1", + "smartupdate": "$TexturePacker:SmartUpdate:d7a5e54c8f8a3fecd508baf190c44807:99a0e3a4dc0f441e4aad77614191ab38:6046b8eb706ddefaa771c33ceb7cb6d5$" +} +} diff --git a/test/core/resources/building1@2x.png b/test/core/resources/building1@2x.png new file mode 100755 index 0000000..d5ecd04 --- /dev/null +++ b/test/core/resources/building1@2x.png Binary files differ diff --git a/package.json b/package.json index 6a366ba..71ade15 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pixi.js", - "version": "4.3.4", + "version": "4.3.5", "description": "Pixi.js is a fast lightweight 2D library that works across all devices.", "author": "Mat Groves", "contributors": [ @@ -61,7 +61,7 @@ "ismobilejs": "^0.4.0", "object-assign": "^4.0.1", "pixi-gl-core": "^1.0.3", - "resource-loader": "^2.0.4" + "resource-loader": "^2.0.6" }, "devDependencies": { "babel-cli": "^6.18.0", @@ -70,9 +70,9 @@ "babel-preset-es2015": "^6.14.0", "babelify": "^7.3.0", "del": "^2.2.0", - "electron-prebuilt": "^1.3.2", + "electron": "^1.4.15", "eslint": "^3.5.0", - "floss": "^1.2.0", + "floss": "^2.0.1", "gh-pages": "^0.11.0", "jaguarjs-jsdoc": "^1.0.1", "js-md5": "^0.4.1", diff --git a/src/accessibility/AccessibilityManager.js b/src/accessibility/AccessibilityManager.js index df2716a..f16bcb1 100644 --- a/src/accessibility/AccessibilityManager.js +++ b/src/accessibility/AccessibilityManager.js @@ -3,7 +3,7 @@ import accessibleTarget from './accessibleTarget'; // add some extra variables to the container.. -Object.assign( +core.utils.mixins.delayMixin( core.DisplayObject.prototype, accessibleTarget ); diff --git a/src/core/Application.js b/src/core/Application.js index ea0f058..5340578 100644 --- a/src/core/Application.js +++ b/src/core/Application.js @@ -1,6 +1,6 @@ import { autoDetectRenderer } from './autoDetectRenderer'; import Container from './display/Container'; -import Ticker from './ticker/Ticker'; +import { shared, Ticker } from './ticker'; /** * Convenience class to create a new PIXI application. @@ -35,9 +35,9 @@ * @param {boolean} [noWebGL=false] - prevents selection of WebGL renderer, even if such is present * @param {boolean} [options.legacy=false] - If true Pixi will aim to ensure compatibility * with older / less advanced devices. If you experience unexplained flickering try setting this to true. - + * @param {boolean} [useSharedTicker=false] - `true` to use PIXI.ticker.shared, `false` to create new ticker. */ - constructor(width, height, options, noWebGL) + constructor(width, height, options, noWebGL, useSharedTicker = false) { /** * WebGL renderer if available, otherwise CanvasRenderer @@ -46,23 +46,46 @@ this.renderer = autoDetectRenderer(width, height, options, noWebGL); /** - * The root display container that's renderered. + * The root display container that's rendered. * @member {PIXI.Container} */ this.stage = new Container(); /** + * Internal reference to the ticker + * @member {PIXI.ticker.Ticker} + * @private + */ + this._ticker = null; + + /** * Ticker for doing render updates. * @member {PIXI.ticker.Ticker} + * @default PIXI.ticker.shared */ - this.ticker = new Ticker(); - - this.ticker.add(this.render, this); + this.ticker = useSharedTicker ? shared : new Ticker(); // Start the rendering this.start(); } + set ticker(ticker) // eslint-disable-line require-jsdoc + { + if (this._ticker) + { + this._ticker.remove(this.render, this); + } + this._ticker = ticker; + if (ticker) + { + ticker.add(this.render, this); + } + } + get ticker() // eslint-disable-line require-jsdoc + { + return this._ticker; + } + /** * Render the current stage. */ @@ -76,7 +99,7 @@ */ stop() { - this.ticker.stop(); + this._ticker.stop(); } /** @@ -84,7 +107,7 @@ */ start() { - this.ticker.start(); + this._ticker.start(); } /** @@ -98,13 +121,22 @@ } /** + * Reference to the renderer's screen rectangle. Its safe to use as filterArea or hitArea for whole screen + * @member {PIXI.Rectangle} + * @readonly + */ + get screen() + { + return this.renderer.screen; + } + + /** * Destroy and don't use after this. * @param {Boolean} [removeView=false] Automatically remove canvas from DOM. */ destroy(removeView) { this.stop(); - this.ticker.remove(this.render, this); this.ticker = null; this.stage.destroy(); diff --git a/src/core/Shader.js b/src/core/Shader.js new file mode 100644 index 0000000..78b17e6 --- /dev/null +++ b/src/core/Shader.js @@ -0,0 +1,46 @@ +import { GLShader } from 'pixi-gl-core'; +import settings from './settings'; + +function checkPrecision(src, def) +{ + if (src instanceof Array) + { + if (src[0].substring(0, 9) !== 'precision') + { + const copy = src.slice(0); + + copy.unshift(`precision ${def} float;`); + + return copy; + } + } + else if (src.substring(0, 9) !== 'precision') + { + return `precision ${def} float;\n${src}`; + } + + return src; +} + +/** + * Wrapper class, webGL Shader for Pixi. + * Adds precision string if vertexSrc or fragmentSrc have no mention of it. + * + * @class + * @extends GLShader + * @memberof PIXI + */ +export default class Shader extends GLShader +{ + /** + * + * @param {WebGLRenderingContext} gl - The current WebGL rendering context + * @param {string|string[]} vertexSrc - The vertex shader source as an array of strings. + * @param {string|string[]} fragmentSrc - The fragment shader source as an array of strings. + */ + constructor(gl, vertexSrc, fragmentSrc) + { + super(gl, checkPrecision(vertexSrc, settings.PRECISION_VERTEX), + checkPrecision(fragmentSrc, settings.PRECISION_FRAGMENT)); + } +} diff --git a/src/core/const.js b/src/core/const.js index ef6f662..3de0efe 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -188,7 +188,7 @@ * The gc modes that are supported by pixi. * * The {@link PIXI.settings.GC_MODE} Garbage Collection mode for pixi textures is AUTO - * If set to GC_MODE, the renderer will occasianally check textures usage. If they are not + * If set to GC_MODE, the renderer will occasionally check textures usage. If they are not * used for a specified period of time they will be removed from the GPU. They will of course * be uploaded again when they are required. This is a silent behind the scenes process that * should ensure that the GPU does not get filled up. diff --git a/src/core/display/Container.js b/src/core/display/Container.js index ed8cd5f..17fde8d 100644 --- a/src/core/display/Container.js +++ b/src/core/display/Container.js @@ -73,11 +73,12 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.push(child); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -108,11 +109,12 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.splice(index, 0, child); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -225,10 +227,11 @@ if (index === -1) return null; child.parent = null; + // ensure child transform will be recalculated + child.transform._parentID = -1; removeItems(this.children, index, 1); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -249,11 +252,12 @@ { const child = this.getChildAt(index); + // ensure child transform will be recalculated.. child.parent = null; + child.transform._parentID = -1; removeItems(this.children, index, 1); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -284,13 +288,12 @@ for (let i = 0; i < removed.length; ++i) { removed[i].parent = null; + if (removed[i].transform) + { + removed[i].transform._parentID = -1; + } } - // ensure a transform will be recalculated.. - if (this.transform) - { - this.transform._parentID = -1; - } this._boundsID++; this.onChildrenChange(beginIndex); diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index f37f2f5..be4c4fb 100644 --- a/src/core/graphics/Graphics.js +++ b/src/core/graphics/Graphics.js @@ -28,8 +28,9 @@ { /** * + * @param {boolean} [nativeLines=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP */ - constructor() + constructor(nativeLines = false) { super(); @@ -50,6 +51,13 @@ this.lineWidth = 0; /** + * If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * + * @member {boolean} + */ + this.nativeLines = nativeLines; + + /** * The color of any lines drawn. * * @member {string} @@ -757,28 +765,14 @@ if (!this._spriteRect) { - if (!Graphics._SPRITE_TEXTURE) - { - Graphics._SPRITE_TEXTURE = RenderTexture.create(10, 10); - - const canvas = document.createElement('canvas'); - - canvas.width = 10; - canvas.height = 10; - - const context = canvas.getContext('2d'); - - context.fillStyle = 'white'; - context.fillRect(0, 0, 10, 10); - - Graphics._SPRITE_TEXTURE = Texture.fromCanvas(canvas); - } - - this._spriteRect = new Sprite(Graphics._SPRITE_TEXTURE); + this._spriteRect = new Sprite(new Texture(Texture.WHITE)); } + + const sprite = this._spriteRect; + if (this.tint === 0xffffff) { - this._spriteRect.tint = this.graphicsData[0].fillColor; + sprite.tint = this.graphicsData[0].fillColor; } else { @@ -792,20 +786,21 @@ t1[1] *= t2[1]; t1[2] *= t2[2]; - this._spriteRect.tint = rgb2hex(t1); + sprite.tint = rgb2hex(t1); } - this._spriteRect.alpha = this.graphicsData[0].fillAlpha; - this._spriteRect.worldAlpha = this.worldAlpha * this._spriteRect.alpha; + sprite.alpha = this.graphicsData[0].fillAlpha; + sprite.worldAlpha = this.worldAlpha * sprite.alpha; + sprite.blendMode = this.blendMode; - Graphics._SPRITE_TEXTURE._frame.width = rect.width; - Graphics._SPRITE_TEXTURE._frame.height = rect.height; + sprite.texture._frame.width = rect.width; + sprite.texture._frame.height = rect.height; - this._spriteRect.transform.worldTransform = this.transform.worldTransform; + sprite.transform.worldTransform = this.transform.worldTransform; - this._spriteRect.anchor.set(-rect.x / rect.width, -rect.y / rect.height); - this._spriteRect._onAnchorUpdate(); + sprite.anchor.set(-rect.x / rect.width, -rect.y / rect.height); + sprite._onAnchorUpdate(); - this._spriteRect._renderWebGL(renderer); + sprite._renderWebGL(renderer); } /** @@ -1031,6 +1026,7 @@ this.fillColor, this.fillAlpha, this.filling, + this.nativeLines, shape ); diff --git a/src/core/graphics/GraphicsData.js b/src/core/graphics/GraphicsData.js index f5a87ae..b6f03c0 100644 --- a/src/core/graphics/GraphicsData.js +++ b/src/core/graphics/GraphicsData.js @@ -14,14 +14,19 @@ * @param {number} fillColor - the color of the fill * @param {number} fillAlpha - the alpha of the fill * @param {boolean} fill - whether or not the shape is filled with a colour + * @param {boolean} nativeLines - the method for drawing lines * @param {PIXI.Circle|PIXI.Rectangle|PIXI.Ellipse|PIXI.Polygon} shape - The shape object to draw. */ - constructor(lineWidth, lineColor, lineAlpha, fillColor, fillAlpha, fill, shape) + constructor(lineWidth, lineColor, lineAlpha, fillColor, fillAlpha, fill, nativeLines, shape) { /** * @member {number} the width of the line to draw */ this.lineWidth = lineWidth; + /** + * @member {boolean} if true the liens will be draw using LINES instead of TRIANGLE_STRIP + */ + this.nativeLines = nativeLines; /** * @member {number} the color of the line to draw @@ -85,6 +90,7 @@ this.fillColor, this.fillAlpha, this.fill, + this.nativeLines, this.shape ); } diff --git a/src/core/graphics/webgl/GraphicsRenderer.js b/src/core/graphics/webgl/GraphicsRenderer.js index 14083fe..67f5e92 100644 --- a/src/core/graphics/webgl/GraphicsRenderer.js +++ b/src/core/graphics/webgl/GraphicsRenderer.js @@ -102,7 +102,15 @@ shaderTemp.uniforms.alpha = graphics.worldAlpha; renderer.bindVao(webGLData.vao); - webGLData.vao.draw(gl.TRIANGLE_STRIP, webGLData.indices.length); + + if (graphics.nativeLines) + { + gl.drawArrays(gl.LINES, 0, webGLData.points.length / 6); + } + else + { + webGLData.vao.draw(gl.TRIANGLE_STRIP, webGLData.indices.length); + } } } diff --git a/src/core/graphics/webgl/utils/buildLine.js b/src/core/graphics/webgl/utils/buildLine.js index ecf43f7..4d6862e 100644 --- a/src/core/graphics/webgl/utils/buildLine.js +++ b/src/core/graphics/webgl/utils/buildLine.js @@ -11,7 +11,29 @@ * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties * @param {object} webGLData - an object containing all the webGL-specific information to create this shape */ -export default function buildLine(graphicsData, webGLData) +export default function (graphicsData, webGLData) +{ + if (graphicsData.nativeLines) + { + buildNativeLine(graphicsData, webGLData); + } + else + { + buildLine(graphicsData, webGLData); + } +} + +/** + * Builds a line to draw using the poligon method. + * + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @private + * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties + * @param {object} webGLData - an object containing all the webGL-specific information to create this shape + */ +function buildLine(graphicsData, webGLData) { // TODO OPTIMISE! let points = graphicsData.points; @@ -224,3 +246,46 @@ indices.push(indexStart - 1); } + +/** + * Builds a line to draw using the gl.drawArrays(gl.LINES) method + * + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @private + * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties + * @param {object} webGLData - an object containing all the webGL-specific information to create this shape + */ +function buildNativeLine(graphicsData, webGLData) +{ + let i = 0; + const points = graphicsData.points; + + if (points.length === 0) return; + + const verts = webGLData.points; + const length = points.length / 2; + + // sort color + const color = hex2rgb(graphicsData.lineColor); + const alpha = graphicsData.lineAlpha; + const r = color[0] * alpha; + const g = color[1] * alpha; + const b = color[2] * alpha; + + for (i = 1; i < length; i++) + { + const p1x = points[(i - 1) * 2]; + const p1y = points[((i - 1) * 2) + 1]; + + const p2x = points[i * 2]; + const p2y = points[(i * 2) + 1]; + + verts.push(p1x, p1y); + verts.push(r, g, b, alpha); + + verts.push(p2x, p2y); + verts.push(r, g, b, alpha); + } +} diff --git a/src/core/graphics/webgl/utils/buildRoundedRectangle.js b/src/core/graphics/webgl/utils/buildRoundedRectangle.js index 9abc807..a6c8461 100644 --- a/src/core/graphics/webgl/utils/buildRoundedRectangle.js +++ b/src/core/graphics/webgl/utils/buildRoundedRectangle.js @@ -77,6 +77,26 @@ } /** + * Calculate a single point for a quadratic bezier curve. + * Utility function used by quadraticBezierCurve. + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @private + * @param {number} n1 - first number + * @param {number} n2 - second number + * @param {number} perc - percentage + * @return {number} the result + * + */ +function getPt(n1, n2, perc) +{ + const diff = n2 - n1; + + return n1 + (diff * perc); +} + +/** * Calculate the points for a quadratic bezier curve. (helper function..) * Based on: https://stackoverflow.com/questions/785097/how-do-i-implement-a-bezier-curve-in-c * @@ -105,13 +125,6 @@ let x = 0; let y = 0; - function getPt(n1, n2, perc) - { - const diff = n2 - n1; - - return n1 + (diff * perc); - } - for (let i = 0, j = 0; i <= n; ++i) { j = i / n; diff --git a/src/core/index.js b/src/core/index.js index f7a817c..4f414e8 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -30,6 +30,7 @@ export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; export { default as CanvasGraphicsRenderer } from './graphics/canvas/CanvasGraphicsRenderer'; +export { default as Spritesheet } from './textures/Spritesheet'; export { default as Texture } from './textures/Texture'; export { default as BaseTexture } from './textures/BaseTexture'; export { default as RenderTexture } from './textures/RenderTexture'; diff --git a/src/core/math/Matrix.js b/src/core/math/Matrix.js index 0b77477..3679860 100644 --- a/src/core/math/Matrix.js +++ b/src/core/math/Matrix.js @@ -13,45 +13,50 @@ export default class Matrix { /** - * + * @param {number} [a=1] - x scale + * @param {number} [b=0] - y skew + * @param {number} [c=0] - x skew + * @param {number} [d=1] - y scale + * @param {number} [tx=0] - x translation + * @param {number} [ty=0] - y translation */ - constructor() + constructor(a = 1, b = 0, c = 0, d = 1, tx = 0, ty = 0) { /** * @member {number} * @default 1 */ - this.a = 1; + this.a = a; /** * @member {number} * @default 0 */ - this.b = 0; + this.b = b; /** * @member {number} * @default 0 */ - this.c = 0; + this.c = c; /** * @member {number} * @default 1 */ - this.d = 1; + this.d = d; /** * @member {number} * @default 0 */ - this.tx = 0; + this.tx = tx; /** * @member {number} * @default 0 */ - this.ty = 0; + this.ty = ty; this.array = null; } diff --git a/src/core/renderers/SystemRenderer.js b/src/core/renderers/SystemRenderer.js index fa116a6..a24c775 100644 --- a/src/core/renderers/SystemRenderer.js +++ b/src/core/renderers/SystemRenderer.js @@ -1,5 +1,5 @@ import { sayHello, hex2string, hex2rgb } from '../utils'; -import { Matrix } from '../math'; +import { Matrix, Rectangle } from '../math'; import { RENDERER_TYPE } from '../const'; import settings from '../settings'; import Container from '../display/Container'; @@ -21,8 +21,8 @@ { /** * @param {string} system - The name of the system this renderer is for. - * @param {number} [width=800] - the width of the canvas view - * @param {number} [height=600] - the height of the canvas view + * @param {number} [screenWidth=800] - the width of the screen + * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false @@ -37,7 +37,7 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(system, width, height, options) + constructor(system, screenWidth, screenHeight, options) { super(); @@ -69,20 +69,13 @@ this.type = RENDERER_TYPE.UNKNOWN; /** - * The width of the canvas view + * Measurements of the screen. (0, 0, screenWidth, screenHeight) * - * @member {number} - * @default 800 - */ - this.width = width || 800; - - /** - * The height of the canvas view + * Its safe to use as filterArea or hitArea for whole stage * - * @member {number} - * @default 600 + * @member {PIXI.Rectangle} */ - this.height = height || 600; + this.screen = new Rectangle(0, 0, screenWidth || 800, screenHeight || 600); /** * The canvas element that everything is drawn to @@ -107,7 +100,7 @@ this.transparent = options.transparent; /** - * Whether the render view should be resized automatically + * Whether css dimensions of canvas view should be resized to screen dimensions automatically * * @member {boolean} */ @@ -192,23 +185,48 @@ } /** - * Resizes the canvas view to the specified width and height + * Same as view.width, actual number of pixels in the canvas by horizontal * - * @param {number} width - the new width of the canvas view - * @param {number} height - the new height of the canvas view + * @member {number} + * @readonly + * @default 800 */ - resize(width, height) + get width() { - this.width = width * this.resolution; - this.height = height * this.resolution; + return this.view.width; + } - this.view.width = this.width; - this.view.height = this.height; + /** + * Same as view.height, actual number of pixels in the canvas by vertical + * + * @member {number} + * @readonly + * @default 600 + */ + get height() + { + return this.view.height; + } + + /** + * Resizes the screen and canvas to the specified width and height + * Canvas dimensions are multiplied by resolution + * + * @param {number} screenWidth - the new width of the screen + * @param {number} screenHeight - the new height of the screen + */ + resize(screenWidth, screenHeight) + { + this.screen.width = screenWidth; + this.screen.height = screenHeight; + + this.view.width = screenWidth * this.resolution; + this.view.height = screenHeight * this.resolution; if (this.autoResize) { - this.view.style.width = `${this.width / this.resolution}px`; - this.view.style.height = `${this.height / this.resolution}px`; + this.view.style.width = `${screenWidth}px`; + this.view.style.height = `${screenHeight}px`; } } @@ -249,11 +267,10 @@ this.type = RENDERER_TYPE.UNKNOWN; - this.width = 0; - this.height = 0; - this.view = null; + this.screen = null; + this.resolution = 0; this.transparent = false; diff --git a/src/core/renderers/canvas/CanvasRenderer.js b/src/core/renderers/canvas/CanvasRenderer.js index 1bd2d5e..e84e49c 100644 --- a/src/core/renderers/canvas/CanvasRenderer.js +++ b/src/core/renderers/canvas/CanvasRenderer.js @@ -18,8 +18,8 @@ export default class CanvasRenderer extends SystemRenderer { /** - * @param {number} [width=800] - the width of the canvas view - * @param {number} [height=600] - the height of the canvas view + * @param {number} [screenWidth=800] - the width of the screen + * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false @@ -34,9 +34,9 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(width, height, options = {}) + constructor(screenWidth, screenHeight, options = {}) { - super('Canvas', width, height, options); + super('Canvas', screenWidth, screenHeight, options); this.type = RENDERER_TYPE.CANVAS; @@ -96,7 +96,7 @@ this.context = null; this.renderingToScreen = false; - this.resize(width, height); + this.resize(screenWidth, screenHeight); } /** @@ -283,12 +283,12 @@ * * @extends PIXI.SystemRenderer#resize * - * @param {number} width - The new width of the canvas view - * @param {number} height - The new height of the canvas view + * @param {number} screenWidth - the new width of the screen + * @param {number} screenHeight - the new height of the screen */ - resize(width, height) + resize(screenWidth, screenHeight) { - super.resize(width, height); + super.resize(screenWidth, screenHeight); // reset the scale mode.. oddly this seems to be reset when the canvas is resized. // surely a browser bug?? Let pixi fix that for you.. diff --git a/src/core/renderers/webgl/TextureManager.js b/src/core/renderers/webgl/TextureManager.js index b685c1a..8227289 100644 --- a/src/core/renderers/webgl/TextureManager.js +++ b/src/core/renderers/webgl/TextureManager.js @@ -61,7 +61,7 @@ * Updates and/or Creates a WebGL texture for the renderer's context. * * @param {PIXI.BaseTexture|PIXI.Texture} texture - the texture to update - * @param {Number} location - the location the texture will be bound to. + * @param {number} location - the location the texture will be bound to. * @return {GLTexture} The gl texture. */ updateTexture(texture, location) diff --git a/src/core/renderers/webgl/WebGLRenderer.js b/src/core/renderers/webgl/WebGLRenderer.js index e4cd8a0..830b382 100644 --- a/src/core/renderers/webgl/WebGLRenderer.js +++ b/src/core/renderers/webgl/WebGLRenderer.js @@ -31,8 +31,8 @@ { /** * - * @param {number} [width=0] - the width of the canvas view - * @param {number} [height=0] - the height of the canvas view + * @param {number} [screenWidth=800] - the width of the screen + * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false @@ -53,9 +53,9 @@ * @param {boolean} [options.legacy=false] - If true Pixi will aim to ensure compatibility * with older / less advanced devices. If you experiance unexplained flickering try setting this to true. */ - constructor(width, height, options = {}) + constructor(screenWidth, screenHeight, options = {}) { - super('WebGL', width, height, options); + super('WebGL', screenWidth, screenHeight, options); this.legacy = !!options.legacy; @@ -245,7 +245,7 @@ glCore._testingContext = gl; // setup the width/height properties and gl viewport - this.resize(this.width, this.height); + this.resize(this.screen.width, this.screen.height); } /** @@ -343,16 +343,16 @@ /** * Resizes the webGL view to the specified width and height. * - * @param {number} width - the new width of the webGL view - * @param {number} height - the new height of the webGL view + * @param {number} screenWidth - the new width of the screen + * @param {number} screenHeight - the new height of the screen */ - resize(width, height) + resize(screenWidth, screenHeight) { // if(width * this.resolution === this.width && height * this.resolution === this.height)return; - SystemRenderer.prototype.resize.call(this, width, height); + SystemRenderer.prototype.resize.call(this, screenWidth, screenHeight); - this.rootRenderTarget.resize(width, height); + this.rootRenderTarget.resize(screenWidth, screenHeight); if (this._activeRenderTarget === this.rootRenderTarget) { @@ -396,6 +396,26 @@ } /** + * Erases the render texture and fills the drawing area with a colour + * + * @param {PIXI.RenderTexture} renderTexture - The render texture to clear + * @param {number} [clearColor] - The colour + * @return {PIXI.WebGLRenderer} Returns itself. + */ + clearRenderTexture(renderTexture, clearColor) + { + const baseTexture = renderTexture.baseTexture; + const renderTarget = baseTexture._glRenderTargets[this.CONTEXT_UID]; + + if (renderTarget) + { + renderTarget.clear(clearColor); + } + + return this; + } + + /** * Binds a render texture for rendering * * @param {PIXI.RenderTexture} renderTexture - The render texture to render @@ -474,9 +494,10 @@ * Changes the current GLShader to the one given in parameter * * @param {PIXI.glCore.Shader} shader - the new glShader + * @param {boolean} [autoProject=true] - Whether automatically set the projection matrix * @return {PIXI.WebGLRenderer} Returns itself. */ - _bindGLShader(shader) + _bindGLShader(shader, autoProject) { // TODO cache if (this._activeShader !== shader) @@ -484,8 +505,14 @@ this._activeShader = shader; shader.bind(); - // automatically set the projection matrix - shader.uniforms.projectionMatrix = this._activeRenderTarget.projectionMatrix.toArray(true); + // `autoProject` normally would be a default parameter set to true + // but because of how Babel transpiles default parameters + // it hinders the performance of this method. + if (autoProject !== false) + { + // automatically set the projection matrix + shader.uniforms.projectionMatrix = this._activeRenderTarget.projectionMatrix.toArray(true); + } } return this; diff --git a/src/core/renderers/webgl/WebGLState.js b/src/core/renderers/webgl/WebGLState.js index ba6ecc5..6c846d5 100755 --- a/src/core/renderers/webgl/WebGLState.js +++ b/src/core/renderers/webgl/WebGLState.js @@ -82,18 +82,20 @@ push() { // next state.. - let state = this.stack[++this.stackIndex]; + let state = this.stack[this.stackIndex]; if (!state) { state = this.stack[this.stackIndex] = new Uint8Array(16); } + ++this.stackIndex; + // copy state.. // set active state so we can force overrides of gl state for (let i = 0; i < this.activeState.length; i++) { - this.activeState[i] = state[i]; + state[i] = this.activeState[i]; } } diff --git a/src/core/renderers/webgl/filters/Filter.js b/src/core/renderers/webgl/filters/Filter.js index 2c3eaad..4eb6128 100644 --- a/src/core/renderers/webgl/filters/Filter.js +++ b/src/core/renderers/webgl/filters/Filter.js @@ -9,7 +9,7 @@ * @memberof PIXI * @extends PIXI.Shader */ -class Filter extends Shader +export default class Filter extends Shader { /** * @param {string} [vertexSrc] - The source of the vertex shader. @@ -56,15 +56,16 @@ * @param {PIXI.RenderTarget} input - The input render target. * @param {PIXI.RenderTarget} output - The target to output to. * @param {boolean} clear - Should the output be cleared before rendering to it + * @param {object} [currentState] - It's current state of filter. + * There are some useful properties in the currentState : + * target, filters, sourceFrame, destinationFrame, renderTarget, resolution */ - apply(filterManager, input, output, clear) + apply(filterManager, input, output, clear, currentState) // eslint-disable-line no-unused-vars { // do as you please! - filterManager.applyFilter(this, input, output, clear); + filterManager.applyFilter(this, input, output, clear, currentState); // or just do a regular render.. } } - -export default Filter; diff --git a/src/core/renderers/webgl/filters/extractUniformsFromSrc.js b/src/core/renderers/webgl/filters/extractUniformsFromSrc.js index d78a0b1..4893214 100644 --- a/src/core/renderers/webgl/filters/extractUniformsFromSrc.js +++ b/src/core/renderers/webgl/filters/extractUniformsFromSrc.js @@ -12,7 +12,7 @@ function extractUniformsFromString(string) { - const maskRegex = new RegExp('^(projectionMatrix|uSampler|filterArea)$'); + const maskRegex = new RegExp('^(projectionMatrix|uSampler|filterArea|filterClamp)$'); const uniforms = {}; let nameSplit; diff --git a/src/core/renderers/webgl/managers/FilterManager.js b/src/core/renderers/webgl/managers/FilterManager.js index 43fc869..6c7422d 100644 --- a/src/core/renderers/webgl/managers/FilterManager.js +++ b/src/core/renderers/webgl/managers/FilterManager.js @@ -2,6 +2,7 @@ import RenderTarget from '../utils/RenderTarget'; import Quad from '../utils/Quad'; import { Rectangle } from '../../../math'; +import Shader from '../../../Shader'; import * as filterTransforms from '../filters/filterTransforms'; import bitTwiddle from 'bit-twiddle'; @@ -22,8 +23,6 @@ this.filters = []; this.target = null; this.resolution = 1; - - this.firstRun = true; } } @@ -155,7 +154,7 @@ if (filters.length === 1) { - filters[0].apply(this, currentState.renderTarget, lastState.renderTarget, false); + filters[0].apply(this, currentState.renderTarget, lastState.renderTarget, false, currentState); this.freePotRenderTarget(currentState.renderTarget); } else @@ -177,7 +176,7 @@ for (i = 0; i < filters.length - 1; ++i) { - filters[i].apply(this, flip, flop, true); + filters[i].apply(this, flip, flop, true, currentState); const t = flip; @@ -185,7 +184,7 @@ flop = t; } - filters[i].apply(this, flip, lastState.renderTarget, false); + filters[i].apply(this, flip, lastState.renderTarget, false, currentState); this.freePotRenderTarget(flip); this.freePotRenderTarget(flop); @@ -212,16 +211,28 @@ const renderer = this.renderer; const gl = renderer.gl; - renderer.bindShader(filter, true); + let shader = filter.glShaders[renderer.CONTEXT_UID]; - // TODO theres a better way! - // quad can be a mesh and we then should be able to pull this off without accessing the shader directly - const shader = renderer.shaderManager.getGLShader(); - - // TODO change quad to mesh.. - if (!this.firstRun) + // cacheing.. + if (!shader) { - this.firstRun = true; + if (filter.glShaderKey) + { + shader = this.shaderCache[filter.glShaderKey]; + + if (!shader) + { + shader = new Shader(this.gl, filter.vertexSrc, filter.fragmentSrc); + + filter.glShaders[renderer.CONTEXT_UID] = this.shaderCache[filter.glShaderKey] = shader; + } + } + else + { + shader = filter.glShaders[renderer.CONTEXT_UID] = new Shader(this.gl, filter.vertexSrc, filter.fragmentSrc); + } + + // TODO - this only needs to be done once? renderer.bindVao(null); this.quad.initVao(shader); @@ -244,58 +255,146 @@ renderer.maskManager.pushScissorMask(null, renderer.maskManager.scissorData); } - // renderer._bindGLShader(shader); + renderer.bindShader(shader); - let currentState; - const uniforms = filter.uniforms; + // free unit 0 for us, doesn't matter what was there + // don't try to restore it, because syncUniforms can upload it to another slot + // and it'll be a problem + const tex = this.renderer.emptyTextures[0]; - if (uniforms.filterArea) - { - currentState = this.filterData.stack[this.filterData.index]; - - const filterArea = uniforms.filterArea; - - filterArea[0] = currentState.renderTarget.size.width; - filterArea[1] = currentState.renderTarget.size.height; - filterArea[2] = currentState.sourceFrame.x; - filterArea[3] = currentState.sourceFrame.y; - - filter.uniforms.filterArea = filterArea; - } - - // use this to clamp displaced texture coords so they belong to filterArea - // see displacementFilter fragment shader for an example - if (uniforms.filterClamp) - { - currentState = this.filterData.stack[this.filterData.index]; - - const filterClamp = uniforms.filterClamp; - - filterClamp[0] = 0; - filterClamp[1] = 0; - filterClamp[2] = (currentState.sourceFrame.width - 1) / currentState.renderTarget.size.width; - filterClamp[3] = (currentState.sourceFrame.height - 1) / currentState.renderTarget.size.height; - - filter.uniforms.filterClamp = filterClamp; - } - - renderer.shaderManager.setUniforms(filter.uniforms); + this.renderer.boundTextures[0] = tex; + // this syncs the pixi filters uniforms with glsl uniforms + this.syncUniforms(shader, filter); renderer.state.setBlendMode(filter.blendMode); - // temporary bypass cache.. - const tex = this.renderer.boundTextures[0]; - gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, input.texture.texture); this.quad.vao.draw(this.renderer.gl.TRIANGLES, 6, 0); - // restore cache. gl.bindTexture(gl.TEXTURE_2D, tex._glTextures[this.renderer.CONTEXT_UID].texture); } /** + * Uploads the uniforms of the filter. + * + * @param {GLShader} shader - The underlying gl shader. + * @param {PIXI.Filter} filter - The filter we are synchronizing. + */ + syncUniforms(shader, filter) + { + const uniformData = filter.uniformData; + const uniforms = filter.uniforms; + + // 0 is reserved for the pixi texture so we start at 1! + let textureCount = 1; + let currentState; + + // filterArea and filterClamp that are handled by FilterManager directly + // they must not appear in uniformData + + if (shader.uniforms.filterArea) + { + currentState = this.filterData.stack[this.filterData.index]; + + const filterArea = shader.uniforms.filterArea; + + filterArea[0] = currentState.renderTarget.size.width; + filterArea[1] = currentState.renderTarget.size.height; + filterArea[2] = currentState.sourceFrame.x; + filterArea[3] = currentState.sourceFrame.y; + + shader.uniforms.filterArea = filterArea; + } + + // use this to clamp displaced texture coords so they belong to filterArea + // see displacementFilter fragment shader for an example + if (shader.uniforms.filterClamp) + { + currentState = currentState || this.filterData.stack[this.filterData.index]; + + const filterClamp = shader.uniforms.filterClamp; + + filterClamp[0] = 0; + filterClamp[1] = 0; + filterClamp[2] = (currentState.sourceFrame.width - 1) / currentState.renderTarget.size.width; + filterClamp[3] = (currentState.sourceFrame.height - 1) / currentState.renderTarget.size.height; + + shader.uniforms.filterClamp = filterClamp; + } + + // TODO Cacheing layer.. + for (const i in uniformData) + { + if (uniformData[i].type === 'sampler2D' && uniforms[i] !== 0) + { + if (uniforms[i].baseTexture) + { + shader.uniforms[i] = this.renderer.bindTexture(uniforms[i].baseTexture, textureCount); + } + else + { + shader.uniforms[i] = textureCount; + + // TODO + // this is helpful as renderTargets can also be set. + // Although thinking about it, we could probably + // make the filter texture cache return a RenderTexture + // rather than a renderTarget + const gl = this.renderer.gl; + + this.renderer.boundTextures[textureCount] = this.renderer.emptyTextures[textureCount]; + gl.activeTexture(gl.TEXTURE0 + textureCount); + + uniforms[i].texture.bind(); + } + + textureCount++; + } + else if (uniformData[i].type === 'mat3') + { + // check if its pixi matrix.. + if (uniforms[i].a !== undefined) + { + shader.uniforms[i] = uniforms[i].toArray(true); + } + else + { + shader.uniforms[i] = uniforms[i]; + } + } + else if (uniformData[i].type === 'vec2') + { + // check if its a point.. + if (uniforms[i].x !== undefined) + { + const val = shader.uniforms[i] || new Float32Array(2); + + val[0] = uniforms[i].x; + val[1] = uniforms[i].y; + shader.uniforms[i] = val; + } + else + { + shader.uniforms[i] = uniforms[i]; + } + } + else if (uniformData[i].type === 'float') + { + if (shader.uniforms.data[i].value !== uniformData[i]) + { + shader.uniforms[i] = uniforms[i]; + } + } + else + { + shader.uniforms[i] = uniforms[i]; + } + } + } + + /** * Gets a render target from the pool, or creates a new one. * * @param {boolean} clear - Should we clear the render texture when we get it? @@ -442,7 +541,6 @@ renderTarget.resolution = resolution; renderTarget.defaultFrame.width = renderTarget.size.width = minWidth / resolution; renderTarget.defaultFrame.height = renderTarget.size.height = minHeight / resolution; - renderTarget.attachStencilBuffer(); return renderTarget; } diff --git a/src/core/settings.js b/src/core/settings.js index bb08cb0..f4dd087 100644 --- a/src/core/settings.js +++ b/src/core/settings.js @@ -77,11 +77,11 @@ * * @static * @memberof PIXI.settings - * @type {RegExp|string} + * @type {RegExp} * @example `@2x` - * @default /@(.+)x/ + * @default /@([0-9\.]+)x/ */ - RETINA_PREFIX: /@(.+)x/, + RETINA_PREFIX: /@([0-9\.]+)x/, /** * The default render options if none are supplied to {@link PIXI.WebGLRenderer} @@ -175,14 +175,24 @@ SCALE_MODE: 0, /** - * Default specify float precision in shaders. + * Default specify float precision in vertex shader. + * + * @static + * @memberof PIXI.settings + * @type {PIXI.PRECISION} + * @default PIXI.PRECISION.HIGH + */ + PRECISION_VERTEX: 'highp', + + /** + * Default specify float precision in fragment shader. * * @static * @memberof PIXI.settings * @type {PIXI.PRECISION} * @default PIXI.PRECISION.MEDIUM */ - PRECISION: 'mediump', + PRECISION_FRAGMENT: 'mediump', /** * Can we upload the same buffer in a single frame? diff --git a/src/core/sprites/Sprite.js b/src/core/sprites/Sprite.js index 28b0b18..121bf58 100644 --- a/src/core/sprites/Sprite.js +++ b/src/core/sprites/Sprite.js @@ -212,11 +212,11 @@ } else { - w0 = orig.width * (1 - anchor._x); - w1 = orig.width * -anchor._x; + w1 = -anchor._x * orig.width; + w0 = w1 + orig.width; - h0 = orig.height * (1 - anchor._y); - h1 = orig.height * -anchor._y; + h1 = -anchor._y * orig.height; + h0 = h1 + orig.height; } // xy @@ -269,11 +269,11 @@ const tx = wt.tx; const ty = wt.ty; - const w0 = (orig.width) * (1 - anchor._x); - const w1 = (orig.width) * -anchor._x; + const w1 = -anchor._x * orig.width; + const w0 = w1 + orig.width; - const h0 = orig.height * (1 - anchor._y); - const h1 = orig.height * -anchor._y; + const h1 = -anchor._y * orig.height; + const h0 = h1 + orig.height; // xy vertexData[0] = (a * w1) + (c * h1) + tx; @@ -440,7 +440,7 @@ * * @static * @param {number|string|PIXI.BaseTexture|HTMLCanvasElement|HTMLVideoElement} source Source to create texture from - * @return {PIXI.Sprite} The newly created texture + * @return {PIXI.Sprite} The newly created sprite */ static from(source) { @@ -538,8 +538,8 @@ } /** - * The tint applied to the sprite. This is a hex value. A value of - * 0xFFFFFF will remove any tint effect. + * The tint applied to the sprite. This is a hex value. + * A value of 0xFFFFFF will remove any tint effect. * * @member {number} * @default 0xFFFFFF diff --git a/src/core/sprites/canvas/CanvasTinter.js b/src/core/sprites/canvas/CanvasTinter.js index 1cf316b..27e5e0c 100644 --- a/src/core/sprites/canvas/CanvasTinter.js +++ b/src/core/sprites/canvas/CanvasTinter.js @@ -75,8 +75,8 @@ crop.width *= resolution; crop.height *= resolution; - canvas.width = crop.width; - canvas.height = crop.height; + canvas.width = Math.ceil(crop.width); + canvas.height = Math.ceil(crop.height); context.fillStyle = `#${(`00000${(color | 0).toString(16)}`).substr(-6)}`; @@ -130,8 +130,8 @@ crop.width *= resolution; crop.height *= resolution; - canvas.width = crop.width; - canvas.height = crop.height; + canvas.width = Math.ceil(crop.width); + canvas.height = Math.ceil(crop.height); context.globalCompositeOperation = 'copy'; context.fillStyle = `#${(`00000${(color | 0).toString(16)}`).substr(-6)}`; @@ -172,8 +172,8 @@ crop.width *= resolution; crop.height *= resolution; - canvas.width = crop.width; - canvas.height = crop.height; + canvas.width = Math.ceil(crop.width); + canvas.height = Math.ceil(crop.height); context.globalCompositeOperation = 'copy'; context.drawImage( diff --git a/src/core/sprites/webgl/SpriteRenderer.js b/src/core/sprites/webgl/SpriteRenderer.js index 2affcd5..7c352e2 100644 --- a/src/core/sprites/webgl/SpriteRenderer.js +++ b/src/core/sprites/webgl/SpriteRenderer.js @@ -30,7 +30,7 @@ /** * Number of values sent in the vertex buffer. - * positionX, positionY, colorR, colorG, colorB = 5 + * aVertexPosition(2), aTextureCoord(1), aColor(1), aTextureId(1) = 5 * * @member {number} */ @@ -75,7 +75,6 @@ this.shader = null; this.currentIndex = 0; - TICK = 0; this.groups = []; for (let k = 0; k < this.size; k++) diff --git a/src/core/textures/BaseTexture.js b/src/core/textures/BaseTexture.js index 853de7e..aeccf39 100644 --- a/src/core/textures/BaseTexture.js +++ b/src/core/textures/BaseTexture.js @@ -232,16 +232,24 @@ this.realWidth = this.source.naturalWidth || this.source.videoWidth || this.source.width; this.realHeight = this.source.naturalHeight || this.source.videoHeight || this.source.height; - this.width = this.realWidth / this.resolution; - this.height = this.realHeight / this.resolution; - - this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); + this._updateDimensions(); } this.emit('update', this); } /** + * Update dimensions from real values + */ + _updateDimensions() + { + this.width = this.realWidth / this.resolution; + this.height = this.realHeight / this.resolution; + + this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); + } + + /** * Load a source. * * If the source is not-immediately-available, such as an image that needs to be @@ -525,11 +533,7 @@ this.realWidth = Math.round(svgWidth * this.sourceScale); this.realHeight = Math.round(svgHeight * this.sourceScale); - this.width = this.realWidth / this.resolution; - this.height = this.realHeight / this.resolution; - - // Check pow2 after scale - this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); + this._updateDimensions(); // Create a canvas element const canvas = document.createElement('canvas'); @@ -690,4 +694,52 @@ return baseTexture; } + + /** + * Helper function that creates a base texture based on the source you provide. + * The source can be - image url, image element, canvas element. + * + * @static + * @param {string|HTMLImageElement|HTMLCanvasElement} source - The source to create base texture from. + * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values + * @param {number} [sourceScale=(auto)] - Scale for the original image, used with Svg images. + * @return {PIXI.BaseTexture} The new base texture. + */ + static from(source, scaleMode, sourceScale) + { + if (typeof source === 'string') + { + return BaseTexture.fromImage(source, undefined, scaleMode, sourceScale); + } + else if (source instanceof HTMLImageElement) + { + const imageUrl = source.src; + let baseTexture = BaseTextureCache[imageUrl]; + + if (!baseTexture) + { + baseTexture = new BaseTexture(source, scaleMode); + baseTexture.imageUrl = imageUrl; + + if (sourceScale) + { + baseTexture.sourceScale = sourceScale; + } + + // if there is an @2x at the end of the url we are going to assume its a highres image + baseTexture.resolution = getResolutionOfUrl(imageUrl); + + BaseTextureCache[imageUrl] = baseTexture; + } + + return baseTexture; + } + else if (source instanceof HTMLCanvasElement) + { + return BaseTexture.fromCanvas(source, scaleMode); + } + + // lets assume its a base texture! + return source; + } } diff --git a/src/core/textures/Spritesheet.js b/src/core/textures/Spritesheet.js new file mode 100644 index 0000000..d469c62 --- /dev/null +++ b/src/core/textures/Spritesheet.js @@ -0,0 +1,274 @@ +import { Rectangle, Texture } from '../'; +import { getResolutionOfUrl, TextureCache } from '../utils'; + +/** + * Utility class for maintaining reference to a collection + * of Textures on a single Spritesheet. + * + * @class + * @memberof PIXI + */ +export default class Spritesheet +{ + /** + * The maximum number of Textures to build per process. + * + * @type {number} + * @default 1000 + */ + static get BATCH_SIZE() + { + return 1000; + } + + /** + * @param {PIXI.BaseTexture} baseTexture Reference to the source BaseTexture object. + * @param {Object} data - Spritesheet image data. + * @param {string} [resolutionFilename] - The filename to consider when determining + * the resolution of the spritesheet. If not provided, the imageUrl will + * be used on the BaseTexture. + */ + constructor(baseTexture, data, resolutionFilename = null) + { + /** + * Reference to ths source texture + * @type {PIXI.BaseTexture} + */ + this.baseTexture = baseTexture; + + /** + * Map of spritesheet textures. + * @type {Object} + */ + this.textures = {}; + + /** + * Reference to the original JSON data. + * @type {Object} + */ + this.data = data; + + /** + * The resolution of the spritesheet. + * @type {number} + */ + this.resolution = this._updateResolution( + resolutionFilename || this.baseTexture.imageUrl + ); + + /** + * Map of spritesheet frames. + * @type {Object} + * @private + */ + this._frames = this.data.frames; + + /** + * Collection of frame names. + * @type {string[]} + * @private + */ + this._frameKeys = Object.keys(this._frames); + + /** + * Current batch index being processed. + * @type {number} + * @private + */ + this._batchIndex = 0; + + /** + * Callback when parse is completed. + * @type {Function} + * @private + */ + this._callback = null; + } + + /** + * Generate the resolution from the filename or fallback + * to the meta.scale field of the JSON data. + * + * @private + * @param {string} resolutionFilename - The filename to use for resolving + * the default resolution. + * @return {number} Resolution to use for spritesheet. + */ + _updateResolution(resolutionFilename) + { + const scale = this.data.meta.scale; + + // Use a defaultValue of `null` to check if a url-based resolution is set + let resolution = getResolutionOfUrl(resolutionFilename, null); + + // No resolution found via URL + if (resolution === null) + { + // Use the scale value or default to 1 + resolution = scale !== undefined ? parseFloat(scale) : 1; + } + + // For non-1 resolutions, update baseTexture + if (resolution !== 1) + { + this.baseTexture.resolution = resolution; + this.baseTexture.update(); + } + + return resolution; + } + + /** + * Parser spritesheet from loaded data. This is done asynchronously + * to prevent creating too many Texture within a single process. + * + * @param {Function} callback - Callback when complete returns + * a map of the Textures for this spritesheet. + */ + parse(callback) + { + this._batchIndex = 0; + this._callback = callback; + + if (this._frameKeys.length <= Spritesheet.BATCH_SIZE) + { + this._processFrames(0); + this._parseComplete(); + } + else + { + this._nextBatch(); + } + } + + /** + * Process a batch of frames + * + * @private + * @param {number} initialFrameIndex - The index of frame to start. + */ + _processFrames(initialFrameIndex) + { + let frameIndex = initialFrameIndex; + const maxFrames = Spritesheet.BATCH_SIZE; + + while (frameIndex - initialFrameIndex < maxFrames && frameIndex < this._frameKeys.length) + { + const i = this._frameKeys[frameIndex]; + const rect = this._frames[i].frame; + + if (rect) + { + let frame = null; + let trim = null; + const orig = new Rectangle( + 0, + 0, + this._frames[i].sourceSize.w / this.resolution, + this._frames[i].sourceSize.h / this.resolution + ); + + if (this._frames[i].rotated) + { + frame = new Rectangle( + rect.x / this.resolution, + rect.y / this.resolution, + rect.h / this.resolution, + rect.w / this.resolution + ); + } + else + { + frame = new Rectangle( + rect.x / this.resolution, + rect.y / this.resolution, + rect.w / this.resolution, + rect.h / this.resolution + ); + } + + // Check to see if the sprite is trimmed + if (this._frames[i].trimmed) + { + trim = new Rectangle( + this._frames[i].spriteSourceSize.x / this.resolution, + this._frames[i].spriteSourceSize.y / this.resolution, + rect.w / this.resolution, + rect.h / this.resolution + ); + } + + this.textures[i] = new Texture( + this.baseTexture, + frame, + orig, + trim, + this._frames[i].rotated ? 2 : 0 + ); + + // lets also add the frame to pixi's global cache for fromFrame and fromImage functions + TextureCache[i] = this.textures[i]; + } + + frameIndex++; + } + } + + /** + * The parse has completed. + * + * @private + */ + _parseComplete() + { + const callback = this._callback; + + this._callback = null; + this._batchIndex = 0; + callback.call(this, this.textures); + } + + /** + * Begin the next batch of textures. + * + * @private + */ + _nextBatch() + { + this._processFrames(this._batchIndex * Spritesheet.BATCH_SIZE); + this._batchIndex++; + setTimeout(() => + { + if (this._batchIndex * Spritesheet.BATCH_SIZE < this._frameKeys.length) + { + this._nextBatch(); + } + else + { + this._parseComplete(); + } + }, 0); + } + + /** + * Destroy Spritesheet and don't use after this. + * + * @param {boolean} [destroyBase=false] Whether to destroy the base texture as well + */ + destroy(destroyBase = false) + { + for (const i in this.textures) + { + this.textures[i].destroy(); + } + this._frames = null; + this._frameKeys = null; + this.data = null; + this.textures = null; + if (destroyBase) + { + this.baseTexture.destroy(); + } + this.baseTexture = null; + } +} diff --git a/src/core/textures/Texture.js b/src/core/textures/Texture.js index 6403d4f..afe4254 100644 --- a/src/core/textures/Texture.js +++ b/src/core/textures/Texture.js @@ -3,7 +3,7 @@ import TextureUvs from './TextureUvs'; import EventEmitter from 'eventemitter3'; import { Rectangle } from '../math'; -import { TextureCache, BaseTextureCache } from '../utils'; +import { TextureCache, BaseTextureCache, getResolutionOfUrl } from '../utils'; /** * A texture stores the information that represents an image or part of an image. It cannot be added @@ -365,7 +365,8 @@ * The source can be - frame id, image url, video url, canvas element, video element, base texture * * @static - * @param {number|string|PIXI.BaseTexture|HTMLCanvasElement|HTMLVideoElement} source - Source to create texture from + * @param {number|string|HTMLImageElement|HTMLCanvasElement|HTMLVideoElement|PIXI.BaseTexture} + * source - Source to create texture from * @return {PIXI.Texture} The newly created texture */ static from(source) @@ -393,7 +394,7 @@ } else if (source instanceof HTMLImageElement) { - return new Texture(new BaseTexture(source)); + return new Texture(BaseTexture.from(source)); } else if (source instanceof HTMLCanvasElement) { @@ -413,6 +414,43 @@ } /** + * Create a texture from a source and add to the cache. + * + * @static + * @param {HTMLImageElement|HTMLCanvasElement} source - The input source. + * @param {String} imageUrl - File name of texture, for cache and resolving resolution. + * @param {String} [name] - Human readible name for the texture cache. If no name is + * specified, only `imageUrl` will be used as the cache ID. + * @return {PIXI.Texture} Output texture + */ + static fromLoader(source, imageUrl, name) + { + const baseTexture = new BaseTexture(source, null, getResolutionOfUrl(imageUrl)); + const texture = new Texture(baseTexture); + + baseTexture.imageUrl = imageUrl; + + // No name, use imageUrl instead + if (!name) + { + name = imageUrl; + } + + // lets also add the frame to pixi's global cache for fromFrame and fromImage fucntions + BaseTextureCache[name] = baseTexture; + TextureCache[name] = texture; + + // also add references by url if they are different. + if (name !== imageUrl) + { + BaseTextureCache[imageUrl] = baseTexture; + TextureCache[imageUrl] = texture; + } + + return texture; + } + + /** * Adds a texture to the global TextureCache. This cache is shared across the whole PIXI object. * * @static @@ -522,6 +560,29 @@ } } +function createWhiteTexture() +{ + const canvas = document.createElement('canvas'); + + canvas.width = 10; + canvas.height = 10; + + const context = canvas.getContext('2d'); + + context.fillStyle = 'white'; + context.fillRect(0, 0, 10, 10); + + return new Texture(new BaseTexture(canvas)); +} + +function removeAllHandlers(tex) +{ + tex.destroy = function _emptyDestroy() { /* empty */ }; + tex.on = function _emptyOn() { /* empty */ }; + tex.once = function _emptyOnce() { /* empty */ }; + tex.emit = function _emptyEmit() { /* empty */ }; +} + /** * An empty texture, used often to not have to create multiple empty textures. * Can not be destroyed. @@ -530,7 +591,14 @@ * @constant */ Texture.EMPTY = new Texture(new BaseTexture()); -Texture.EMPTY.destroy = function _emptyDestroy() { /* empty */ }; -Texture.EMPTY.on = function _emptyOn() { /* empty */ }; -Texture.EMPTY.once = function _emptyOnce() { /* empty */ }; -Texture.EMPTY.emit = function _emptyEmit() { /* empty */ }; +removeAllHandlers(Texture.EMPTY); + +/** + * A white texture of 10x10 size, used for graphics and other things + * Can not be destroyed. + * + * @static + * @constant + */ +Texture.WHITE = createWhiteTexture(); +removeAllHandlers(Texture.WHITE); diff --git a/src/core/ticker/Ticker.js b/src/core/ticker/Ticker.js index d98a1f0..17a1517 100644 --- a/src/core/ticker/Ticker.js +++ b/src/core/ticker/Ticker.js @@ -67,11 +67,12 @@ * is based, this value is neither capped nor scaled. * If the platform supports DOMHighResTimeStamp, * this value will have a precision of 1 µs. + * Defaults to target frame time * * @member {number} - * @default 1 / TARGET_FPMS + * @default 16.66 */ - this.elapsedMS = 1 / settings.TARGET_FPMS; // default to target frame time + this.elapsedMS = 1 / settings.TARGET_FPMS; /** * The last time {@link PIXI.ticker.Ticker#update} was invoked. diff --git a/src/core/utils/index.js b/src/core/utils/index.js index 6ff609f..be44750 100644 --- a/src/core/utils/index.js +++ b/src/core/utils/index.js @@ -2,6 +2,7 @@ import settings from '../settings'; import EventEmitter from 'eventemitter3'; import pluginTarget from './pluginTarget'; +import * as mixins from './mixin'; import * as isMobile from 'ismobilejs'; let nextUid = 0; @@ -33,6 +34,7 @@ * @type {mixin} */ pluginTarget, + mixins, }; /** @@ -93,7 +95,7 @@ */ export function rgb2hex(rgb) { - return (((rgb[0] * 255) << 16) + ((rgb[1] * 255) << 8) + (rgb[2] * 255)); + return (((rgb[0] * 255) << 16) + ((rgb[1] * 255) << 8) + (rgb[2] * 255 | 0)); } /** @@ -368,3 +370,43 @@ * @private */ export const ProgramCache = {}; + +/** + * Destroys all texture in the cache + * + * @memberof PIXI.utils + * @function destroyTextureCache + */ +export function destroyTextureCache() +{ + let key; + + for (key in TextureCache) + { + TextureCache[key].destroy(); + } + for (key in BaseTextureCache) + { + BaseTextureCache[key].destroy(); + } +} + +/** + * Removes all textures from cache, but does not destroy them + * + * @memberof PIXI.utils + * @function clearTextureCache + */ +export function clearTextureCache() +{ + let key; + + for (key in TextureCache) + { + delete TextureCache[key]; + } + for (key in BaseTextureCache) + { + delete BaseTextureCache[key]; + } +} diff --git a/src/core/utils/mixin.js b/src/core/utils/mixin.js new file mode 100644 index 0000000..114de2a --- /dev/null +++ b/src/core/utils/mixin.js @@ -0,0 +1,59 @@ +/** + * Mixes all enumerable properties and methods from a source object to a target object. + * + * @memberof PIXI.utils.mixins + * @function mixin + * @param {object} target The prototype or instance that properties and methods should be added to. + * @param {object} source The source of properties and methods to mix in. + */ +export function mixin(target, source) +{ + if (!target || !source) return; + // in ES8/ES2017, this would be really easy: + // Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); + + // get all the enumerable property keys + const keys = Object.keys(source); + + // loop through properties + for (let i = 0; i < keys.length; ++i) + { + const propertyName = keys[i]; + + // Set the property using the property descriptor - this works for accessors and normal value properties + Object.defineProperty(target, propertyName, Object.getOwnPropertyDescriptor(source, propertyName)); + } +} + +const mixins = []; + +/** + * Queues a mixin to be handled towards the end of the initialization of PIXI, so that deprecation + * can take effect. + * + * @memberof PIXI.utils.mixins + * @function delayMixin + * @private + * @param {object} target The prototype or instance that properties and methods should be added to. + * @param {object} source The source of properties and methods to mix in. + */ +export function delayMixin(target, source) +{ + mixins.push(target, source); +} + +/** + * Handles all mixins queued via delayMixin(). + * + * @memberof PIXI.utils.mixins + * @function performMixins + * @private + */ +export function performMixins() +{ + for (let i = 0; i < mixins.length; i += 2) + { + mixin(mixins[i], mixins[i + 1]); + } + mixins.length = 0; +} diff --git a/src/deprecation.js b/src/deprecation.js index 045a19f..b9c73b3 100644 --- a/src/deprecation.js +++ b/src/deprecation.js @@ -5,6 +5,7 @@ import * as filters from './filters'; import * as prepare from './prepare'; import * as loaders from './loaders'; +import * as interaction from './interaction'; // provide method to give a stack track for warnings // useful for tracking-down where deprecated methods/properties/classes @@ -534,7 +535,7 @@ { parent: 'GC_MODES', target: 'GC_MODE' }, { parent: 'WRAP_MODES', target: 'WRAP_MODE' }, { parent: 'SCALE_MODES', target: 'SCALE_MODE' }, - { parent: 'PRECISION', target: 'PRECISION' }, + { parent: 'PRECISION', target: 'PRECISION_FRAGMENT' }, ]; for (let i = 0; i < defaults.length; i++) @@ -558,6 +559,32 @@ }); } +Object.defineProperties(core.settings, { + + /** + * @static + * @name PRECISION + * @memberof PIXI.settings + * @see PIXI.PRECISION + * @deprecated since version 4.4.0 + */ + PRECISION: { + enumerable: true, + get() + { + warn('PIXI.settings.PRECISION has been deprecated, please use PIXI.settings.PRECISION_FRAGMENT'); + + return core.settings.PRECISION_FRAGMENT; + }, + set(value) + { + warn('PIXI.settings.PRECISION has been deprecated, please use PIXI.settings.PRECISION_FRAGMENT'); + + core.settings.PRECISION_FRAGMENT = value; + }, + }, +}); + Object.defineProperties(extras, { /** @@ -694,7 +721,7 @@ warn('determineFontProperties is now deprecated, please use the static calculateFontProperties method, ' + 'e.g : Text.calculateFontProperties(fontStyle);'); - return Text.calculateFontProperties(fontStyle); + return core.Text.calculateFontProperties(fontStyle); }; Object.defineProperties(core.TextStyle.prototype, { @@ -1003,3 +1030,69 @@ }, }, }); + +/** + * @name PIXI.interaction.interactiveTarget#defaultCursor + * @static + * @type {number} + * @see PIXI.interaction.interactiveTarget#cursor + * @deprecated since 4.3.0 + */ +Object.defineProperty(interaction.interactiveTarget, 'defaultCursor', { + set(value) + { + warn('Property defaultCursor has been replaced with \'cursor\'. '); + this.cursor = value; + }, + get() + { + warn('Property defaultCursor has been replaced with \'cursor\'. '); + + return this.cursor; + }, + enumerable: true, +}); + +/** + * @name PIXI.interaction.InteractionManager#defaultCursorStyle + * @static + * @type {string} + * @see PIXI.interaction.InteractionManager#cursorStyles + * @deprecated since 4.3.0 + */ +Object.defineProperty(interaction.InteractionManager, 'defaultCursorStyle', { + set(value) + { + warn('Property defaultCursorStyle has been replaced with \'cursorStyles.default\'. '); + this.cursorStyles.default = value; + }, + get() + { + warn('Property defaultCursorStyle has been replaced with \'cursorStyles.default\'. '); + + return this.cursorStyles.default; + }, +}); + +/** + * @name PIXI.interaction.InteractionManager#currentCursorStyle + * @static + * @type {string} + * @see PIXI.interaction.InteractionManager#cursorStyles + * @deprecated since 4.3.0 + */ +Object.defineProperty(interaction.InteractionManager, 'currentCursorStyle', { + set(value) + { + warn('Property currentCursorStyle has been removed.' + + 'See the currentCursorMode property, which works differently.'); + this.currentCursorMode = value; + }, + get() + { + warn('Property currentCursorStyle has been removed.' + + 'See the currentCursorMode property, which works differently.'); + + return this.currentCursorMode; + }, +}); diff --git a/src/extras/BitmapText.js b/src/extras/BitmapText.js index fb6c343..0a0e150 100644 --- a/src/extras/BitmapText.js +++ b/src/extras/BitmapText.js @@ -455,6 +455,73 @@ return this._textHeight; } + + /** + * Register a bitmap font with data and a texture. + * + * @static + * @param {XMLDocument} xml - The XML document data. + * @param {PIXI.Texture} texture - Texture with all symbols. + * @return {Object} Result font object with font, size, lineHeight and char fields. + */ + static registerFont(xml, texture) + { + const data = {}; + const info = xml.getElementsByTagName('info')[0]; + const common = xml.getElementsByTagName('common')[0]; + + data.font = info.getAttribute('face'); + data.size = parseInt(info.getAttribute('size'), 10); + data.lineHeight = parseInt(common.getAttribute('lineHeight'), 10); + data.chars = {}; + + // parse letters + const letters = xml.getElementsByTagName('char'); + + for (let i = 0; i < letters.length; i++) + { + const letter = letters[i]; + const charCode = parseInt(letter.getAttribute('id'), 10); + + const textureRect = new core.Rectangle( + parseInt(letter.getAttribute('x'), 10) + texture.frame.x, + parseInt(letter.getAttribute('y'), 10) + texture.frame.y, + parseInt(letter.getAttribute('width'), 10), + parseInt(letter.getAttribute('height'), 10) + ); + + data.chars[charCode] = { + xOffset: parseInt(letter.getAttribute('xoffset'), 10), + yOffset: parseInt(letter.getAttribute('yoffset'), 10), + xAdvance: parseInt(letter.getAttribute('xadvance'), 10), + kerning: {}, + texture: new core.Texture(texture.baseTexture, textureRect), + + }; + } + + // parse kernings + const kernings = xml.getElementsByTagName('kerning'); + + for (let i = 0; i < kernings.length; i++) + { + const kerning = kernings[i]; + const first = parseInt(kerning.getAttribute('first'), 10); + const second = parseInt(kerning.getAttribute('second'), 10); + const amount = parseInt(kerning.getAttribute('amount'), 10); + + if (data.chars[second]) + { + data.chars[second].kerning[first] = amount; + } + } + + // I'm leaving this as a temporary fix so we can test the bitmap fonts in v3 + // but it's very likely to change + BitmapText.fonts[data.font] = data; + + return data; + } } BitmapText.fonts = {}; diff --git a/src/index.js b/src/index.js index 59356cf..c553427 100644 --- a/src/index.js +++ b/src/index.js @@ -16,6 +16,10 @@ import * as particles from './particles'; import * as prepare from './prepare'; +// handle mixins now, after all code has been added, including deprecation +import { utils } from './core'; +utils.mixins.performMixins(); + export { accessibility, extract, diff --git a/src/interaction/InteractionManager.js b/src/interaction/InteractionManager.js index 7d51134..8baf13a 100644 --- a/src/interaction/InteractionManager.js +++ b/src/interaction/InteractionManager.js @@ -1,18 +1,20 @@ 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'; -// Mix interactiveTarget into core.DisplayObject.prototype -Object.assign( +// Mix interactiveTarget into core.DisplayObject.prototype, after deprecation has been handled +core.utils.mixins.delayMixin( core.DisplayObject.prototype, 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 +70,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 +101,6 @@ this.eventData = new InteractionEvent(); /** - * Tiny little interactiveData pool ! - * - * @member {PIXI.interaction.InteractionData[]} - */ - this.interactiveDataPool = []; - - /** * The DOM element to bind to. * * @private @@ -153,66 +155,12 @@ */ this.supportsPointerEvents = !!window.PointerEvent; - /** - * Are touch events being 'normalized' and converted into pointer events if pointer events are not supported - * For example, on a touch screen mobile device, a touchstart would also be emitted as a pointerdown - * - * @private - * @readonly - * @member {boolean} - */ - this.normalizeTouchEvents = !this.supportsPointerEvents && this.supportsTouchEvents; - - /** - * Are mouse events being 'normalized' and converted into pointer events if pointer events are not supported - * For example, on a desktop pc, a mousedown would also be emitted as a pointerdown - * - * @private - * @readonly - * @member {boolean} - */ - this.normalizeMouseEvents = !this.supportsPointerEvents && !MobileDevice.any; - // this will make it so that you don't have to call bind all the time /** * @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 +168,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,41 +199,32 @@ this.onPointerOver = this.onPointerOver.bind(this); /** - * @private - * @member {Function} + * Dictionary of how different cursor modes are handled. Strings are handled as CSS cursor + * values, objects are handled as dictionaries of CSS values for interactionDOMElement, + * and functions are called instead of changing the CSS. + * Default CSS cursor values are provided for 'default' and 'pointer' modes. + * @member {Object.)>} */ - this.onTouchStart = this.onTouchStart.bind(this); - this.processTouchStart = this.processTouchStart.bind(this); + this.cursorStyles = { + default: 'inherit', + pointer: 'pointer', + }; /** - * @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. - * - * @member {string} - * @default 'inherit' - */ - this.defaultCursorStyle = 'inherit'; - - /** - * The css style of the cursor that is being used. + * The mode of the cursor that is being used. + * The value of this is a key from the cursorStyles dictionary. * * @member {string} */ - this.currentCursorStyle = 'inherit'; + this.currentCursorMode = null; + + /** + * Internal cached let. + * + * @private + * @member {string} + */ + this.cursor = null; /** * Internal cached let. @@ -303,6 +249,7 @@ * object. * * @event mousedown + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -311,6 +258,7 @@ * on the display object. * * @event rightdown + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -319,6 +267,7 @@ * object. * * @event mouseup + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -327,6 +276,7 @@ * over the display object. * * @event rightup + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -335,6 +285,7 @@ * the display object. * * @event click + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -343,6 +294,7 @@ * and released on the display object. * * @event rightclick + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -352,6 +304,7 @@ * [mousedown]{@link PIXI.interaction.InteractionManager#event:mousedown}. * * @event mouseupoutside + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -361,6 +314,7 @@ * [rightdown]{@link PIXI.interaction.InteractionManager#event:rightdown}. * * @event rightupoutside + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -368,6 +322,7 @@ * Fired when a pointer device (usually a mouse) is moved while over the display object * * @event mousemove + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -375,6 +330,7 @@ * Fired when a pointer device (usually a mouse) is moved onto the display object * * @event mouseover + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -382,6 +338,7 @@ * Fired when a pointer device (usually a mouse) is moved off the display object * * @event mouseout + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -389,6 +346,7 @@ * Fired when a pointer device button is pressed on the display object. * * @event pointerdown + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -396,6 +354,14 @@ * Fired when a pointer device button is released over the display object. * * @event pointerup + * @type {PIXI.interaction.InteractionData} + * @memberof PIXI.interaction.InteractionManager# + */ + + /** + * Fired when the operating system cancels a pointer event + * + * @event pointercancel * @memberof PIXI.interaction.InteractionManager# */ @@ -403,6 +369,7 @@ * Fired when a pointer device button is pressed and released on the display object. * * @event pointertap + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -411,6 +378,7 @@ * registered a [pointerdown]{@link PIXI.interaction.InteractionManager#event:pointerdown}. * * @event pointerupoutside + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -418,6 +386,7 @@ * Fired when a pointer device is moved while over the display object * * @event pointermove + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -425,6 +394,7 @@ * Fired when a pointer device is moved onto the display object * * @event pointerover + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -432,6 +402,7 @@ * Fired when a pointer device is moved off the display object * * @event pointerout + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -439,6 +410,7 @@ * Fired when a touch point is placed on the display object. * * @event touchstart + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -446,6 +418,14 @@ * Fired when a touch point is removed from the display object. * * @event touchend + * @type {PIXI.interaction.InteractionData} + * @memberof PIXI.interaction.InteractionManager# + */ + + /** + * Fired when the operating system cancels a touch + * + * @event touchcancel * @memberof PIXI.interaction.InteractionManager# */ @@ -453,6 +433,7 @@ * Fired when a touch point is placed and removed from the display object. * * @event tap + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -461,6 +442,7 @@ * registered a [touchstart]{@link PIXI.interaction.InteractionManager#event:touchstart}. * * @event touchendoutside + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -468,6 +450,7 @@ * Fired when a touch point is moved along the display object. * * @event touchmove + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ } @@ -524,45 +507,30 @@ { window.document.addEventListener('pointermove', this.onPointerMove, true); this.interactionDOMElement.addEventListener('pointerdown', this.onPointerDown, true); - this.interactionDOMElement.addEventListener('pointerout', this.onPointerOut, true); + // pointerout is fired in addition to pointerup (for touch events) and pointercancel + // we already handle those, so for the purposes of what we do in onPointerOut, we only + // care about the pointerleave event + this.interactionDOMElement.addEventListener('pointerleave', 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; @@ -596,45 +564,26 @@ { window.document.removeEventListener('pointermove', this.onPointerMove, true); this.interactionDOMElement.removeEventListener('pointerdown', this.onPointerDown, true); - this.interactionDOMElement.removeEventListener('pointerout', this.onPointerOut, true); + this.interactionDOMElement.removeEventListener('pointerleave', 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) + 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.onPointerDown, true); + this.interactionDOMElement.removeEventListener('touchcancel', this.onPointerCancel, 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); - - 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 = null; @@ -664,7 +613,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; @@ -672,25 +621,80 @@ return; } - this.cursor = this.defaultCursorStyle; + this.cursor = null; // 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(); - - this.processInteractive(this.mouse.global, this.renderer._lastObjectRendered, this.processMouseOverOut, true); - - if (this.currentCursorStyle !== this.cursor) + for (const k in this.activeInteractionData) { - this.currentCursorStyle = this.cursor; - this.interactionDOMElement.style.cursor = this.cursor; + // eslint-disable-next-line no-prototype-builtins + if (this.activeInteractionData.hasOwnProperty(k)) + { + const interactionData = this.activeInteractionData[k]; + + if (interactionData.originalEvent && interactionData.pointerType !== 'touch') + { + const interactionEvent = this.configureInteractionEventForDOMEvent( + this.eventData, + interactionData.originalEvent, + interactionData + ); + + this.processInteractive( + interactionEvent, + this.renderer._lastObjectRendered, + this.processPointerOverOut, + true + ); + } + } } + this.setCursorMode(this.cursor); + // TODO } /** + * Sets the current cursor mode, handling any callbacks or CSS style changes. + * + * @param {string} mode - cursor mode, a key from the cursorStyles dictionary + */ + setCursorMode(mode) + { + mode = mode || 'default'; + // if the mode didn't actually change, bail early + if (this.currentCursorMode === mode) + { + return; + } + this.currentCursorMode = mode; + const style = this.cursorStyles[mode]; + + // only do things if there is a cursor style for it + if (style) + { + switch (typeof style) + { + case 'string': + // string styles are handled as cursor CSS + this.interactionDOMElement.style.cursor = style; + break; + case 'function': + // functions are just called, and passed the cursor mode + style(mode); + break; + case 'object': + // if it is an object, assume that it is a dictionary of CSS styles, + // apply it to the interactionDOMElement + Object.assign(this.interactionDOMElement.style, style); + break; + } + } + } + + /** * Dispatches an event on the display object that was interacted with * * @param {PIXI.Container|PIXI.Sprite|PIXI.extras.TilingSprite} displayObject - the display object in question @@ -748,22 +752,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 @@ -787,9 +795,8 @@ { interactiveParent = false; } - - // it has a mask! Then lets hit test that before continuing.. - if (hitTest && displayObject._mask) + // it has a mask! Then lets hit test that before continuing + else if (hitTest && displayObject._mask) { if (!displayObject._mask.containsPoint(point)) { @@ -797,14 +804,7 @@ } } - // it has a filterArea! Same as mask but easier, its a rectangle - if (hitTest && displayObject.filterArea) - { - if (!displayObject.filterArea.contains(point.x, point.y)) - { - hitTest = false; - } - } + let keepHitTestingAfterChildren = hitTest; // ** FREE TIP **! If an object is not interactive or has no buttons in it // (such as a game scene!) set interactiveChildren to false for that displayObject. @@ -818,7 +818,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 @@ -838,15 +838,20 @@ // This means we no longer need to hit test anything else. We still need to run // through all objects, but we don't need to perform any hit tests. - // { - hitTest = false; - // } + keepHitTestingAfterChildren = false; + + if (child.interactive) + { + hitTest = false; + } // we can break now as we have hit an object. } } } + hitTest = keepHitTestingAfterChildren; + // no point running this if the item is not interactive or does not have an interactive parent. if (interactive) { @@ -868,14 +873,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 +886,175 @@ } /** - * 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].isNormalized) { - 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); + if (!displayObject.trackedPointers[id]) + { + displayObject.trackedPointers[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.trackedPointers[id].rightDown = true; + } + else + { + displayObject.trackedPointers[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.trackedPointers[id] !== undefined) + { + delete displayObject.trackedPointers[id]; + this.dispatchEvent(displayObject, 'pointercancel', interactionEvent); + + if (e.type === 'touchcancel' || e.pointerType === 'touch') + { + this.dispatchEvent(displayObject, 'touchcancel', interactionEvent); + } } } @@ -1159,42 +1066,94 @@ */ 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) { - if (hit) - { - this.dispatchEvent(displayObject, 'pointerup', this.eventData); + const e = interactionEvent.data.originalEvent; - if (displayObject._pointerDown) + const id = interactionEvent.data.identifier; + + const trackingData = displayObject.trackedPointers[id]; + + const isTouch = (e.type === 'touchend' || e.pointerType === 'touch'); + + const isMouse = (e.type.indexOf('mouse') === 0 || e.pointerType === 'mouse'); + + // 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) { - displayObject._pointerDown = false; - this.dispatchEvent(displayObject, 'pointertap', this.eventData); + 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); + } + // update the down state of the tracking data + if (trackingData) + { + if (isRightButton) + { + trackingData.rightDown = hit; + } + else + { + trackingData.leftDown = hit; + } } } - else if (displayObject._pointerDown) + + // Pointers and Touches, and Mouse + if (hit) { - displayObject._pointerDown = false; - this.dispatchEvent(displayObject, 'pointerupoutside', this.eventData); + this.dispatchEvent(displayObject, 'pointerup', interactionEvent); + if (isTouch) this.dispatchEvent(displayObject, 'touchend', interactionEvent); + + if (trackingData) + { + this.dispatchEvent(displayObject, 'pointertap', interactionEvent); + if (isTouch) + { + this.dispatchEvent(displayObject, 'tap', interactionEvent); + // touches are no longer over (if they ever were) when we get the touchend + // so we should ensure that we don't keep pretending that they are + trackingData.over = false; + } + } + } + else if (trackingData) + { + this.dispatchEvent(displayObject, 'pointerupoutside', interactionEvent); + if (isTouch) this.dispatchEvent(displayObject, 'touchendoutside', interactionEvent); + } + // Only remove the tracking data if there is no over/down state still associated with it + if (trackingData && trackingData.none) + { + delete displayObject.trackedPointers[id]; } } @@ -1202,39 +1161,78 @@ * 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 = null; + } - 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') + { + this.setCursorMode(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 (isMouse) { - 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 +1240,94 @@ * 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.setCursorMode(null); + } - 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'); + + let trackingData = displayObject.trackedPointers[id]; + + // if we just moused over the display object, then we need to track that state + if (hit && !trackingData) + { + trackingData = displayObject.trackedPointers[id] = new InteractionTrackingData(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); + } + } + + // only change the cursor if it has not already been changed (by something deeper in the + // display tree) + if (isMouse && this.cursor === null) + { + this.cursor = displayObject.cursor; } } - 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); + } + // if there is no mouse down information for the pointer, then it is safe to delete + if (trackingData.none) + { + delete displayObject.trackedPointers[id]; + } } } @@ -1287,253 +1335,154 @@ * 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 && pointerEvent.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; + + // mark the touch as normalized, just so that we know we did it + touch.isNormalized = true; + + normalizedEvents.push(touch); + } } - else if (this.normalizeMouseEvents) + // apparently PointerEvent subclasses MouseEvent, so yay + else if (event instanceof MouseEvent && (!this.supportsPointerEvents || !(event instanceof window.PointerEvent))) { if (typeof event.isPrimary === 'undefined') event.isPrimary = true; if (typeof event.width === 'undefined') event.width = 1; @@ -1541,10 +1490,21 @@ 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; + + // mark the mouse event as normalized, just so that we know we did it + event.isNormalized = true; + + normalizedEvents.push(event); } + else + { + normalizedEvents.push(event); + } + + return normalizedEvents; } /** @@ -1563,30 +1523,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 +1542,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..6a1f69e --- /dev/null +++ b/src/interaction/InteractionTrackingData.js @@ -0,0 +1,147 @@ +/** + * 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 inactive (not over or down)? + * + * @member {number} + * @memberof PIXI.interaction.InteractionTrackingData# + */ + get none() + { + return this._flags === this.constructor.FLAGS.NONE; + } + + /** + * 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..d252fba 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 * ); */ @@ -38,68 +38,56 @@ hitArea: null, /** - * If enabled, the mouse cursor will change when hovered over the displayObject if it is interactive + * If enabled, the mouse cursor use the pointer behavior when hovered over the displayObject if it is interactive + * Setting this changes the 'cursor' property to `'pointer'`. * - * @inner {boolean} + * @member {boolean} + * @memberof PIXI.interaction.interactiveTarget# */ - buttonMode: false, + get buttonMode() + { + return this.cursor === 'pointer'; + }, + set buttonMode(value) + { + if (value) + { + this.cursor = 'pointer'; + } + else if (this.cursor === 'pointer') + { + this.cursor = null; + } + }, /** - * If buttonMode is enabled, this defines what CSS cursor property is used when the mouse cursor - * is hovered over the displayObject + * This defines what cursor mode is used when the mouse cursor + * is hovered over the displayObject. * * @see https://developer.mozilla.org/en/docs/Web/CSS/cursor * * @inner {string} */ - defaultCursor: 'pointer', - - // some internal checks.. - /** - * Internal check to detect if the mouse cursor is hovered over the displayObject - * - * @inner {boolean} - * @private - */ - _over: false, + cursor: null, /** - * Internal check to detect if the left mouse button is pressed on the displayObject + * Internal set of all active pointers, by identifier * - * @inner {boolean} + * @member {Map} + * @memberof PIXI.interaction.interactiveTarget# * @private */ - _isLeftDown: false, + get trackedPointers() + { + if (this._trackedPointers === undefined) this._trackedPointers = {}; + + return this._trackedPointers; + }, /** - * Internal check to detect if the right mouse button is pressed on the displayObject + * Map of all tracked pointers, by identifier. Use trackedPointers to access. * - * @inner {boolean} - * @private + * @private {Map} */ - _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, + _trackedPointers: undefined, }; diff --git a/src/loaders/bitmapFontParser.js b/src/loaders/bitmapFontParser.js index cc2618c..f4cf996 100644 --- a/src/loaders/bitmapFontParser.js +++ b/src/loaders/bitmapFontParser.js @@ -1,63 +1,19 @@ import * as path from 'path'; -import { Rectangle, Texture, utils } from '../core'; +import { utils } from '../core'; import { Resource } from 'resource-loader'; import { BitmapText } from '../extras'; +/** + * Register a BitmapText font from loader resource. + * + * @function parseBitmapFontData + * @memberof PIXI.loaders + * @param {PIXI.loaders.Resource} resource - Loader resource. + * @param {PIXI.Texture} texture - Reference to texture. + */ export function parse(resource, texture) { - const data = {}; - const info = resource.data.getElementsByTagName('info')[0]; - const common = resource.data.getElementsByTagName('common')[0]; - - data.font = info.getAttribute('face'); - data.size = parseInt(info.getAttribute('size'), 10); - data.lineHeight = parseInt(common.getAttribute('lineHeight'), 10); - data.chars = {}; - - // parse letters - const letters = resource.data.getElementsByTagName('char'); - - for (let i = 0; i < letters.length; i++) - { - const charCode = parseInt(letters[i].getAttribute('id'), 10); - - const textureRect = new Rectangle( - parseInt(letters[i].getAttribute('x'), 10) + texture.frame.x, - parseInt(letters[i].getAttribute('y'), 10) + texture.frame.y, - parseInt(letters[i].getAttribute('width'), 10), - parseInt(letters[i].getAttribute('height'), 10) - ); - - data.chars[charCode] = { - xOffset: parseInt(letters[i].getAttribute('xoffset'), 10), - yOffset: parseInt(letters[i].getAttribute('yoffset'), 10), - xAdvance: parseInt(letters[i].getAttribute('xadvance'), 10), - kerning: {}, - texture: new Texture(texture.baseTexture, textureRect), - - }; - } - - // parse kernings - const kernings = resource.data.getElementsByTagName('kerning'); - - for (let i = 0; i < kernings.length; i++) - { - const first = parseInt(kernings[i].getAttribute('first'), 10); - const second = parseInt(kernings[i].getAttribute('second'), 10); - const amount = parseInt(kernings[i].getAttribute('amount'), 10); - - if (data.chars[second]) - { - data.chars[second].kerning[first] = amount; - } - } - - resource.bitmapFont = data; - - // I'm leaving this as a temporary fix so we can test the bitmap fonts in v3 - // but it's very likely to change - BitmapText.fonts[data.font] = data; + resource.bitmapFont = BitmapText.registerFont(resource.data, texture); } export default function () diff --git a/src/loaders/index.js b/src/loaders/index.js index 8942436..5189793 100644 --- a/src/loaders/index.js +++ b/src/loaders/index.js @@ -5,4 +5,11 @@ export { default as bitmapFontParser, parse as parseBitmapFontData } from './bitmapFontParser'; export { default as spritesheetParser } from './spritesheetParser'; export { default as textureParser } from './textureParser'; + +/** + * Reference to **resource-loader**'s Resource class. + * See https://github.com/englercj/resource-loader + * @class Resource + * @memberof PIXI.loaders + */ export { Resource } from 'resource-loader'; diff --git a/src/loaders/loader.js b/src/loaders/loader.js index 0a670cb..767a411 100644 --- a/src/loaders/loader.js +++ b/src/loaders/loader.js @@ -7,20 +7,47 @@ /** * - * The new loader, extends Resource Loader by Chad Engler : https://github.com/englercj/resource-loader + * The new loader, extends Resource Loader by Chad Engler: https://github.com/englercj/resource-loader * * ```js - * let loader = PIXI.loader; // pixi exposes a premade instance for you to use. + * const loader = PIXI.loader; // pixi exposes a premade instance for you to use. * //or - * let loader = new PIXI.loaders.Loader(); // you can also create your own if you want + * const loader = new PIXI.loaders.Loader(); // you can also create your own if you want * - * loader.add('bunny', 'data/bunny.png'); - * loader.add('spaceship', 'assets/spritesheet.json'); + * const sprites = {}; + * + * // Chainable `add` to enqueue a resource + * loader.add('bunny', 'data/bunny.png') + * .add('spaceship', 'assets/spritesheet.json'); * loader.add('scoreFont', 'assets/score.fnt'); * - * loader.once('complete',onAssetsLoaded); + * // Chainable `pre` to add a middleware that runs for each resource, *before* loading that resource. + * // This is useful to implement custom caching modules (using filesystem, indexeddb, memory, etc). + * loader.pre(cachingMiddleware); * - * loader.load(); + * // Chainable `use` to add a middleware that runs for each resource, *after* loading that resource. + * // This is useful to implement custom parsing modules (like spritesheet parsers, spine parser, etc). + * loader.use(parsingMiddleware); + * + * // The `load` method loads the queue of resources, and calls the passed in callback called once all + * // resources have loaded. + * loader.load((loader, resources) => { + * // resources is an object where the key is the name of the resource loaded and the value is the resource object. + * // They have a couple default properties: + * // - `url`: The URL that the resource was loaded from + * // - `error`: The error that happened when trying to load (if any) + * // - `data`: The raw data that was loaded + * // also may contain other properties based on the middleware that runs. + * sprites.bunny = new PIXI.TilingSprite(resources.bunny.texture); + * sprites.spaceship = new PIXI.TilingSprite(resources.spaceship.texture); + * sprites.scoreFont = new PIXI.TilingSprite(resources.scoreFont.texture); + * }); + * + * // throughout the process multiple signals can be dispatched. + * loader.onProgress.add(() => {}); // called once per loaded/errored file + * loader.onError.add(() => {}); // called once per errored file + * loader.onLoad.add(() => {}); // called once per loaded file + * loader.onComplete.add(() => {}); // called once when the queued resources all load. * ``` * * @see https://github.com/englercj/resource-loader diff --git a/src/loaders/spritesheetParser.js b/src/loaders/spritesheetParser.js index 7570332..e57dfb2 100644 --- a/src/loaders/spritesheetParser.js +++ b/src/loaders/spritesheetParser.js @@ -1,8 +1,6 @@ import { Resource } from 'resource-loader'; import path from 'path'; -import * as core from '../core'; - -const BATCH_SIZE = 1000; +import { Spritesheet } from '../core'; export default function () { @@ -43,134 +41,18 @@ // load the image for this sheet this.add(imageResourceName, resourcePath, loadOptions, function onImageLoad(res) { - resource.textures = {}; + const spritesheet = new Spritesheet( + res.texture.baseTexture, + resource.data, + resource.url + ); - const frames = resource.data.frames; - const frameKeys = Object.keys(frames); - const baseTexture = res.texture.baseTexture; - const scale = resource.data.meta.scale; - - // Use a defaultValue of `null` to check if a url-based resolution is set - let resolution = core.utils.getResolutionOfUrl(resource.url, null); - - // No resolution found via URL - if (resolution === null) + spritesheet.parse(() => { - // Use the scale value or default to 1 - resolution = scale !== undefined ? scale : 1; - } - - // For non-1 resolutions, update baseTexture - if (resolution !== 1) - { - baseTexture.resolution = resolution; - baseTexture.update(); - } - - let batchIndex = 0; - - function processFrames(initialFrameIndex, maxFrames) - { - let frameIndex = initialFrameIndex; - - while (frameIndex - initialFrameIndex < maxFrames && frameIndex < frameKeys.length) - { - const i = frameKeys[frameIndex]; - const rect = frames[i].frame; - - if (rect) - { - let frame = null; - let trim = null; - const orig = new core.Rectangle( - 0, - 0, - frames[i].sourceSize.w / resolution, - frames[i].sourceSize.h / resolution - ); - - if (frames[i].rotated) - { - frame = new core.Rectangle( - rect.x / resolution, - rect.y / resolution, - rect.h / resolution, - rect.w / resolution - ); - } - else - { - frame = new core.Rectangle( - rect.x / resolution, - rect.y / resolution, - rect.w / resolution, - rect.h / resolution - ); - } - - // Check to see if the sprite is trimmed - if (frames[i].trimmed) - { - trim = new core.Rectangle( - frames[i].spriteSourceSize.x / resolution, - frames[i].spriteSourceSize.y / resolution, - rect.w / resolution, - rect.h / resolution - ); - } - - resource.textures[i] = new core.Texture( - baseTexture, - frame, - orig, - trim, - frames[i].rotated ? 2 : 0 - ); - - // lets also add the frame to pixi's global cache for fromFrame and fromImage functions - core.utils.TextureCache[i] = resource.textures[i]; - } - - frameIndex++; - } - } - - function shouldProcessNextBatch() - { - return batchIndex * BATCH_SIZE < frameKeys.length; - } - - function processNextBatch(done) - { - processFrames(batchIndex * BATCH_SIZE, BATCH_SIZE); - batchIndex++; - setTimeout(done, 0); - } - - function iteration() - { - processNextBatch(() => - { - if (shouldProcessNextBatch()) - { - iteration(); - } - else - { - next(); - } - }); - } - - if (frameKeys.length <= BATCH_SIZE) - { - processFrames(0, BATCH_SIZE); + resource.spritesheet = spritesheet; + resource.textures = spritesheet.textures; next(); - } - else - { - iteration(); - } + }); }); }; } diff --git a/src/loaders/textureParser.js b/src/loaders/textureParser.js index 5398a7f..480edc1 100644 --- a/src/loaders/textureParser.js +++ b/src/loaders/textureParser.js @@ -1,5 +1,5 @@ -import * as core from '../core'; import { Resource } from 'resource-loader'; +import Texture from '../core/textures/Texture'; export default function () { @@ -8,23 +8,12 @@ // create a new texture if the data is an Image object if (resource.data && resource.type === Resource.TYPE.IMAGE) { - const baseTexture = new core.BaseTexture(resource.data, null, core.utils.getResolutionOfUrl(resource.url)); - - baseTexture.imageUrl = resource.url; - resource.texture = new core.Texture(baseTexture); - - // lets also add the frame to pixi's global cache for fromFrame and fromImage fucntions - core.utils.BaseTextureCache[resource.name] = baseTexture; - core.utils.TextureCache[resource.name] = resource.texture; - - // also add references by url if they are different. - if (resource.name !== resource.url) - { - core.utils.BaseTextureCache[resource.url] = baseTexture; - core.utils.TextureCache[resource.url] = resource.texture; - } + resource.texture = Texture.fromLoader( + resource.data, + resource.url, + resource.name + ); } - next(); }; } diff --git a/test/core/Container.js b/test/core/Container.js index 8a10b40..d11be33 100644 --- a/test/core/Container.js +++ b/test/core/Container.js @@ -1,5 +1,33 @@ 'use strict'; +function testAddChild(fn) +{ + fn(function (container, obj) + { + container.addChild(obj); + }); + fn(function (container, obj) + { + container.addChildAt(obj); + }); +} + +function testRemoveChild(fn) +{ + fn(function (container, obj) + { + container.removeChild(obj); + }); + fn(function (container, obj) + { + container.removeChildAt(container.children.indexOf(obj)); + }); + fn(function (container, obj) + { + container.removeChildren(container.children.indexOf(obj), container.children.indexOf(obj) + 1); + }); +} + describe('PIXI.Container', function () { describe('parent', function () @@ -70,6 +98,49 @@ expect(spy).to.have.been.called; expect(spy).to.have.been.calledWith(0); }); + + it('should flag child transform and container bounds for recalculation', testAddChild(function (mockAddChild) + { + const container = new PIXI.Container(); + const child = new PIXI.Container(); + + container.getBounds(); + child.getBounds(); + + const boundsID = container._boundsID; + const childParentID = child.transform._parentID; + + mockAddChild(container, child); + + expect(boundsID).to.not.be.equals(container._boundsID); + expect(childParentID).to.not.be.equals(child.transform._parentID); + })); + + it('should recalculate added child correctly', testAddChild(function (mockAddChild) + { + const parent = new PIXI.Container(); + const container = new PIXI.Container(); + const graphics = new PIXI.Graphics(); + + parent.addChild(container); + + graphics.drawRect(0, 0, 10, 10); + container.position.set(100, 200); + container.updateTransform(); + + graphics.getBounds(); + // Oops, that can happen sometimes! + graphics.transform._parentID = container.transform._worldID + 1; + + mockAddChild(container, graphics); + + const bounds = graphics.getBounds(); + + expect(bounds.x).to.be.equal(100); + expect(bounds.y).to.be.equal(200); + expect(bounds.width).to.be.equal(10); + expect(bounds.height).to.be.equal(10); + })); }); describe('removeChildAt', function () @@ -202,6 +273,44 @@ expect(spy).to.have.been.called; expect(spy).to.have.been.calledWith(0); }); + + it('should flag transform for recalculation', testRemoveChild(function (mockRemoveChild) + { + const container = new PIXI.Container(); + const child = new PIXI.Container(); + + container.addChild(child); + container.getBounds(); + + const childParentID = child.transform._parentID; + const boundsID = container._boundsID; + + mockRemoveChild(container, child); + + expect(childParentID).to.not.be.equals(child.transform._parentID); + expect(boundsID).to.not.be.equals(container._boundsID); + })); + + it('should recalculate removed child correctly', testRemoveChild(function (mockRemoveChild) + { + const parent = new PIXI.Container(); + const container = new PIXI.Container(); + const graphics = new PIXI.Graphics(); + + parent.addChild(container); + + graphics.drawRect(0, 0, 10, 10); + container.position.set(100, 200); + container.addChild(graphics); + graphics.getBounds(); + + mockRemoveChild(container, graphics); + + const bounds = graphics.getBounds(); + + expect(bounds.x).to.be.equal(0); + expect(bounds.y).to.be.equal(0); + })); }); describe('getChildIndex', function () diff --git a/test/core/Graphics.js b/test/core/Graphics.js index 98c7584..11e47a0 100644 --- a/test/core/Graphics.js +++ b/test/core/Graphics.js @@ -1,5 +1,8 @@ 'use strict'; +const MockPointer = require('../interaction/MockPointer'); +const withGL = require('../withGL'); + describe('PIXI.Graphics', function () { describe('constructor', function () @@ -218,4 +221,153 @@ expect(spy).to.have.been.calledOnce; }); }); + + describe('mask', function () + { + it('should trigger interaction callback when no mask present', function () + { + const stage = new PIXI.Container(); + const pointer = new MockPointer(stage); + const graphics = new PIXI.Graphics(); + const mask = new PIXI.Graphics(); + const spy = sinon.spy(); + + graphics.interactive = true; + graphics.beginFill(0xFF0000); + graphics.drawRect(0, 0, 50, 50); + graphics.on('click', spy); + stage.addChild(graphics); + mask.beginFill(); + mask.drawRect(0, 0, 50, 50); + graphics.mask = mask; + + pointer.click(10, 10); + + expect(spy).to.have.been.calledOnce; + }); + it('should trigger interaction callback when mask uses beginFill', function () + { + const stage = new PIXI.Container(); + const pointer = new MockPointer(stage); + const graphics = new PIXI.Graphics(); + const mask = new PIXI.Graphics(); + const spy = sinon.spy(); + + graphics.interactive = true; + graphics.beginFill(0xFF0000); + graphics.drawRect(0, 0, 50, 50); + graphics.on('click', spy); + stage.addChild(graphics); + mask.beginFill(); + mask.drawRect(0, 0, 50, 50); + graphics.mask = mask; + + pointer.click(10, 10); + + expect(spy).to.have.been.calledOnce; + }); + + it('should not trigger interaction callback when mask doesn\'t use beginFill', function () + { + const stage = new PIXI.Container(); + const pointer = new MockPointer(stage); + const graphics = new PIXI.Graphics(); + const mask = new PIXI.Graphics(); + const spy = sinon.spy(); + + graphics.interactive = true; + graphics.beginFill(0xFF0000); + graphics.drawRect(0, 0, 50, 50); + graphics.on('click', spy); + stage.addChild(graphics); + mask.drawRect(0, 0, 50, 50); + graphics.mask = mask; + + pointer.click(10, 10); + + expect(spy).to.have.not.been.called; + }); + + it('should trigger interaction callback when mask doesn\'t use beginFill but hitArea is defined', function () + { + const stage = new PIXI.Container(); + const pointer = new MockPointer(stage); + const graphics = new PIXI.Graphics(); + const mask = new PIXI.Graphics(); + const spy = sinon.spy(); + + graphics.interactive = true; + graphics.beginFill(0xFF0000); + graphics.hitArea = new PIXI.Rectangle(0, 0, 50, 50); + graphics.drawRect(0, 0, 50, 50); + graphics.on('click', spy); + stage.addChild(graphics); + mask.drawRect(0, 0, 50, 50); + graphics.mask = mask; + + pointer.click(10, 10); + + expect(spy).to.have.been.calledOnce; + }); + + it('should trigger interaction callback when mask is a sprite', function () + { + const stage = new PIXI.Container(); + const pointer = new MockPointer(stage); + const graphics = new PIXI.Graphics(); + const mask = new PIXI.Graphics(); + const spy = sinon.spy(); + + graphics.interactive = true; + graphics.beginFill(0xFF0000); + graphics.drawRect(0, 0, 50, 50); + graphics.on('click', spy); + stage.addChild(graphics); + mask.drawRect(0, 0, 50, 50); + graphics.mask = new PIXI.Sprite(mask.generateCanvasTexture()); + + pointer.click(10, 10); + + expect(spy).to.have.been.calledOnce; + }); + + it('should calculate tint, alpha and blendMode of fastRect correctly', withGL(function () + { + const renderer = new PIXI.WebGLRenderer(200, 200, {}); + + try + { + const graphics = new PIXI.Graphics(); + + graphics.beginFill(0x102030, 0.6); + graphics.drawRect(2, 3, 100, 100); + graphics.endFill(); + graphics.tint = 0x101010; + graphics.blendMode = 2; + graphics.alpha = 0.3; + + renderer.render(graphics); + + expect(graphics.isFastRect()).to.be.true; + + const sprite = graphics._spriteRect; + + expect(sprite).to.not.be.equals(null); + expect(sprite.worldAlpha).to.equals(0.18); + expect(sprite.blendMode).to.equals(2); + expect(sprite.tint).to.equals(0x010203); + + const bounds = sprite.getBounds(); + + expect(bounds.x).to.equals(2); + expect(bounds.y).to.equals(3); + expect(bounds.width).to.equals(100); + expect(bounds.height).to.equals(100); + } + finally + { + renderer.destroy(); + } + })); + }); }); diff --git a/test/core/Spritesheet.js b/test/core/Spritesheet.js new file mode 100644 index 0000000..283adf5 --- /dev/null +++ b/test/core/Spritesheet.js @@ -0,0 +1,89 @@ +'use strict'; + +const path = require('path'); + +describe('PIXI.Spritesheet', function () +{ + before(function () + { + this.resources = path.join(__dirname, 'resources'); + this.validate = function (spritesheet, done) + { + spritesheet.parse(function (textures) + { + const id = 'goldmine_10_5.png'; + + expect(Object.keys(textures).length).to.equal(1); + expect(Object.keys(spritesheet.textures).length).to.equal(1); + expect(textures[id]).to.be.an.instanceof(PIXI.Texture); + expect(textures[id].width).to.equal(spritesheet.data.frames[id].frame.w / spritesheet.resolution); + expect(textures[id].height).to.equal(spritesheet.data.frames[id].frame.h / spritesheet.resolution); + spritesheet.destroy(true); + expect(spritesheet.textures).to.be.null; + expect(spritesheet.baseTexture).to.be.null; + done(); + }); + }; + }); + + it('should exist on PIXI', function () + { + expect(PIXI.Spritesheet).to.be.a.function; + expect(PIXI.Spritesheet.BATCH_SIZE).to.be.a.number; + }); + + it('should create an instance', function () + { + const baseTexture = new PIXI.BaseTexture(); + const data = { + frames: {}, + meta: {}, + }; + const spritesheet = new PIXI.Spritesheet(baseTexture, data); + + expect(spritesheet.data).to.equal(data); + expect(spritesheet.baseTexture).to.equal(baseTexture); + expect(spritesheet.resolution).to.equal(1); + + spritesheet.destroy(true); + }); + + it('should create instance with scale resolution', function (done) + { + const data = require(path.resolve(this.resources, 'building1.json')); // eslint-disable-line global-require + const image = new Image(); + + image.src = path.join(this.resources, data.meta.image); + image.onload = () => + { + const baseTexture = new PIXI.BaseTexture(image, null, 1); + const spritesheet = new PIXI.Spritesheet(baseTexture, data); + + expect(data).to.be.an.object; + expect(data.meta.image).to.equal('building1.png'); + expect(spritesheet.resolution).to.equal(0.5); + + this.validate(spritesheet, done); + }; + }); + + it('should create instance with filename resolution', function (done) + { + const uri = path.resolve(this.resources, 'building1@2x.json'); + const data = require(uri); // eslint-disable-line global-require + const image = new Image(); + + image.src = path.join(this.resources, data.meta.image); + image.onload = () => + { + const baseTexture = new PIXI.BaseTexture(image, null, 1); + const spritesheet = new PIXI.Spritesheet(baseTexture, data, uri); + + expect(data).to.be.an.object; + expect(data.meta.image).to.equal('building1@2x.png'); + expect(spritesheet.resolution).to.equal(2); + + this.validate(spritesheet, done); + }; + }); +}); diff --git a/test/core/Texture.js b/test/core/Texture.js new file mode 100644 index 0000000..8c4ef98 --- /dev/null +++ b/test/core/Texture.js @@ -0,0 +1,19 @@ +'use strict'; + +describe('PIXI.Texture', function () +{ + it('should register Texture from Loader', function () + { + const URL = 'foo.png'; + const NAME = 'bar'; + const image = new Image(); + + const texture = PIXI.Texture.fromLoader(image, URL, NAME); + + expect(texture.baseTexture.imageUrl).to.equal('foo.png'); + expect(PIXI.utils.TextureCache[NAME]).to.equal(texture); + expect(PIXI.utils.BaseTextureCache[NAME]).to.equal(texture.baseTexture); + expect(PIXI.utils.TextureCache[URL]).to.equal(texture); + expect(PIXI.utils.BaseTextureCache[URL]).to.equal(texture.baseTexture); + }); +}); diff --git a/test/core/WebGLRenderer.js b/test/core/WebGLRenderer.js index 9bc3bc2..3d6cf98 100644 --- a/test/core/WebGLRenderer.js +++ b/test/core/WebGLRenderer.js @@ -1,21 +1,21 @@ 'use strict'; -function isWebGLSupported(fn) -{ - return PIXI.utils.isWebGLSupported() ? fn : function () {}; // eslint-disable-line no-empty-function -} +const withGL = require('../withGL'); -describe('PIXI.WebGLRenderer', isWebGLSupported(function () +describe('PIXI.WebGLRenderer', function () { - it('setting option legacy should disable VAOs and set minimum SPRITE_MAX_TEXTURES to 1', function () + it('setting option legacy should disable VAOs and SPRITE_MAX_TEXTURES', withGL(function () { const renderer = new PIXI.WebGLRenderer(1, 1, { legacy: true }); - - expect(PIXI.glCore.VertexArrayObject.FORCE_NATIVE).to.equal(true); - expect(renderer.plugins.sprite.MAX_TEXTURES).to.equal(1); - - renderer.destroy(); - }); -})); - + try + { + expect(PIXI.glCore.VertexArrayObject.FORCE_NATIVE).to.equal(true); + expect(renderer.plugins.sprite.MAX_TEXTURES).to.equal(1); + } + finally + { + renderer.destroy(); + } + })); +}); diff --git a/test/core/filters.js b/test/core/filters.js new file mode 100644 index 0000000..df459f6 --- /dev/null +++ b/test/core/filters.js @@ -0,0 +1,21 @@ +'use strict'; + +describe('PIXI.filters', function () +{ + it('should correctly form uniformData', function () + { + const sprite = new PIXI.Sprite(PIXI.Texture.EMPTY); + const displ = new PIXI.filters.DisplacementFilter(sprite); + + expect(!!displ.uniformData.scale).to.be.true; + expect(!!displ.uniformData.filterMatrix).to.be.true; + expect(!!displ.uniformData.mapSampler).to.be.true; + // it does have filterClamp, but it is handled by FilterManager + expect(!!displ.uniformData.filterClamp).to.be.false; + + const fxaa = new PIXI.filters.FXAAFilter(); + + // it does have filterArea, but it is handled by FilterManager + expect(!!fxaa.uniformData.filterArea).to.be.false; + }); +}); diff --git a/test/core/index.js b/test/core/index.js index 63a8f21..8530131 100755 --- a/test/core/index.js +++ b/test/core/index.js @@ -7,6 +7,7 @@ require('./DisplayObject'); require('./getLocalBounds'); require('./Sprite'); +require('./Spritesheet'); require('./TilingSprite'); require('./TextStyle'); require('./Text'); @@ -25,3 +26,5 @@ require('./SpriteRenderer'); require('./WebGLRenderer'); require('./Ellipse'); +require('./Texture'); +require('./filters'); diff --git a/test/core/resources/building1.json b/test/core/resources/building1.json new file mode 100755 index 0000000..03fa5c1 --- /dev/null +++ b/test/core/resources/building1.json @@ -0,0 +1,21 @@ +{"frames": { + +"goldmine_10_5.png": +{ + "frame": {"x":1,"y":1,"w":95,"h":115}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":95,"h":115}, + "sourceSize": {"w":95,"h":115}, + "pivot": {"x":0.5,"y":0.5} +}}, +"meta": { + "app": "http://www.codeandweb.com/texturepacker", + "version": "1.0", + "image": "building1.png", + "format": "RGBA8888", + "size": {"w":128,"h":128}, + "scale": "0.5", + "smartupdate": "$TexturePacker:SmartUpdate:d7a5e54c8f8a3fecd508baf190c44807:99a0e3a4dc0f441e4aad77614191ab38:6046b8eb706ddefaa771c33ceb7cb6d5$" +} +} diff --git a/test/core/resources/building1.png b/test/core/resources/building1.png new file mode 100755 index 0000000..7e1e114 --- /dev/null +++ b/test/core/resources/building1.png Binary files differ diff --git a/test/core/resources/building1@2x.json b/test/core/resources/building1@2x.json new file mode 100755 index 0000000..24e25ff --- /dev/null +++ b/test/core/resources/building1@2x.json @@ -0,0 +1,21 @@ +{"frames": { + +"goldmine_10_5.png": +{ + "frame": {"x":1,"y":1,"w":190,"h":229}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":190,"h":229}, + "sourceSize": {"w":190,"h":229}, + "pivot": {"x":0.5,"y":0.5} +}}, +"meta": { + "app": "http://www.codeandweb.com/texturepacker", + "version": "1.0", + "image": "building1@2x.png", + "format": "RGBA8888", + "size": {"w":256,"h":256}, + "scale": "1", + "smartupdate": "$TexturePacker:SmartUpdate:d7a5e54c8f8a3fecd508baf190c44807:99a0e3a4dc0f441e4aad77614191ab38:6046b8eb706ddefaa771c33ceb7cb6d5$" +} +} diff --git a/test/core/resources/building1@2x.png b/test/core/resources/building1@2x.png new file mode 100755 index 0000000..d5ecd04 --- /dev/null +++ b/test/core/resources/building1@2x.png Binary files differ diff --git a/test/core/util.js b/test/core/util.js index dce6003..239aaeb 100755 --- a/test/core/util.js +++ b/test/core/util.js @@ -47,6 +47,11 @@ .to.be.a('function'); }); + it('should calculate correctly', function () + { + expect(PIXI.utils.rgb2hex([0.3, 0.2, 0.1])).to.equals(0x4c3319); + }); + // it('should properly convert rgb array to hex color string'); }); diff --git a/package.json b/package.json index 6a366ba..71ade15 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pixi.js", - "version": "4.3.4", + "version": "4.3.5", "description": "Pixi.js is a fast lightweight 2D library that works across all devices.", "author": "Mat Groves", "contributors": [ @@ -61,7 +61,7 @@ "ismobilejs": "^0.4.0", "object-assign": "^4.0.1", "pixi-gl-core": "^1.0.3", - "resource-loader": "^2.0.4" + "resource-loader": "^2.0.6" }, "devDependencies": { "babel-cli": "^6.18.0", @@ -70,9 +70,9 @@ "babel-preset-es2015": "^6.14.0", "babelify": "^7.3.0", "del": "^2.2.0", - "electron-prebuilt": "^1.3.2", + "electron": "^1.4.15", "eslint": "^3.5.0", - "floss": "^1.2.0", + "floss": "^2.0.1", "gh-pages": "^0.11.0", "jaguarjs-jsdoc": "^1.0.1", "js-md5": "^0.4.1", diff --git a/src/accessibility/AccessibilityManager.js b/src/accessibility/AccessibilityManager.js index df2716a..f16bcb1 100644 --- a/src/accessibility/AccessibilityManager.js +++ b/src/accessibility/AccessibilityManager.js @@ -3,7 +3,7 @@ import accessibleTarget from './accessibleTarget'; // add some extra variables to the container.. -Object.assign( +core.utils.mixins.delayMixin( core.DisplayObject.prototype, accessibleTarget ); diff --git a/src/core/Application.js b/src/core/Application.js index ea0f058..5340578 100644 --- a/src/core/Application.js +++ b/src/core/Application.js @@ -1,6 +1,6 @@ import { autoDetectRenderer } from './autoDetectRenderer'; import Container from './display/Container'; -import Ticker from './ticker/Ticker'; +import { shared, Ticker } from './ticker'; /** * Convenience class to create a new PIXI application. @@ -35,9 +35,9 @@ * @param {boolean} [noWebGL=false] - prevents selection of WebGL renderer, even if such is present * @param {boolean} [options.legacy=false] - If true Pixi will aim to ensure compatibility * with older / less advanced devices. If you experience unexplained flickering try setting this to true. - + * @param {boolean} [useSharedTicker=false] - `true` to use PIXI.ticker.shared, `false` to create new ticker. */ - constructor(width, height, options, noWebGL) + constructor(width, height, options, noWebGL, useSharedTicker = false) { /** * WebGL renderer if available, otherwise CanvasRenderer @@ -46,23 +46,46 @@ this.renderer = autoDetectRenderer(width, height, options, noWebGL); /** - * The root display container that's renderered. + * The root display container that's rendered. * @member {PIXI.Container} */ this.stage = new Container(); /** + * Internal reference to the ticker + * @member {PIXI.ticker.Ticker} + * @private + */ + this._ticker = null; + + /** * Ticker for doing render updates. * @member {PIXI.ticker.Ticker} + * @default PIXI.ticker.shared */ - this.ticker = new Ticker(); - - this.ticker.add(this.render, this); + this.ticker = useSharedTicker ? shared : new Ticker(); // Start the rendering this.start(); } + set ticker(ticker) // eslint-disable-line require-jsdoc + { + if (this._ticker) + { + this._ticker.remove(this.render, this); + } + this._ticker = ticker; + if (ticker) + { + ticker.add(this.render, this); + } + } + get ticker() // eslint-disable-line require-jsdoc + { + return this._ticker; + } + /** * Render the current stage. */ @@ -76,7 +99,7 @@ */ stop() { - this.ticker.stop(); + this._ticker.stop(); } /** @@ -84,7 +107,7 @@ */ start() { - this.ticker.start(); + this._ticker.start(); } /** @@ -98,13 +121,22 @@ } /** + * Reference to the renderer's screen rectangle. Its safe to use as filterArea or hitArea for whole screen + * @member {PIXI.Rectangle} + * @readonly + */ + get screen() + { + return this.renderer.screen; + } + + /** * Destroy and don't use after this. * @param {Boolean} [removeView=false] Automatically remove canvas from DOM. */ destroy(removeView) { this.stop(); - this.ticker.remove(this.render, this); this.ticker = null; this.stage.destroy(); diff --git a/src/core/Shader.js b/src/core/Shader.js new file mode 100644 index 0000000..78b17e6 --- /dev/null +++ b/src/core/Shader.js @@ -0,0 +1,46 @@ +import { GLShader } from 'pixi-gl-core'; +import settings from './settings'; + +function checkPrecision(src, def) +{ + if (src instanceof Array) + { + if (src[0].substring(0, 9) !== 'precision') + { + const copy = src.slice(0); + + copy.unshift(`precision ${def} float;`); + + return copy; + } + } + else if (src.substring(0, 9) !== 'precision') + { + return `precision ${def} float;\n${src}`; + } + + return src; +} + +/** + * Wrapper class, webGL Shader for Pixi. + * Adds precision string if vertexSrc or fragmentSrc have no mention of it. + * + * @class + * @extends GLShader + * @memberof PIXI + */ +export default class Shader extends GLShader +{ + /** + * + * @param {WebGLRenderingContext} gl - The current WebGL rendering context + * @param {string|string[]} vertexSrc - The vertex shader source as an array of strings. + * @param {string|string[]} fragmentSrc - The fragment shader source as an array of strings. + */ + constructor(gl, vertexSrc, fragmentSrc) + { + super(gl, checkPrecision(vertexSrc, settings.PRECISION_VERTEX), + checkPrecision(fragmentSrc, settings.PRECISION_FRAGMENT)); + } +} diff --git a/src/core/const.js b/src/core/const.js index ef6f662..3de0efe 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -188,7 +188,7 @@ * The gc modes that are supported by pixi. * * The {@link PIXI.settings.GC_MODE} Garbage Collection mode for pixi textures is AUTO - * If set to GC_MODE, the renderer will occasianally check textures usage. If they are not + * If set to GC_MODE, the renderer will occasionally check textures usage. If they are not * used for a specified period of time they will be removed from the GPU. They will of course * be uploaded again when they are required. This is a silent behind the scenes process that * should ensure that the GPU does not get filled up. diff --git a/src/core/display/Container.js b/src/core/display/Container.js index ed8cd5f..17fde8d 100644 --- a/src/core/display/Container.js +++ b/src/core/display/Container.js @@ -73,11 +73,12 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.push(child); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -108,11 +109,12 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.splice(index, 0, child); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -225,10 +227,11 @@ if (index === -1) return null; child.parent = null; + // ensure child transform will be recalculated + child.transform._parentID = -1; removeItems(this.children, index, 1); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -249,11 +252,12 @@ { const child = this.getChildAt(index); + // ensure child transform will be recalculated.. child.parent = null; + child.transform._parentID = -1; removeItems(this.children, index, 1); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -284,13 +288,12 @@ for (let i = 0; i < removed.length; ++i) { removed[i].parent = null; + if (removed[i].transform) + { + removed[i].transform._parentID = -1; + } } - // ensure a transform will be recalculated.. - if (this.transform) - { - this.transform._parentID = -1; - } this._boundsID++; this.onChildrenChange(beginIndex); diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index f37f2f5..be4c4fb 100644 --- a/src/core/graphics/Graphics.js +++ b/src/core/graphics/Graphics.js @@ -28,8 +28,9 @@ { /** * + * @param {boolean} [nativeLines=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP */ - constructor() + constructor(nativeLines = false) { super(); @@ -50,6 +51,13 @@ this.lineWidth = 0; /** + * If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * + * @member {boolean} + */ + this.nativeLines = nativeLines; + + /** * The color of any lines drawn. * * @member {string} @@ -757,28 +765,14 @@ if (!this._spriteRect) { - if (!Graphics._SPRITE_TEXTURE) - { - Graphics._SPRITE_TEXTURE = RenderTexture.create(10, 10); - - const canvas = document.createElement('canvas'); - - canvas.width = 10; - canvas.height = 10; - - const context = canvas.getContext('2d'); - - context.fillStyle = 'white'; - context.fillRect(0, 0, 10, 10); - - Graphics._SPRITE_TEXTURE = Texture.fromCanvas(canvas); - } - - this._spriteRect = new Sprite(Graphics._SPRITE_TEXTURE); + this._spriteRect = new Sprite(new Texture(Texture.WHITE)); } + + const sprite = this._spriteRect; + if (this.tint === 0xffffff) { - this._spriteRect.tint = this.graphicsData[0].fillColor; + sprite.tint = this.graphicsData[0].fillColor; } else { @@ -792,20 +786,21 @@ t1[1] *= t2[1]; t1[2] *= t2[2]; - this._spriteRect.tint = rgb2hex(t1); + sprite.tint = rgb2hex(t1); } - this._spriteRect.alpha = this.graphicsData[0].fillAlpha; - this._spriteRect.worldAlpha = this.worldAlpha * this._spriteRect.alpha; + sprite.alpha = this.graphicsData[0].fillAlpha; + sprite.worldAlpha = this.worldAlpha * sprite.alpha; + sprite.blendMode = this.blendMode; - Graphics._SPRITE_TEXTURE._frame.width = rect.width; - Graphics._SPRITE_TEXTURE._frame.height = rect.height; + sprite.texture._frame.width = rect.width; + sprite.texture._frame.height = rect.height; - this._spriteRect.transform.worldTransform = this.transform.worldTransform; + sprite.transform.worldTransform = this.transform.worldTransform; - this._spriteRect.anchor.set(-rect.x / rect.width, -rect.y / rect.height); - this._spriteRect._onAnchorUpdate(); + sprite.anchor.set(-rect.x / rect.width, -rect.y / rect.height); + sprite._onAnchorUpdate(); - this._spriteRect._renderWebGL(renderer); + sprite._renderWebGL(renderer); } /** @@ -1031,6 +1026,7 @@ this.fillColor, this.fillAlpha, this.filling, + this.nativeLines, shape ); diff --git a/src/core/graphics/GraphicsData.js b/src/core/graphics/GraphicsData.js index f5a87ae..b6f03c0 100644 --- a/src/core/graphics/GraphicsData.js +++ b/src/core/graphics/GraphicsData.js @@ -14,14 +14,19 @@ * @param {number} fillColor - the color of the fill * @param {number} fillAlpha - the alpha of the fill * @param {boolean} fill - whether or not the shape is filled with a colour + * @param {boolean} nativeLines - the method for drawing lines * @param {PIXI.Circle|PIXI.Rectangle|PIXI.Ellipse|PIXI.Polygon} shape - The shape object to draw. */ - constructor(lineWidth, lineColor, lineAlpha, fillColor, fillAlpha, fill, shape) + constructor(lineWidth, lineColor, lineAlpha, fillColor, fillAlpha, fill, nativeLines, shape) { /** * @member {number} the width of the line to draw */ this.lineWidth = lineWidth; + /** + * @member {boolean} if true the liens will be draw using LINES instead of TRIANGLE_STRIP + */ + this.nativeLines = nativeLines; /** * @member {number} the color of the line to draw @@ -85,6 +90,7 @@ this.fillColor, this.fillAlpha, this.fill, + this.nativeLines, this.shape ); } diff --git a/src/core/graphics/webgl/GraphicsRenderer.js b/src/core/graphics/webgl/GraphicsRenderer.js index 14083fe..67f5e92 100644 --- a/src/core/graphics/webgl/GraphicsRenderer.js +++ b/src/core/graphics/webgl/GraphicsRenderer.js @@ -102,7 +102,15 @@ shaderTemp.uniforms.alpha = graphics.worldAlpha; renderer.bindVao(webGLData.vao); - webGLData.vao.draw(gl.TRIANGLE_STRIP, webGLData.indices.length); + + if (graphics.nativeLines) + { + gl.drawArrays(gl.LINES, 0, webGLData.points.length / 6); + } + else + { + webGLData.vao.draw(gl.TRIANGLE_STRIP, webGLData.indices.length); + } } } diff --git a/src/core/graphics/webgl/utils/buildLine.js b/src/core/graphics/webgl/utils/buildLine.js index ecf43f7..4d6862e 100644 --- a/src/core/graphics/webgl/utils/buildLine.js +++ b/src/core/graphics/webgl/utils/buildLine.js @@ -11,7 +11,29 @@ * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties * @param {object} webGLData - an object containing all the webGL-specific information to create this shape */ -export default function buildLine(graphicsData, webGLData) +export default function (graphicsData, webGLData) +{ + if (graphicsData.nativeLines) + { + buildNativeLine(graphicsData, webGLData); + } + else + { + buildLine(graphicsData, webGLData); + } +} + +/** + * Builds a line to draw using the poligon method. + * + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @private + * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties + * @param {object} webGLData - an object containing all the webGL-specific information to create this shape + */ +function buildLine(graphicsData, webGLData) { // TODO OPTIMISE! let points = graphicsData.points; @@ -224,3 +246,46 @@ indices.push(indexStart - 1); } + +/** + * Builds a line to draw using the gl.drawArrays(gl.LINES) method + * + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @private + * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties + * @param {object} webGLData - an object containing all the webGL-specific information to create this shape + */ +function buildNativeLine(graphicsData, webGLData) +{ + let i = 0; + const points = graphicsData.points; + + if (points.length === 0) return; + + const verts = webGLData.points; + const length = points.length / 2; + + // sort color + const color = hex2rgb(graphicsData.lineColor); + const alpha = graphicsData.lineAlpha; + const r = color[0] * alpha; + const g = color[1] * alpha; + const b = color[2] * alpha; + + for (i = 1; i < length; i++) + { + const p1x = points[(i - 1) * 2]; + const p1y = points[((i - 1) * 2) + 1]; + + const p2x = points[i * 2]; + const p2y = points[(i * 2) + 1]; + + verts.push(p1x, p1y); + verts.push(r, g, b, alpha); + + verts.push(p2x, p2y); + verts.push(r, g, b, alpha); + } +} diff --git a/src/core/graphics/webgl/utils/buildRoundedRectangle.js b/src/core/graphics/webgl/utils/buildRoundedRectangle.js index 9abc807..a6c8461 100644 --- a/src/core/graphics/webgl/utils/buildRoundedRectangle.js +++ b/src/core/graphics/webgl/utils/buildRoundedRectangle.js @@ -77,6 +77,26 @@ } /** + * Calculate a single point for a quadratic bezier curve. + * Utility function used by quadraticBezierCurve. + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @private + * @param {number} n1 - first number + * @param {number} n2 - second number + * @param {number} perc - percentage + * @return {number} the result + * + */ +function getPt(n1, n2, perc) +{ + const diff = n2 - n1; + + return n1 + (diff * perc); +} + +/** * Calculate the points for a quadratic bezier curve. (helper function..) * Based on: https://stackoverflow.com/questions/785097/how-do-i-implement-a-bezier-curve-in-c * @@ -105,13 +125,6 @@ let x = 0; let y = 0; - function getPt(n1, n2, perc) - { - const diff = n2 - n1; - - return n1 + (diff * perc); - } - for (let i = 0, j = 0; i <= n; ++i) { j = i / n; diff --git a/src/core/index.js b/src/core/index.js index f7a817c..4f414e8 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -30,6 +30,7 @@ export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; export { default as CanvasGraphicsRenderer } from './graphics/canvas/CanvasGraphicsRenderer'; +export { default as Spritesheet } from './textures/Spritesheet'; export { default as Texture } from './textures/Texture'; export { default as BaseTexture } from './textures/BaseTexture'; export { default as RenderTexture } from './textures/RenderTexture'; diff --git a/src/core/math/Matrix.js b/src/core/math/Matrix.js index 0b77477..3679860 100644 --- a/src/core/math/Matrix.js +++ b/src/core/math/Matrix.js @@ -13,45 +13,50 @@ export default class Matrix { /** - * + * @param {number} [a=1] - x scale + * @param {number} [b=0] - y skew + * @param {number} [c=0] - x skew + * @param {number} [d=1] - y scale + * @param {number} [tx=0] - x translation + * @param {number} [ty=0] - y translation */ - constructor() + constructor(a = 1, b = 0, c = 0, d = 1, tx = 0, ty = 0) { /** * @member {number} * @default 1 */ - this.a = 1; + this.a = a; /** * @member {number} * @default 0 */ - this.b = 0; + this.b = b; /** * @member {number} * @default 0 */ - this.c = 0; + this.c = c; /** * @member {number} * @default 1 */ - this.d = 1; + this.d = d; /** * @member {number} * @default 0 */ - this.tx = 0; + this.tx = tx; /** * @member {number} * @default 0 */ - this.ty = 0; + this.ty = ty; this.array = null; } diff --git a/src/core/renderers/SystemRenderer.js b/src/core/renderers/SystemRenderer.js index fa116a6..a24c775 100644 --- a/src/core/renderers/SystemRenderer.js +++ b/src/core/renderers/SystemRenderer.js @@ -1,5 +1,5 @@ import { sayHello, hex2string, hex2rgb } from '../utils'; -import { Matrix } from '../math'; +import { Matrix, Rectangle } from '../math'; import { RENDERER_TYPE } from '../const'; import settings from '../settings'; import Container from '../display/Container'; @@ -21,8 +21,8 @@ { /** * @param {string} system - The name of the system this renderer is for. - * @param {number} [width=800] - the width of the canvas view - * @param {number} [height=600] - the height of the canvas view + * @param {number} [screenWidth=800] - the width of the screen + * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false @@ -37,7 +37,7 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(system, width, height, options) + constructor(system, screenWidth, screenHeight, options) { super(); @@ -69,20 +69,13 @@ this.type = RENDERER_TYPE.UNKNOWN; /** - * The width of the canvas view + * Measurements of the screen. (0, 0, screenWidth, screenHeight) * - * @member {number} - * @default 800 - */ - this.width = width || 800; - - /** - * The height of the canvas view + * Its safe to use as filterArea or hitArea for whole stage * - * @member {number} - * @default 600 + * @member {PIXI.Rectangle} */ - this.height = height || 600; + this.screen = new Rectangle(0, 0, screenWidth || 800, screenHeight || 600); /** * The canvas element that everything is drawn to @@ -107,7 +100,7 @@ this.transparent = options.transparent; /** - * Whether the render view should be resized automatically + * Whether css dimensions of canvas view should be resized to screen dimensions automatically * * @member {boolean} */ @@ -192,23 +185,48 @@ } /** - * Resizes the canvas view to the specified width and height + * Same as view.width, actual number of pixels in the canvas by horizontal * - * @param {number} width - the new width of the canvas view - * @param {number} height - the new height of the canvas view + * @member {number} + * @readonly + * @default 800 */ - resize(width, height) + get width() { - this.width = width * this.resolution; - this.height = height * this.resolution; + return this.view.width; + } - this.view.width = this.width; - this.view.height = this.height; + /** + * Same as view.height, actual number of pixels in the canvas by vertical + * + * @member {number} + * @readonly + * @default 600 + */ + get height() + { + return this.view.height; + } + + /** + * Resizes the screen and canvas to the specified width and height + * Canvas dimensions are multiplied by resolution + * + * @param {number} screenWidth - the new width of the screen + * @param {number} screenHeight - the new height of the screen + */ + resize(screenWidth, screenHeight) + { + this.screen.width = screenWidth; + this.screen.height = screenHeight; + + this.view.width = screenWidth * this.resolution; + this.view.height = screenHeight * this.resolution; if (this.autoResize) { - this.view.style.width = `${this.width / this.resolution}px`; - this.view.style.height = `${this.height / this.resolution}px`; + this.view.style.width = `${screenWidth}px`; + this.view.style.height = `${screenHeight}px`; } } @@ -249,11 +267,10 @@ this.type = RENDERER_TYPE.UNKNOWN; - this.width = 0; - this.height = 0; - this.view = null; + this.screen = null; + this.resolution = 0; this.transparent = false; diff --git a/src/core/renderers/canvas/CanvasRenderer.js b/src/core/renderers/canvas/CanvasRenderer.js index 1bd2d5e..e84e49c 100644 --- a/src/core/renderers/canvas/CanvasRenderer.js +++ b/src/core/renderers/canvas/CanvasRenderer.js @@ -18,8 +18,8 @@ export default class CanvasRenderer extends SystemRenderer { /** - * @param {number} [width=800] - the width of the canvas view - * @param {number} [height=600] - the height of the canvas view + * @param {number} [screenWidth=800] - the width of the screen + * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false @@ -34,9 +34,9 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(width, height, options = {}) + constructor(screenWidth, screenHeight, options = {}) { - super('Canvas', width, height, options); + super('Canvas', screenWidth, screenHeight, options); this.type = RENDERER_TYPE.CANVAS; @@ -96,7 +96,7 @@ this.context = null; this.renderingToScreen = false; - this.resize(width, height); + this.resize(screenWidth, screenHeight); } /** @@ -283,12 +283,12 @@ * * @extends PIXI.SystemRenderer#resize * - * @param {number} width - The new width of the canvas view - * @param {number} height - The new height of the canvas view + * @param {number} screenWidth - the new width of the screen + * @param {number} screenHeight - the new height of the screen */ - resize(width, height) + resize(screenWidth, screenHeight) { - super.resize(width, height); + super.resize(screenWidth, screenHeight); // reset the scale mode.. oddly this seems to be reset when the canvas is resized. // surely a browser bug?? Let pixi fix that for you.. diff --git a/src/core/renderers/webgl/TextureManager.js b/src/core/renderers/webgl/TextureManager.js index b685c1a..8227289 100644 --- a/src/core/renderers/webgl/TextureManager.js +++ b/src/core/renderers/webgl/TextureManager.js @@ -61,7 +61,7 @@ * Updates and/or Creates a WebGL texture for the renderer's context. * * @param {PIXI.BaseTexture|PIXI.Texture} texture - the texture to update - * @param {Number} location - the location the texture will be bound to. + * @param {number} location - the location the texture will be bound to. * @return {GLTexture} The gl texture. */ updateTexture(texture, location) diff --git a/src/core/renderers/webgl/WebGLRenderer.js b/src/core/renderers/webgl/WebGLRenderer.js index e4cd8a0..830b382 100644 --- a/src/core/renderers/webgl/WebGLRenderer.js +++ b/src/core/renderers/webgl/WebGLRenderer.js @@ -31,8 +31,8 @@ { /** * - * @param {number} [width=0] - the width of the canvas view - * @param {number} [height=0] - the height of the canvas view + * @param {number} [screenWidth=800] - the width of the screen + * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false @@ -53,9 +53,9 @@ * @param {boolean} [options.legacy=false] - If true Pixi will aim to ensure compatibility * with older / less advanced devices. If you experiance unexplained flickering try setting this to true. */ - constructor(width, height, options = {}) + constructor(screenWidth, screenHeight, options = {}) { - super('WebGL', width, height, options); + super('WebGL', screenWidth, screenHeight, options); this.legacy = !!options.legacy; @@ -245,7 +245,7 @@ glCore._testingContext = gl; // setup the width/height properties and gl viewport - this.resize(this.width, this.height); + this.resize(this.screen.width, this.screen.height); } /** @@ -343,16 +343,16 @@ /** * Resizes the webGL view to the specified width and height. * - * @param {number} width - the new width of the webGL view - * @param {number} height - the new height of the webGL view + * @param {number} screenWidth - the new width of the screen + * @param {number} screenHeight - the new height of the screen */ - resize(width, height) + resize(screenWidth, screenHeight) { // if(width * this.resolution === this.width && height * this.resolution === this.height)return; - SystemRenderer.prototype.resize.call(this, width, height); + SystemRenderer.prototype.resize.call(this, screenWidth, screenHeight); - this.rootRenderTarget.resize(width, height); + this.rootRenderTarget.resize(screenWidth, screenHeight); if (this._activeRenderTarget === this.rootRenderTarget) { @@ -396,6 +396,26 @@ } /** + * Erases the render texture and fills the drawing area with a colour + * + * @param {PIXI.RenderTexture} renderTexture - The render texture to clear + * @param {number} [clearColor] - The colour + * @return {PIXI.WebGLRenderer} Returns itself. + */ + clearRenderTexture(renderTexture, clearColor) + { + const baseTexture = renderTexture.baseTexture; + const renderTarget = baseTexture._glRenderTargets[this.CONTEXT_UID]; + + if (renderTarget) + { + renderTarget.clear(clearColor); + } + + return this; + } + + /** * Binds a render texture for rendering * * @param {PIXI.RenderTexture} renderTexture - The render texture to render @@ -474,9 +494,10 @@ * Changes the current GLShader to the one given in parameter * * @param {PIXI.glCore.Shader} shader - the new glShader + * @param {boolean} [autoProject=true] - Whether automatically set the projection matrix * @return {PIXI.WebGLRenderer} Returns itself. */ - _bindGLShader(shader) + _bindGLShader(shader, autoProject) { // TODO cache if (this._activeShader !== shader) @@ -484,8 +505,14 @@ this._activeShader = shader; shader.bind(); - // automatically set the projection matrix - shader.uniforms.projectionMatrix = this._activeRenderTarget.projectionMatrix.toArray(true); + // `autoProject` normally would be a default parameter set to true + // but because of how Babel transpiles default parameters + // it hinders the performance of this method. + if (autoProject !== false) + { + // automatically set the projection matrix + shader.uniforms.projectionMatrix = this._activeRenderTarget.projectionMatrix.toArray(true); + } } return this; diff --git a/src/core/renderers/webgl/WebGLState.js b/src/core/renderers/webgl/WebGLState.js index ba6ecc5..6c846d5 100755 --- a/src/core/renderers/webgl/WebGLState.js +++ b/src/core/renderers/webgl/WebGLState.js @@ -82,18 +82,20 @@ push() { // next state.. - let state = this.stack[++this.stackIndex]; + let state = this.stack[this.stackIndex]; if (!state) { state = this.stack[this.stackIndex] = new Uint8Array(16); } + ++this.stackIndex; + // copy state.. // set active state so we can force overrides of gl state for (let i = 0; i < this.activeState.length; i++) { - this.activeState[i] = state[i]; + state[i] = this.activeState[i]; } } diff --git a/src/core/renderers/webgl/filters/Filter.js b/src/core/renderers/webgl/filters/Filter.js index 2c3eaad..4eb6128 100644 --- a/src/core/renderers/webgl/filters/Filter.js +++ b/src/core/renderers/webgl/filters/Filter.js @@ -9,7 +9,7 @@ * @memberof PIXI * @extends PIXI.Shader */ -class Filter extends Shader +export default class Filter extends Shader { /** * @param {string} [vertexSrc] - The source of the vertex shader. @@ -56,15 +56,16 @@ * @param {PIXI.RenderTarget} input - The input render target. * @param {PIXI.RenderTarget} output - The target to output to. * @param {boolean} clear - Should the output be cleared before rendering to it + * @param {object} [currentState] - It's current state of filter. + * There are some useful properties in the currentState : + * target, filters, sourceFrame, destinationFrame, renderTarget, resolution */ - apply(filterManager, input, output, clear) + apply(filterManager, input, output, clear, currentState) // eslint-disable-line no-unused-vars { // do as you please! - filterManager.applyFilter(this, input, output, clear); + filterManager.applyFilter(this, input, output, clear, currentState); // or just do a regular render.. } } - -export default Filter; diff --git a/src/core/renderers/webgl/filters/extractUniformsFromSrc.js b/src/core/renderers/webgl/filters/extractUniformsFromSrc.js index d78a0b1..4893214 100644 --- a/src/core/renderers/webgl/filters/extractUniformsFromSrc.js +++ b/src/core/renderers/webgl/filters/extractUniformsFromSrc.js @@ -12,7 +12,7 @@ function extractUniformsFromString(string) { - const maskRegex = new RegExp('^(projectionMatrix|uSampler|filterArea)$'); + const maskRegex = new RegExp('^(projectionMatrix|uSampler|filterArea|filterClamp)$'); const uniforms = {}; let nameSplit; diff --git a/src/core/renderers/webgl/managers/FilterManager.js b/src/core/renderers/webgl/managers/FilterManager.js index 43fc869..6c7422d 100644 --- a/src/core/renderers/webgl/managers/FilterManager.js +++ b/src/core/renderers/webgl/managers/FilterManager.js @@ -2,6 +2,7 @@ import RenderTarget from '../utils/RenderTarget'; import Quad from '../utils/Quad'; import { Rectangle } from '../../../math'; +import Shader from '../../../Shader'; import * as filterTransforms from '../filters/filterTransforms'; import bitTwiddle from 'bit-twiddle'; @@ -22,8 +23,6 @@ this.filters = []; this.target = null; this.resolution = 1; - - this.firstRun = true; } } @@ -155,7 +154,7 @@ if (filters.length === 1) { - filters[0].apply(this, currentState.renderTarget, lastState.renderTarget, false); + filters[0].apply(this, currentState.renderTarget, lastState.renderTarget, false, currentState); this.freePotRenderTarget(currentState.renderTarget); } else @@ -177,7 +176,7 @@ for (i = 0; i < filters.length - 1; ++i) { - filters[i].apply(this, flip, flop, true); + filters[i].apply(this, flip, flop, true, currentState); const t = flip; @@ -185,7 +184,7 @@ flop = t; } - filters[i].apply(this, flip, lastState.renderTarget, false); + filters[i].apply(this, flip, lastState.renderTarget, false, currentState); this.freePotRenderTarget(flip); this.freePotRenderTarget(flop); @@ -212,16 +211,28 @@ const renderer = this.renderer; const gl = renderer.gl; - renderer.bindShader(filter, true); + let shader = filter.glShaders[renderer.CONTEXT_UID]; - // TODO theres a better way! - // quad can be a mesh and we then should be able to pull this off without accessing the shader directly - const shader = renderer.shaderManager.getGLShader(); - - // TODO change quad to mesh.. - if (!this.firstRun) + // cacheing.. + if (!shader) { - this.firstRun = true; + if (filter.glShaderKey) + { + shader = this.shaderCache[filter.glShaderKey]; + + if (!shader) + { + shader = new Shader(this.gl, filter.vertexSrc, filter.fragmentSrc); + + filter.glShaders[renderer.CONTEXT_UID] = this.shaderCache[filter.glShaderKey] = shader; + } + } + else + { + shader = filter.glShaders[renderer.CONTEXT_UID] = new Shader(this.gl, filter.vertexSrc, filter.fragmentSrc); + } + + // TODO - this only needs to be done once? renderer.bindVao(null); this.quad.initVao(shader); @@ -244,58 +255,146 @@ renderer.maskManager.pushScissorMask(null, renderer.maskManager.scissorData); } - // renderer._bindGLShader(shader); + renderer.bindShader(shader); - let currentState; - const uniforms = filter.uniforms; + // free unit 0 for us, doesn't matter what was there + // don't try to restore it, because syncUniforms can upload it to another slot + // and it'll be a problem + const tex = this.renderer.emptyTextures[0]; - if (uniforms.filterArea) - { - currentState = this.filterData.stack[this.filterData.index]; - - const filterArea = uniforms.filterArea; - - filterArea[0] = currentState.renderTarget.size.width; - filterArea[1] = currentState.renderTarget.size.height; - filterArea[2] = currentState.sourceFrame.x; - filterArea[3] = currentState.sourceFrame.y; - - filter.uniforms.filterArea = filterArea; - } - - // use this to clamp displaced texture coords so they belong to filterArea - // see displacementFilter fragment shader for an example - if (uniforms.filterClamp) - { - currentState = this.filterData.stack[this.filterData.index]; - - const filterClamp = uniforms.filterClamp; - - filterClamp[0] = 0; - filterClamp[1] = 0; - filterClamp[2] = (currentState.sourceFrame.width - 1) / currentState.renderTarget.size.width; - filterClamp[3] = (currentState.sourceFrame.height - 1) / currentState.renderTarget.size.height; - - filter.uniforms.filterClamp = filterClamp; - } - - renderer.shaderManager.setUniforms(filter.uniforms); + this.renderer.boundTextures[0] = tex; + // this syncs the pixi filters uniforms with glsl uniforms + this.syncUniforms(shader, filter); renderer.state.setBlendMode(filter.blendMode); - // temporary bypass cache.. - const tex = this.renderer.boundTextures[0]; - gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, input.texture.texture); this.quad.vao.draw(this.renderer.gl.TRIANGLES, 6, 0); - // restore cache. gl.bindTexture(gl.TEXTURE_2D, tex._glTextures[this.renderer.CONTEXT_UID].texture); } /** + * Uploads the uniforms of the filter. + * + * @param {GLShader} shader - The underlying gl shader. + * @param {PIXI.Filter} filter - The filter we are synchronizing. + */ + syncUniforms(shader, filter) + { + const uniformData = filter.uniformData; + const uniforms = filter.uniforms; + + // 0 is reserved for the pixi texture so we start at 1! + let textureCount = 1; + let currentState; + + // filterArea and filterClamp that are handled by FilterManager directly + // they must not appear in uniformData + + if (shader.uniforms.filterArea) + { + currentState = this.filterData.stack[this.filterData.index]; + + const filterArea = shader.uniforms.filterArea; + + filterArea[0] = currentState.renderTarget.size.width; + filterArea[1] = currentState.renderTarget.size.height; + filterArea[2] = currentState.sourceFrame.x; + filterArea[3] = currentState.sourceFrame.y; + + shader.uniforms.filterArea = filterArea; + } + + // use this to clamp displaced texture coords so they belong to filterArea + // see displacementFilter fragment shader for an example + if (shader.uniforms.filterClamp) + { + currentState = currentState || this.filterData.stack[this.filterData.index]; + + const filterClamp = shader.uniforms.filterClamp; + + filterClamp[0] = 0; + filterClamp[1] = 0; + filterClamp[2] = (currentState.sourceFrame.width - 1) / currentState.renderTarget.size.width; + filterClamp[3] = (currentState.sourceFrame.height - 1) / currentState.renderTarget.size.height; + + shader.uniforms.filterClamp = filterClamp; + } + + // TODO Cacheing layer.. + for (const i in uniformData) + { + if (uniformData[i].type === 'sampler2D' && uniforms[i] !== 0) + { + if (uniforms[i].baseTexture) + { + shader.uniforms[i] = this.renderer.bindTexture(uniforms[i].baseTexture, textureCount); + } + else + { + shader.uniforms[i] = textureCount; + + // TODO + // this is helpful as renderTargets can also be set. + // Although thinking about it, we could probably + // make the filter texture cache return a RenderTexture + // rather than a renderTarget + const gl = this.renderer.gl; + + this.renderer.boundTextures[textureCount] = this.renderer.emptyTextures[textureCount]; + gl.activeTexture(gl.TEXTURE0 + textureCount); + + uniforms[i].texture.bind(); + } + + textureCount++; + } + else if (uniformData[i].type === 'mat3') + { + // check if its pixi matrix.. + if (uniforms[i].a !== undefined) + { + shader.uniforms[i] = uniforms[i].toArray(true); + } + else + { + shader.uniforms[i] = uniforms[i]; + } + } + else if (uniformData[i].type === 'vec2') + { + // check if its a point.. + if (uniforms[i].x !== undefined) + { + const val = shader.uniforms[i] || new Float32Array(2); + + val[0] = uniforms[i].x; + val[1] = uniforms[i].y; + shader.uniforms[i] = val; + } + else + { + shader.uniforms[i] = uniforms[i]; + } + } + else if (uniformData[i].type === 'float') + { + if (shader.uniforms.data[i].value !== uniformData[i]) + { + shader.uniforms[i] = uniforms[i]; + } + } + else + { + shader.uniforms[i] = uniforms[i]; + } + } + } + + /** * Gets a render target from the pool, or creates a new one. * * @param {boolean} clear - Should we clear the render texture when we get it? @@ -442,7 +541,6 @@ renderTarget.resolution = resolution; renderTarget.defaultFrame.width = renderTarget.size.width = minWidth / resolution; renderTarget.defaultFrame.height = renderTarget.size.height = minHeight / resolution; - renderTarget.attachStencilBuffer(); return renderTarget; } diff --git a/src/core/settings.js b/src/core/settings.js index bb08cb0..f4dd087 100644 --- a/src/core/settings.js +++ b/src/core/settings.js @@ -77,11 +77,11 @@ * * @static * @memberof PIXI.settings - * @type {RegExp|string} + * @type {RegExp} * @example `@2x` - * @default /@(.+)x/ + * @default /@([0-9\.]+)x/ */ - RETINA_PREFIX: /@(.+)x/, + RETINA_PREFIX: /@([0-9\.]+)x/, /** * The default render options if none are supplied to {@link PIXI.WebGLRenderer} @@ -175,14 +175,24 @@ SCALE_MODE: 0, /** - * Default specify float precision in shaders. + * Default specify float precision in vertex shader. + * + * @static + * @memberof PIXI.settings + * @type {PIXI.PRECISION} + * @default PIXI.PRECISION.HIGH + */ + PRECISION_VERTEX: 'highp', + + /** + * Default specify float precision in fragment shader. * * @static * @memberof PIXI.settings * @type {PIXI.PRECISION} * @default PIXI.PRECISION.MEDIUM */ - PRECISION: 'mediump', + PRECISION_FRAGMENT: 'mediump', /** * Can we upload the same buffer in a single frame? diff --git a/src/core/sprites/Sprite.js b/src/core/sprites/Sprite.js index 28b0b18..121bf58 100644 --- a/src/core/sprites/Sprite.js +++ b/src/core/sprites/Sprite.js @@ -212,11 +212,11 @@ } else { - w0 = orig.width * (1 - anchor._x); - w1 = orig.width * -anchor._x; + w1 = -anchor._x * orig.width; + w0 = w1 + orig.width; - h0 = orig.height * (1 - anchor._y); - h1 = orig.height * -anchor._y; + h1 = -anchor._y * orig.height; + h0 = h1 + orig.height; } // xy @@ -269,11 +269,11 @@ const tx = wt.tx; const ty = wt.ty; - const w0 = (orig.width) * (1 - anchor._x); - const w1 = (orig.width) * -anchor._x; + const w1 = -anchor._x * orig.width; + const w0 = w1 + orig.width; - const h0 = orig.height * (1 - anchor._y); - const h1 = orig.height * -anchor._y; + const h1 = -anchor._y * orig.height; + const h0 = h1 + orig.height; // xy vertexData[0] = (a * w1) + (c * h1) + tx; @@ -440,7 +440,7 @@ * * @static * @param {number|string|PIXI.BaseTexture|HTMLCanvasElement|HTMLVideoElement} source Source to create texture from - * @return {PIXI.Sprite} The newly created texture + * @return {PIXI.Sprite} The newly created sprite */ static from(source) { @@ -538,8 +538,8 @@ } /** - * The tint applied to the sprite. This is a hex value. A value of - * 0xFFFFFF will remove any tint effect. + * The tint applied to the sprite. This is a hex value. + * A value of 0xFFFFFF will remove any tint effect. * * @member {number} * @default 0xFFFFFF diff --git a/src/core/sprites/canvas/CanvasTinter.js b/src/core/sprites/canvas/CanvasTinter.js index 1cf316b..27e5e0c 100644 --- a/src/core/sprites/canvas/CanvasTinter.js +++ b/src/core/sprites/canvas/CanvasTinter.js @@ -75,8 +75,8 @@ crop.width *= resolution; crop.height *= resolution; - canvas.width = crop.width; - canvas.height = crop.height; + canvas.width = Math.ceil(crop.width); + canvas.height = Math.ceil(crop.height); context.fillStyle = `#${(`00000${(color | 0).toString(16)}`).substr(-6)}`; @@ -130,8 +130,8 @@ crop.width *= resolution; crop.height *= resolution; - canvas.width = crop.width; - canvas.height = crop.height; + canvas.width = Math.ceil(crop.width); + canvas.height = Math.ceil(crop.height); context.globalCompositeOperation = 'copy'; context.fillStyle = `#${(`00000${(color | 0).toString(16)}`).substr(-6)}`; @@ -172,8 +172,8 @@ crop.width *= resolution; crop.height *= resolution; - canvas.width = crop.width; - canvas.height = crop.height; + canvas.width = Math.ceil(crop.width); + canvas.height = Math.ceil(crop.height); context.globalCompositeOperation = 'copy'; context.drawImage( diff --git a/src/core/sprites/webgl/SpriteRenderer.js b/src/core/sprites/webgl/SpriteRenderer.js index 2affcd5..7c352e2 100644 --- a/src/core/sprites/webgl/SpriteRenderer.js +++ b/src/core/sprites/webgl/SpriteRenderer.js @@ -30,7 +30,7 @@ /** * Number of values sent in the vertex buffer. - * positionX, positionY, colorR, colorG, colorB = 5 + * aVertexPosition(2), aTextureCoord(1), aColor(1), aTextureId(1) = 5 * * @member {number} */ @@ -75,7 +75,6 @@ this.shader = null; this.currentIndex = 0; - TICK = 0; this.groups = []; for (let k = 0; k < this.size; k++) diff --git a/src/core/textures/BaseTexture.js b/src/core/textures/BaseTexture.js index 853de7e..aeccf39 100644 --- a/src/core/textures/BaseTexture.js +++ b/src/core/textures/BaseTexture.js @@ -232,16 +232,24 @@ this.realWidth = this.source.naturalWidth || this.source.videoWidth || this.source.width; this.realHeight = this.source.naturalHeight || this.source.videoHeight || this.source.height; - this.width = this.realWidth / this.resolution; - this.height = this.realHeight / this.resolution; - - this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); + this._updateDimensions(); } this.emit('update', this); } /** + * Update dimensions from real values + */ + _updateDimensions() + { + this.width = this.realWidth / this.resolution; + this.height = this.realHeight / this.resolution; + + this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); + } + + /** * Load a source. * * If the source is not-immediately-available, such as an image that needs to be @@ -525,11 +533,7 @@ this.realWidth = Math.round(svgWidth * this.sourceScale); this.realHeight = Math.round(svgHeight * this.sourceScale); - this.width = this.realWidth / this.resolution; - this.height = this.realHeight / this.resolution; - - // Check pow2 after scale - this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); + this._updateDimensions(); // Create a canvas element const canvas = document.createElement('canvas'); @@ -690,4 +694,52 @@ return baseTexture; } + + /** + * Helper function that creates a base texture based on the source you provide. + * The source can be - image url, image element, canvas element. + * + * @static + * @param {string|HTMLImageElement|HTMLCanvasElement} source - The source to create base texture from. + * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values + * @param {number} [sourceScale=(auto)] - Scale for the original image, used with Svg images. + * @return {PIXI.BaseTexture} The new base texture. + */ + static from(source, scaleMode, sourceScale) + { + if (typeof source === 'string') + { + return BaseTexture.fromImage(source, undefined, scaleMode, sourceScale); + } + else if (source instanceof HTMLImageElement) + { + const imageUrl = source.src; + let baseTexture = BaseTextureCache[imageUrl]; + + if (!baseTexture) + { + baseTexture = new BaseTexture(source, scaleMode); + baseTexture.imageUrl = imageUrl; + + if (sourceScale) + { + baseTexture.sourceScale = sourceScale; + } + + // if there is an @2x at the end of the url we are going to assume its a highres image + baseTexture.resolution = getResolutionOfUrl(imageUrl); + + BaseTextureCache[imageUrl] = baseTexture; + } + + return baseTexture; + } + else if (source instanceof HTMLCanvasElement) + { + return BaseTexture.fromCanvas(source, scaleMode); + } + + // lets assume its a base texture! + return source; + } } diff --git a/src/core/textures/Spritesheet.js b/src/core/textures/Spritesheet.js new file mode 100644 index 0000000..d469c62 --- /dev/null +++ b/src/core/textures/Spritesheet.js @@ -0,0 +1,274 @@ +import { Rectangle, Texture } from '../'; +import { getResolutionOfUrl, TextureCache } from '../utils'; + +/** + * Utility class for maintaining reference to a collection + * of Textures on a single Spritesheet. + * + * @class + * @memberof PIXI + */ +export default class Spritesheet +{ + /** + * The maximum number of Textures to build per process. + * + * @type {number} + * @default 1000 + */ + static get BATCH_SIZE() + { + return 1000; + } + + /** + * @param {PIXI.BaseTexture} baseTexture Reference to the source BaseTexture object. + * @param {Object} data - Spritesheet image data. + * @param {string} [resolutionFilename] - The filename to consider when determining + * the resolution of the spritesheet. If not provided, the imageUrl will + * be used on the BaseTexture. + */ + constructor(baseTexture, data, resolutionFilename = null) + { + /** + * Reference to ths source texture + * @type {PIXI.BaseTexture} + */ + this.baseTexture = baseTexture; + + /** + * Map of spritesheet textures. + * @type {Object} + */ + this.textures = {}; + + /** + * Reference to the original JSON data. + * @type {Object} + */ + this.data = data; + + /** + * The resolution of the spritesheet. + * @type {number} + */ + this.resolution = this._updateResolution( + resolutionFilename || this.baseTexture.imageUrl + ); + + /** + * Map of spritesheet frames. + * @type {Object} + * @private + */ + this._frames = this.data.frames; + + /** + * Collection of frame names. + * @type {string[]} + * @private + */ + this._frameKeys = Object.keys(this._frames); + + /** + * Current batch index being processed. + * @type {number} + * @private + */ + this._batchIndex = 0; + + /** + * Callback when parse is completed. + * @type {Function} + * @private + */ + this._callback = null; + } + + /** + * Generate the resolution from the filename or fallback + * to the meta.scale field of the JSON data. + * + * @private + * @param {string} resolutionFilename - The filename to use for resolving + * the default resolution. + * @return {number} Resolution to use for spritesheet. + */ + _updateResolution(resolutionFilename) + { + const scale = this.data.meta.scale; + + // Use a defaultValue of `null` to check if a url-based resolution is set + let resolution = getResolutionOfUrl(resolutionFilename, null); + + // No resolution found via URL + if (resolution === null) + { + // Use the scale value or default to 1 + resolution = scale !== undefined ? parseFloat(scale) : 1; + } + + // For non-1 resolutions, update baseTexture + if (resolution !== 1) + { + this.baseTexture.resolution = resolution; + this.baseTexture.update(); + } + + return resolution; + } + + /** + * Parser spritesheet from loaded data. This is done asynchronously + * to prevent creating too many Texture within a single process. + * + * @param {Function} callback - Callback when complete returns + * a map of the Textures for this spritesheet. + */ + parse(callback) + { + this._batchIndex = 0; + this._callback = callback; + + if (this._frameKeys.length <= Spritesheet.BATCH_SIZE) + { + this._processFrames(0); + this._parseComplete(); + } + else + { + this._nextBatch(); + } + } + + /** + * Process a batch of frames + * + * @private + * @param {number} initialFrameIndex - The index of frame to start. + */ + _processFrames(initialFrameIndex) + { + let frameIndex = initialFrameIndex; + const maxFrames = Spritesheet.BATCH_SIZE; + + while (frameIndex - initialFrameIndex < maxFrames && frameIndex < this._frameKeys.length) + { + const i = this._frameKeys[frameIndex]; + const rect = this._frames[i].frame; + + if (rect) + { + let frame = null; + let trim = null; + const orig = new Rectangle( + 0, + 0, + this._frames[i].sourceSize.w / this.resolution, + this._frames[i].sourceSize.h / this.resolution + ); + + if (this._frames[i].rotated) + { + frame = new Rectangle( + rect.x / this.resolution, + rect.y / this.resolution, + rect.h / this.resolution, + rect.w / this.resolution + ); + } + else + { + frame = new Rectangle( + rect.x / this.resolution, + rect.y / this.resolution, + rect.w / this.resolution, + rect.h / this.resolution + ); + } + + // Check to see if the sprite is trimmed + if (this._frames[i].trimmed) + { + trim = new Rectangle( + this._frames[i].spriteSourceSize.x / this.resolution, + this._frames[i].spriteSourceSize.y / this.resolution, + rect.w / this.resolution, + rect.h / this.resolution + ); + } + + this.textures[i] = new Texture( + this.baseTexture, + frame, + orig, + trim, + this._frames[i].rotated ? 2 : 0 + ); + + // lets also add the frame to pixi's global cache for fromFrame and fromImage functions + TextureCache[i] = this.textures[i]; + } + + frameIndex++; + } + } + + /** + * The parse has completed. + * + * @private + */ + _parseComplete() + { + const callback = this._callback; + + this._callback = null; + this._batchIndex = 0; + callback.call(this, this.textures); + } + + /** + * Begin the next batch of textures. + * + * @private + */ + _nextBatch() + { + this._processFrames(this._batchIndex * Spritesheet.BATCH_SIZE); + this._batchIndex++; + setTimeout(() => + { + if (this._batchIndex * Spritesheet.BATCH_SIZE < this._frameKeys.length) + { + this._nextBatch(); + } + else + { + this._parseComplete(); + } + }, 0); + } + + /** + * Destroy Spritesheet and don't use after this. + * + * @param {boolean} [destroyBase=false] Whether to destroy the base texture as well + */ + destroy(destroyBase = false) + { + for (const i in this.textures) + { + this.textures[i].destroy(); + } + this._frames = null; + this._frameKeys = null; + this.data = null; + this.textures = null; + if (destroyBase) + { + this.baseTexture.destroy(); + } + this.baseTexture = null; + } +} diff --git a/src/core/textures/Texture.js b/src/core/textures/Texture.js index 6403d4f..afe4254 100644 --- a/src/core/textures/Texture.js +++ b/src/core/textures/Texture.js @@ -3,7 +3,7 @@ import TextureUvs from './TextureUvs'; import EventEmitter from 'eventemitter3'; import { Rectangle } from '../math'; -import { TextureCache, BaseTextureCache } from '../utils'; +import { TextureCache, BaseTextureCache, getResolutionOfUrl } from '../utils'; /** * A texture stores the information that represents an image or part of an image. It cannot be added @@ -365,7 +365,8 @@ * The source can be - frame id, image url, video url, canvas element, video element, base texture * * @static - * @param {number|string|PIXI.BaseTexture|HTMLCanvasElement|HTMLVideoElement} source - Source to create texture from + * @param {number|string|HTMLImageElement|HTMLCanvasElement|HTMLVideoElement|PIXI.BaseTexture} + * source - Source to create texture from * @return {PIXI.Texture} The newly created texture */ static from(source) @@ -393,7 +394,7 @@ } else if (source instanceof HTMLImageElement) { - return new Texture(new BaseTexture(source)); + return new Texture(BaseTexture.from(source)); } else if (source instanceof HTMLCanvasElement) { @@ -413,6 +414,43 @@ } /** + * Create a texture from a source and add to the cache. + * + * @static + * @param {HTMLImageElement|HTMLCanvasElement} source - The input source. + * @param {String} imageUrl - File name of texture, for cache and resolving resolution. + * @param {String} [name] - Human readible name for the texture cache. If no name is + * specified, only `imageUrl` will be used as the cache ID. + * @return {PIXI.Texture} Output texture + */ + static fromLoader(source, imageUrl, name) + { + const baseTexture = new BaseTexture(source, null, getResolutionOfUrl(imageUrl)); + const texture = new Texture(baseTexture); + + baseTexture.imageUrl = imageUrl; + + // No name, use imageUrl instead + if (!name) + { + name = imageUrl; + } + + // lets also add the frame to pixi's global cache for fromFrame and fromImage fucntions + BaseTextureCache[name] = baseTexture; + TextureCache[name] = texture; + + // also add references by url if they are different. + if (name !== imageUrl) + { + BaseTextureCache[imageUrl] = baseTexture; + TextureCache[imageUrl] = texture; + } + + return texture; + } + + /** * Adds a texture to the global TextureCache. This cache is shared across the whole PIXI object. * * @static @@ -522,6 +560,29 @@ } } +function createWhiteTexture() +{ + const canvas = document.createElement('canvas'); + + canvas.width = 10; + canvas.height = 10; + + const context = canvas.getContext('2d'); + + context.fillStyle = 'white'; + context.fillRect(0, 0, 10, 10); + + return new Texture(new BaseTexture(canvas)); +} + +function removeAllHandlers(tex) +{ + tex.destroy = function _emptyDestroy() { /* empty */ }; + tex.on = function _emptyOn() { /* empty */ }; + tex.once = function _emptyOnce() { /* empty */ }; + tex.emit = function _emptyEmit() { /* empty */ }; +} + /** * An empty texture, used often to not have to create multiple empty textures. * Can not be destroyed. @@ -530,7 +591,14 @@ * @constant */ Texture.EMPTY = new Texture(new BaseTexture()); -Texture.EMPTY.destroy = function _emptyDestroy() { /* empty */ }; -Texture.EMPTY.on = function _emptyOn() { /* empty */ }; -Texture.EMPTY.once = function _emptyOnce() { /* empty */ }; -Texture.EMPTY.emit = function _emptyEmit() { /* empty */ }; +removeAllHandlers(Texture.EMPTY); + +/** + * A white texture of 10x10 size, used for graphics and other things + * Can not be destroyed. + * + * @static + * @constant + */ +Texture.WHITE = createWhiteTexture(); +removeAllHandlers(Texture.WHITE); diff --git a/src/core/ticker/Ticker.js b/src/core/ticker/Ticker.js index d98a1f0..17a1517 100644 --- a/src/core/ticker/Ticker.js +++ b/src/core/ticker/Ticker.js @@ -67,11 +67,12 @@ * is based, this value is neither capped nor scaled. * If the platform supports DOMHighResTimeStamp, * this value will have a precision of 1 µs. + * Defaults to target frame time * * @member {number} - * @default 1 / TARGET_FPMS + * @default 16.66 */ - this.elapsedMS = 1 / settings.TARGET_FPMS; // default to target frame time + this.elapsedMS = 1 / settings.TARGET_FPMS; /** * The last time {@link PIXI.ticker.Ticker#update} was invoked. diff --git a/src/core/utils/index.js b/src/core/utils/index.js index 6ff609f..be44750 100644 --- a/src/core/utils/index.js +++ b/src/core/utils/index.js @@ -2,6 +2,7 @@ import settings from '../settings'; import EventEmitter from 'eventemitter3'; import pluginTarget from './pluginTarget'; +import * as mixins from './mixin'; import * as isMobile from 'ismobilejs'; let nextUid = 0; @@ -33,6 +34,7 @@ * @type {mixin} */ pluginTarget, + mixins, }; /** @@ -93,7 +95,7 @@ */ export function rgb2hex(rgb) { - return (((rgb[0] * 255) << 16) + ((rgb[1] * 255) << 8) + (rgb[2] * 255)); + return (((rgb[0] * 255) << 16) + ((rgb[1] * 255) << 8) + (rgb[2] * 255 | 0)); } /** @@ -368,3 +370,43 @@ * @private */ export const ProgramCache = {}; + +/** + * Destroys all texture in the cache + * + * @memberof PIXI.utils + * @function destroyTextureCache + */ +export function destroyTextureCache() +{ + let key; + + for (key in TextureCache) + { + TextureCache[key].destroy(); + } + for (key in BaseTextureCache) + { + BaseTextureCache[key].destroy(); + } +} + +/** + * Removes all textures from cache, but does not destroy them + * + * @memberof PIXI.utils + * @function clearTextureCache + */ +export function clearTextureCache() +{ + let key; + + for (key in TextureCache) + { + delete TextureCache[key]; + } + for (key in BaseTextureCache) + { + delete BaseTextureCache[key]; + } +} diff --git a/src/core/utils/mixin.js b/src/core/utils/mixin.js new file mode 100644 index 0000000..114de2a --- /dev/null +++ b/src/core/utils/mixin.js @@ -0,0 +1,59 @@ +/** + * Mixes all enumerable properties and methods from a source object to a target object. + * + * @memberof PIXI.utils.mixins + * @function mixin + * @param {object} target The prototype or instance that properties and methods should be added to. + * @param {object} source The source of properties and methods to mix in. + */ +export function mixin(target, source) +{ + if (!target || !source) return; + // in ES8/ES2017, this would be really easy: + // Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); + + // get all the enumerable property keys + const keys = Object.keys(source); + + // loop through properties + for (let i = 0; i < keys.length; ++i) + { + const propertyName = keys[i]; + + // Set the property using the property descriptor - this works for accessors and normal value properties + Object.defineProperty(target, propertyName, Object.getOwnPropertyDescriptor(source, propertyName)); + } +} + +const mixins = []; + +/** + * Queues a mixin to be handled towards the end of the initialization of PIXI, so that deprecation + * can take effect. + * + * @memberof PIXI.utils.mixins + * @function delayMixin + * @private + * @param {object} target The prototype or instance that properties and methods should be added to. + * @param {object} source The source of properties and methods to mix in. + */ +export function delayMixin(target, source) +{ + mixins.push(target, source); +} + +/** + * Handles all mixins queued via delayMixin(). + * + * @memberof PIXI.utils.mixins + * @function performMixins + * @private + */ +export function performMixins() +{ + for (let i = 0; i < mixins.length; i += 2) + { + mixin(mixins[i], mixins[i + 1]); + } + mixins.length = 0; +} diff --git a/src/deprecation.js b/src/deprecation.js index 045a19f..b9c73b3 100644 --- a/src/deprecation.js +++ b/src/deprecation.js @@ -5,6 +5,7 @@ import * as filters from './filters'; import * as prepare from './prepare'; import * as loaders from './loaders'; +import * as interaction from './interaction'; // provide method to give a stack track for warnings // useful for tracking-down where deprecated methods/properties/classes @@ -534,7 +535,7 @@ { parent: 'GC_MODES', target: 'GC_MODE' }, { parent: 'WRAP_MODES', target: 'WRAP_MODE' }, { parent: 'SCALE_MODES', target: 'SCALE_MODE' }, - { parent: 'PRECISION', target: 'PRECISION' }, + { parent: 'PRECISION', target: 'PRECISION_FRAGMENT' }, ]; for (let i = 0; i < defaults.length; i++) @@ -558,6 +559,32 @@ }); } +Object.defineProperties(core.settings, { + + /** + * @static + * @name PRECISION + * @memberof PIXI.settings + * @see PIXI.PRECISION + * @deprecated since version 4.4.0 + */ + PRECISION: { + enumerable: true, + get() + { + warn('PIXI.settings.PRECISION has been deprecated, please use PIXI.settings.PRECISION_FRAGMENT'); + + return core.settings.PRECISION_FRAGMENT; + }, + set(value) + { + warn('PIXI.settings.PRECISION has been deprecated, please use PIXI.settings.PRECISION_FRAGMENT'); + + core.settings.PRECISION_FRAGMENT = value; + }, + }, +}); + Object.defineProperties(extras, { /** @@ -694,7 +721,7 @@ warn('determineFontProperties is now deprecated, please use the static calculateFontProperties method, ' + 'e.g : Text.calculateFontProperties(fontStyle);'); - return Text.calculateFontProperties(fontStyle); + return core.Text.calculateFontProperties(fontStyle); }; Object.defineProperties(core.TextStyle.prototype, { @@ -1003,3 +1030,69 @@ }, }, }); + +/** + * @name PIXI.interaction.interactiveTarget#defaultCursor + * @static + * @type {number} + * @see PIXI.interaction.interactiveTarget#cursor + * @deprecated since 4.3.0 + */ +Object.defineProperty(interaction.interactiveTarget, 'defaultCursor', { + set(value) + { + warn('Property defaultCursor has been replaced with \'cursor\'. '); + this.cursor = value; + }, + get() + { + warn('Property defaultCursor has been replaced with \'cursor\'. '); + + return this.cursor; + }, + enumerable: true, +}); + +/** + * @name PIXI.interaction.InteractionManager#defaultCursorStyle + * @static + * @type {string} + * @see PIXI.interaction.InteractionManager#cursorStyles + * @deprecated since 4.3.0 + */ +Object.defineProperty(interaction.InteractionManager, 'defaultCursorStyle', { + set(value) + { + warn('Property defaultCursorStyle has been replaced with \'cursorStyles.default\'. '); + this.cursorStyles.default = value; + }, + get() + { + warn('Property defaultCursorStyle has been replaced with \'cursorStyles.default\'. '); + + return this.cursorStyles.default; + }, +}); + +/** + * @name PIXI.interaction.InteractionManager#currentCursorStyle + * @static + * @type {string} + * @see PIXI.interaction.InteractionManager#cursorStyles + * @deprecated since 4.3.0 + */ +Object.defineProperty(interaction.InteractionManager, 'currentCursorStyle', { + set(value) + { + warn('Property currentCursorStyle has been removed.' + + 'See the currentCursorMode property, which works differently.'); + this.currentCursorMode = value; + }, + get() + { + warn('Property currentCursorStyle has been removed.' + + 'See the currentCursorMode property, which works differently.'); + + return this.currentCursorMode; + }, +}); diff --git a/src/extras/BitmapText.js b/src/extras/BitmapText.js index fb6c343..0a0e150 100644 --- a/src/extras/BitmapText.js +++ b/src/extras/BitmapText.js @@ -455,6 +455,73 @@ return this._textHeight; } + + /** + * Register a bitmap font with data and a texture. + * + * @static + * @param {XMLDocument} xml - The XML document data. + * @param {PIXI.Texture} texture - Texture with all symbols. + * @return {Object} Result font object with font, size, lineHeight and char fields. + */ + static registerFont(xml, texture) + { + const data = {}; + const info = xml.getElementsByTagName('info')[0]; + const common = xml.getElementsByTagName('common')[0]; + + data.font = info.getAttribute('face'); + data.size = parseInt(info.getAttribute('size'), 10); + data.lineHeight = parseInt(common.getAttribute('lineHeight'), 10); + data.chars = {}; + + // parse letters + const letters = xml.getElementsByTagName('char'); + + for (let i = 0; i < letters.length; i++) + { + const letter = letters[i]; + const charCode = parseInt(letter.getAttribute('id'), 10); + + const textureRect = new core.Rectangle( + parseInt(letter.getAttribute('x'), 10) + texture.frame.x, + parseInt(letter.getAttribute('y'), 10) + texture.frame.y, + parseInt(letter.getAttribute('width'), 10), + parseInt(letter.getAttribute('height'), 10) + ); + + data.chars[charCode] = { + xOffset: parseInt(letter.getAttribute('xoffset'), 10), + yOffset: parseInt(letter.getAttribute('yoffset'), 10), + xAdvance: parseInt(letter.getAttribute('xadvance'), 10), + kerning: {}, + texture: new core.Texture(texture.baseTexture, textureRect), + + }; + } + + // parse kernings + const kernings = xml.getElementsByTagName('kerning'); + + for (let i = 0; i < kernings.length; i++) + { + const kerning = kernings[i]; + const first = parseInt(kerning.getAttribute('first'), 10); + const second = parseInt(kerning.getAttribute('second'), 10); + const amount = parseInt(kerning.getAttribute('amount'), 10); + + if (data.chars[second]) + { + data.chars[second].kerning[first] = amount; + } + } + + // I'm leaving this as a temporary fix so we can test the bitmap fonts in v3 + // but it's very likely to change + BitmapText.fonts[data.font] = data; + + return data; + } } BitmapText.fonts = {}; diff --git a/src/index.js b/src/index.js index 59356cf..c553427 100644 --- a/src/index.js +++ b/src/index.js @@ -16,6 +16,10 @@ import * as particles from './particles'; import * as prepare from './prepare'; +// handle mixins now, after all code has been added, including deprecation +import { utils } from './core'; +utils.mixins.performMixins(); + export { accessibility, extract, diff --git a/src/interaction/InteractionManager.js b/src/interaction/InteractionManager.js index 7d51134..8baf13a 100644 --- a/src/interaction/InteractionManager.js +++ b/src/interaction/InteractionManager.js @@ -1,18 +1,20 @@ 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'; -// Mix interactiveTarget into core.DisplayObject.prototype -Object.assign( +// Mix interactiveTarget into core.DisplayObject.prototype, after deprecation has been handled +core.utils.mixins.delayMixin( core.DisplayObject.prototype, 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 +70,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 +101,6 @@ this.eventData = new InteractionEvent(); /** - * Tiny little interactiveData pool ! - * - * @member {PIXI.interaction.InteractionData[]} - */ - this.interactiveDataPool = []; - - /** * The DOM element to bind to. * * @private @@ -153,66 +155,12 @@ */ this.supportsPointerEvents = !!window.PointerEvent; - /** - * Are touch events being 'normalized' and converted into pointer events if pointer events are not supported - * For example, on a touch screen mobile device, a touchstart would also be emitted as a pointerdown - * - * @private - * @readonly - * @member {boolean} - */ - this.normalizeTouchEvents = !this.supportsPointerEvents && this.supportsTouchEvents; - - /** - * Are mouse events being 'normalized' and converted into pointer events if pointer events are not supported - * For example, on a desktop pc, a mousedown would also be emitted as a pointerdown - * - * @private - * @readonly - * @member {boolean} - */ - this.normalizeMouseEvents = !this.supportsPointerEvents && !MobileDevice.any; - // this will make it so that you don't have to call bind all the time /** * @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 +168,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,41 +199,32 @@ this.onPointerOver = this.onPointerOver.bind(this); /** - * @private - * @member {Function} + * Dictionary of how different cursor modes are handled. Strings are handled as CSS cursor + * values, objects are handled as dictionaries of CSS values for interactionDOMElement, + * and functions are called instead of changing the CSS. + * Default CSS cursor values are provided for 'default' and 'pointer' modes. + * @member {Object.)>} */ - this.onTouchStart = this.onTouchStart.bind(this); - this.processTouchStart = this.processTouchStart.bind(this); + this.cursorStyles = { + default: 'inherit', + pointer: 'pointer', + }; /** - * @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. - * - * @member {string} - * @default 'inherit' - */ - this.defaultCursorStyle = 'inherit'; - - /** - * The css style of the cursor that is being used. + * The mode of the cursor that is being used. + * The value of this is a key from the cursorStyles dictionary. * * @member {string} */ - this.currentCursorStyle = 'inherit'; + this.currentCursorMode = null; + + /** + * Internal cached let. + * + * @private + * @member {string} + */ + this.cursor = null; /** * Internal cached let. @@ -303,6 +249,7 @@ * object. * * @event mousedown + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -311,6 +258,7 @@ * on the display object. * * @event rightdown + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -319,6 +267,7 @@ * object. * * @event mouseup + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -327,6 +276,7 @@ * over the display object. * * @event rightup + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -335,6 +285,7 @@ * the display object. * * @event click + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -343,6 +294,7 @@ * and released on the display object. * * @event rightclick + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -352,6 +304,7 @@ * [mousedown]{@link PIXI.interaction.InteractionManager#event:mousedown}. * * @event mouseupoutside + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -361,6 +314,7 @@ * [rightdown]{@link PIXI.interaction.InteractionManager#event:rightdown}. * * @event rightupoutside + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -368,6 +322,7 @@ * Fired when a pointer device (usually a mouse) is moved while over the display object * * @event mousemove + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -375,6 +330,7 @@ * Fired when a pointer device (usually a mouse) is moved onto the display object * * @event mouseover + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -382,6 +338,7 @@ * Fired when a pointer device (usually a mouse) is moved off the display object * * @event mouseout + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -389,6 +346,7 @@ * Fired when a pointer device button is pressed on the display object. * * @event pointerdown + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -396,6 +354,14 @@ * Fired when a pointer device button is released over the display object. * * @event pointerup + * @type {PIXI.interaction.InteractionData} + * @memberof PIXI.interaction.InteractionManager# + */ + + /** + * Fired when the operating system cancels a pointer event + * + * @event pointercancel * @memberof PIXI.interaction.InteractionManager# */ @@ -403,6 +369,7 @@ * Fired when a pointer device button is pressed and released on the display object. * * @event pointertap + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -411,6 +378,7 @@ * registered a [pointerdown]{@link PIXI.interaction.InteractionManager#event:pointerdown}. * * @event pointerupoutside + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -418,6 +386,7 @@ * Fired when a pointer device is moved while over the display object * * @event pointermove + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -425,6 +394,7 @@ * Fired when a pointer device is moved onto the display object * * @event pointerover + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -432,6 +402,7 @@ * Fired when a pointer device is moved off the display object * * @event pointerout + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -439,6 +410,7 @@ * Fired when a touch point is placed on the display object. * * @event touchstart + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -446,6 +418,14 @@ * Fired when a touch point is removed from the display object. * * @event touchend + * @type {PIXI.interaction.InteractionData} + * @memberof PIXI.interaction.InteractionManager# + */ + + /** + * Fired when the operating system cancels a touch + * + * @event touchcancel * @memberof PIXI.interaction.InteractionManager# */ @@ -453,6 +433,7 @@ * Fired when a touch point is placed and removed from the display object. * * @event tap + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -461,6 +442,7 @@ * registered a [touchstart]{@link PIXI.interaction.InteractionManager#event:touchstart}. * * @event touchendoutside + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -468,6 +450,7 @@ * Fired when a touch point is moved along the display object. * * @event touchmove + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ } @@ -524,45 +507,30 @@ { window.document.addEventListener('pointermove', this.onPointerMove, true); this.interactionDOMElement.addEventListener('pointerdown', this.onPointerDown, true); - this.interactionDOMElement.addEventListener('pointerout', this.onPointerOut, true); + // pointerout is fired in addition to pointerup (for touch events) and pointercancel + // we already handle those, so for the purposes of what we do in onPointerOut, we only + // care about the pointerleave event + this.interactionDOMElement.addEventListener('pointerleave', 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; @@ -596,45 +564,26 @@ { window.document.removeEventListener('pointermove', this.onPointerMove, true); this.interactionDOMElement.removeEventListener('pointerdown', this.onPointerDown, true); - this.interactionDOMElement.removeEventListener('pointerout', this.onPointerOut, true); + this.interactionDOMElement.removeEventListener('pointerleave', 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) + 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.onPointerDown, true); + this.interactionDOMElement.removeEventListener('touchcancel', this.onPointerCancel, 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); - - 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 = null; @@ -664,7 +613,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; @@ -672,25 +621,80 @@ return; } - this.cursor = this.defaultCursorStyle; + this.cursor = null; // 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(); - - this.processInteractive(this.mouse.global, this.renderer._lastObjectRendered, this.processMouseOverOut, true); - - if (this.currentCursorStyle !== this.cursor) + for (const k in this.activeInteractionData) { - this.currentCursorStyle = this.cursor; - this.interactionDOMElement.style.cursor = this.cursor; + // eslint-disable-next-line no-prototype-builtins + if (this.activeInteractionData.hasOwnProperty(k)) + { + const interactionData = this.activeInteractionData[k]; + + if (interactionData.originalEvent && interactionData.pointerType !== 'touch') + { + const interactionEvent = this.configureInteractionEventForDOMEvent( + this.eventData, + interactionData.originalEvent, + interactionData + ); + + this.processInteractive( + interactionEvent, + this.renderer._lastObjectRendered, + this.processPointerOverOut, + true + ); + } + } } + this.setCursorMode(this.cursor); + // TODO } /** + * Sets the current cursor mode, handling any callbacks or CSS style changes. + * + * @param {string} mode - cursor mode, a key from the cursorStyles dictionary + */ + setCursorMode(mode) + { + mode = mode || 'default'; + // if the mode didn't actually change, bail early + if (this.currentCursorMode === mode) + { + return; + } + this.currentCursorMode = mode; + const style = this.cursorStyles[mode]; + + // only do things if there is a cursor style for it + if (style) + { + switch (typeof style) + { + case 'string': + // string styles are handled as cursor CSS + this.interactionDOMElement.style.cursor = style; + break; + case 'function': + // functions are just called, and passed the cursor mode + style(mode); + break; + case 'object': + // if it is an object, assume that it is a dictionary of CSS styles, + // apply it to the interactionDOMElement + Object.assign(this.interactionDOMElement.style, style); + break; + } + } + } + + /** * Dispatches an event on the display object that was interacted with * * @param {PIXI.Container|PIXI.Sprite|PIXI.extras.TilingSprite} displayObject - the display object in question @@ -748,22 +752,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 @@ -787,9 +795,8 @@ { interactiveParent = false; } - - // it has a mask! Then lets hit test that before continuing.. - if (hitTest && displayObject._mask) + // it has a mask! Then lets hit test that before continuing + else if (hitTest && displayObject._mask) { if (!displayObject._mask.containsPoint(point)) { @@ -797,14 +804,7 @@ } } - // it has a filterArea! Same as mask but easier, its a rectangle - if (hitTest && displayObject.filterArea) - { - if (!displayObject.filterArea.contains(point.x, point.y)) - { - hitTest = false; - } - } + let keepHitTestingAfterChildren = hitTest; // ** FREE TIP **! If an object is not interactive or has no buttons in it // (such as a game scene!) set interactiveChildren to false for that displayObject. @@ -818,7 +818,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 @@ -838,15 +838,20 @@ // This means we no longer need to hit test anything else. We still need to run // through all objects, but we don't need to perform any hit tests. - // { - hitTest = false; - // } + keepHitTestingAfterChildren = false; + + if (child.interactive) + { + hitTest = false; + } // we can break now as we have hit an object. } } } + hitTest = keepHitTestingAfterChildren; + // no point running this if the item is not interactive or does not have an interactive parent. if (interactive) { @@ -868,14 +873,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 +886,175 @@ } /** - * 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].isNormalized) { - 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); + if (!displayObject.trackedPointers[id]) + { + displayObject.trackedPointers[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.trackedPointers[id].rightDown = true; + } + else + { + displayObject.trackedPointers[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.trackedPointers[id] !== undefined) + { + delete displayObject.trackedPointers[id]; + this.dispatchEvent(displayObject, 'pointercancel', interactionEvent); + + if (e.type === 'touchcancel' || e.pointerType === 'touch') + { + this.dispatchEvent(displayObject, 'touchcancel', interactionEvent); + } } } @@ -1159,42 +1066,94 @@ */ 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) { - if (hit) - { - this.dispatchEvent(displayObject, 'pointerup', this.eventData); + const e = interactionEvent.data.originalEvent; - if (displayObject._pointerDown) + const id = interactionEvent.data.identifier; + + const trackingData = displayObject.trackedPointers[id]; + + const isTouch = (e.type === 'touchend' || e.pointerType === 'touch'); + + const isMouse = (e.type.indexOf('mouse') === 0 || e.pointerType === 'mouse'); + + // 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) { - displayObject._pointerDown = false; - this.dispatchEvent(displayObject, 'pointertap', this.eventData); + 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); + } + // update the down state of the tracking data + if (trackingData) + { + if (isRightButton) + { + trackingData.rightDown = hit; + } + else + { + trackingData.leftDown = hit; + } } } - else if (displayObject._pointerDown) + + // Pointers and Touches, and Mouse + if (hit) { - displayObject._pointerDown = false; - this.dispatchEvent(displayObject, 'pointerupoutside', this.eventData); + this.dispatchEvent(displayObject, 'pointerup', interactionEvent); + if (isTouch) this.dispatchEvent(displayObject, 'touchend', interactionEvent); + + if (trackingData) + { + this.dispatchEvent(displayObject, 'pointertap', interactionEvent); + if (isTouch) + { + this.dispatchEvent(displayObject, 'tap', interactionEvent); + // touches are no longer over (if they ever were) when we get the touchend + // so we should ensure that we don't keep pretending that they are + trackingData.over = false; + } + } + } + else if (trackingData) + { + this.dispatchEvent(displayObject, 'pointerupoutside', interactionEvent); + if (isTouch) this.dispatchEvent(displayObject, 'touchendoutside', interactionEvent); + } + // Only remove the tracking data if there is no over/down state still associated with it + if (trackingData && trackingData.none) + { + delete displayObject.trackedPointers[id]; } } @@ -1202,39 +1161,78 @@ * 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 = null; + } - 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') + { + this.setCursorMode(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 (isMouse) { - 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 +1240,94 @@ * 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.setCursorMode(null); + } - 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'); + + let trackingData = displayObject.trackedPointers[id]; + + // if we just moused over the display object, then we need to track that state + if (hit && !trackingData) + { + trackingData = displayObject.trackedPointers[id] = new InteractionTrackingData(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); + } + } + + // only change the cursor if it has not already been changed (by something deeper in the + // display tree) + if (isMouse && this.cursor === null) + { + this.cursor = displayObject.cursor; } } - 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); + } + // if there is no mouse down information for the pointer, then it is safe to delete + if (trackingData.none) + { + delete displayObject.trackedPointers[id]; + } } } @@ -1287,253 +1335,154 @@ * 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 && pointerEvent.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; + + // mark the touch as normalized, just so that we know we did it + touch.isNormalized = true; + + normalizedEvents.push(touch); + } } - else if (this.normalizeMouseEvents) + // apparently PointerEvent subclasses MouseEvent, so yay + else if (event instanceof MouseEvent && (!this.supportsPointerEvents || !(event instanceof window.PointerEvent))) { if (typeof event.isPrimary === 'undefined') event.isPrimary = true; if (typeof event.width === 'undefined') event.width = 1; @@ -1541,10 +1490,21 @@ 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; + + // mark the mouse event as normalized, just so that we know we did it + event.isNormalized = true; + + normalizedEvents.push(event); } + else + { + normalizedEvents.push(event); + } + + return normalizedEvents; } /** @@ -1563,30 +1523,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 +1542,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..6a1f69e --- /dev/null +++ b/src/interaction/InteractionTrackingData.js @@ -0,0 +1,147 @@ +/** + * 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 inactive (not over or down)? + * + * @member {number} + * @memberof PIXI.interaction.InteractionTrackingData# + */ + get none() + { + return this._flags === this.constructor.FLAGS.NONE; + } + + /** + * 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..d252fba 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 * ); */ @@ -38,68 +38,56 @@ hitArea: null, /** - * If enabled, the mouse cursor will change when hovered over the displayObject if it is interactive + * If enabled, the mouse cursor use the pointer behavior when hovered over the displayObject if it is interactive + * Setting this changes the 'cursor' property to `'pointer'`. * - * @inner {boolean} + * @member {boolean} + * @memberof PIXI.interaction.interactiveTarget# */ - buttonMode: false, + get buttonMode() + { + return this.cursor === 'pointer'; + }, + set buttonMode(value) + { + if (value) + { + this.cursor = 'pointer'; + } + else if (this.cursor === 'pointer') + { + this.cursor = null; + } + }, /** - * If buttonMode is enabled, this defines what CSS cursor property is used when the mouse cursor - * is hovered over the displayObject + * This defines what cursor mode is used when the mouse cursor + * is hovered over the displayObject. * * @see https://developer.mozilla.org/en/docs/Web/CSS/cursor * * @inner {string} */ - defaultCursor: 'pointer', - - // some internal checks.. - /** - * Internal check to detect if the mouse cursor is hovered over the displayObject - * - * @inner {boolean} - * @private - */ - _over: false, + cursor: null, /** - * Internal check to detect if the left mouse button is pressed on the displayObject + * Internal set of all active pointers, by identifier * - * @inner {boolean} + * @member {Map} + * @memberof PIXI.interaction.interactiveTarget# * @private */ - _isLeftDown: false, + get trackedPointers() + { + if (this._trackedPointers === undefined) this._trackedPointers = {}; + + return this._trackedPointers; + }, /** - * Internal check to detect if the right mouse button is pressed on the displayObject + * Map of all tracked pointers, by identifier. Use trackedPointers to access. * - * @inner {boolean} - * @private + * @private {Map} */ - _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, + _trackedPointers: undefined, }; diff --git a/src/loaders/bitmapFontParser.js b/src/loaders/bitmapFontParser.js index cc2618c..f4cf996 100644 --- a/src/loaders/bitmapFontParser.js +++ b/src/loaders/bitmapFontParser.js @@ -1,63 +1,19 @@ import * as path from 'path'; -import { Rectangle, Texture, utils } from '../core'; +import { utils } from '../core'; import { Resource } from 'resource-loader'; import { BitmapText } from '../extras'; +/** + * Register a BitmapText font from loader resource. + * + * @function parseBitmapFontData + * @memberof PIXI.loaders + * @param {PIXI.loaders.Resource} resource - Loader resource. + * @param {PIXI.Texture} texture - Reference to texture. + */ export function parse(resource, texture) { - const data = {}; - const info = resource.data.getElementsByTagName('info')[0]; - const common = resource.data.getElementsByTagName('common')[0]; - - data.font = info.getAttribute('face'); - data.size = parseInt(info.getAttribute('size'), 10); - data.lineHeight = parseInt(common.getAttribute('lineHeight'), 10); - data.chars = {}; - - // parse letters - const letters = resource.data.getElementsByTagName('char'); - - for (let i = 0; i < letters.length; i++) - { - const charCode = parseInt(letters[i].getAttribute('id'), 10); - - const textureRect = new Rectangle( - parseInt(letters[i].getAttribute('x'), 10) + texture.frame.x, - parseInt(letters[i].getAttribute('y'), 10) + texture.frame.y, - parseInt(letters[i].getAttribute('width'), 10), - parseInt(letters[i].getAttribute('height'), 10) - ); - - data.chars[charCode] = { - xOffset: parseInt(letters[i].getAttribute('xoffset'), 10), - yOffset: parseInt(letters[i].getAttribute('yoffset'), 10), - xAdvance: parseInt(letters[i].getAttribute('xadvance'), 10), - kerning: {}, - texture: new Texture(texture.baseTexture, textureRect), - - }; - } - - // parse kernings - const kernings = resource.data.getElementsByTagName('kerning'); - - for (let i = 0; i < kernings.length; i++) - { - const first = parseInt(kernings[i].getAttribute('first'), 10); - const second = parseInt(kernings[i].getAttribute('second'), 10); - const amount = parseInt(kernings[i].getAttribute('amount'), 10); - - if (data.chars[second]) - { - data.chars[second].kerning[first] = amount; - } - } - - resource.bitmapFont = data; - - // I'm leaving this as a temporary fix so we can test the bitmap fonts in v3 - // but it's very likely to change - BitmapText.fonts[data.font] = data; + resource.bitmapFont = BitmapText.registerFont(resource.data, texture); } export default function () diff --git a/src/loaders/index.js b/src/loaders/index.js index 8942436..5189793 100644 --- a/src/loaders/index.js +++ b/src/loaders/index.js @@ -5,4 +5,11 @@ export { default as bitmapFontParser, parse as parseBitmapFontData } from './bitmapFontParser'; export { default as spritesheetParser } from './spritesheetParser'; export { default as textureParser } from './textureParser'; + +/** + * Reference to **resource-loader**'s Resource class. + * See https://github.com/englercj/resource-loader + * @class Resource + * @memberof PIXI.loaders + */ export { Resource } from 'resource-loader'; diff --git a/src/loaders/loader.js b/src/loaders/loader.js index 0a670cb..767a411 100644 --- a/src/loaders/loader.js +++ b/src/loaders/loader.js @@ -7,20 +7,47 @@ /** * - * The new loader, extends Resource Loader by Chad Engler : https://github.com/englercj/resource-loader + * The new loader, extends Resource Loader by Chad Engler: https://github.com/englercj/resource-loader * * ```js - * let loader = PIXI.loader; // pixi exposes a premade instance for you to use. + * const loader = PIXI.loader; // pixi exposes a premade instance for you to use. * //or - * let loader = new PIXI.loaders.Loader(); // you can also create your own if you want + * const loader = new PIXI.loaders.Loader(); // you can also create your own if you want * - * loader.add('bunny', 'data/bunny.png'); - * loader.add('spaceship', 'assets/spritesheet.json'); + * const sprites = {}; + * + * // Chainable `add` to enqueue a resource + * loader.add('bunny', 'data/bunny.png') + * .add('spaceship', 'assets/spritesheet.json'); * loader.add('scoreFont', 'assets/score.fnt'); * - * loader.once('complete',onAssetsLoaded); + * // Chainable `pre` to add a middleware that runs for each resource, *before* loading that resource. + * // This is useful to implement custom caching modules (using filesystem, indexeddb, memory, etc). + * loader.pre(cachingMiddleware); * - * loader.load(); + * // Chainable `use` to add a middleware that runs for each resource, *after* loading that resource. + * // This is useful to implement custom parsing modules (like spritesheet parsers, spine parser, etc). + * loader.use(parsingMiddleware); + * + * // The `load` method loads the queue of resources, and calls the passed in callback called once all + * // resources have loaded. + * loader.load((loader, resources) => { + * // resources is an object where the key is the name of the resource loaded and the value is the resource object. + * // They have a couple default properties: + * // - `url`: The URL that the resource was loaded from + * // - `error`: The error that happened when trying to load (if any) + * // - `data`: The raw data that was loaded + * // also may contain other properties based on the middleware that runs. + * sprites.bunny = new PIXI.TilingSprite(resources.bunny.texture); + * sprites.spaceship = new PIXI.TilingSprite(resources.spaceship.texture); + * sprites.scoreFont = new PIXI.TilingSprite(resources.scoreFont.texture); + * }); + * + * // throughout the process multiple signals can be dispatched. + * loader.onProgress.add(() => {}); // called once per loaded/errored file + * loader.onError.add(() => {}); // called once per errored file + * loader.onLoad.add(() => {}); // called once per loaded file + * loader.onComplete.add(() => {}); // called once when the queued resources all load. * ``` * * @see https://github.com/englercj/resource-loader diff --git a/src/loaders/spritesheetParser.js b/src/loaders/spritesheetParser.js index 7570332..e57dfb2 100644 --- a/src/loaders/spritesheetParser.js +++ b/src/loaders/spritesheetParser.js @@ -1,8 +1,6 @@ import { Resource } from 'resource-loader'; import path from 'path'; -import * as core from '../core'; - -const BATCH_SIZE = 1000; +import { Spritesheet } from '../core'; export default function () { @@ -43,134 +41,18 @@ // load the image for this sheet this.add(imageResourceName, resourcePath, loadOptions, function onImageLoad(res) { - resource.textures = {}; + const spritesheet = new Spritesheet( + res.texture.baseTexture, + resource.data, + resource.url + ); - const frames = resource.data.frames; - const frameKeys = Object.keys(frames); - const baseTexture = res.texture.baseTexture; - const scale = resource.data.meta.scale; - - // Use a defaultValue of `null` to check if a url-based resolution is set - let resolution = core.utils.getResolutionOfUrl(resource.url, null); - - // No resolution found via URL - if (resolution === null) + spritesheet.parse(() => { - // Use the scale value or default to 1 - resolution = scale !== undefined ? scale : 1; - } - - // For non-1 resolutions, update baseTexture - if (resolution !== 1) - { - baseTexture.resolution = resolution; - baseTexture.update(); - } - - let batchIndex = 0; - - function processFrames(initialFrameIndex, maxFrames) - { - let frameIndex = initialFrameIndex; - - while (frameIndex - initialFrameIndex < maxFrames && frameIndex < frameKeys.length) - { - const i = frameKeys[frameIndex]; - const rect = frames[i].frame; - - if (rect) - { - let frame = null; - let trim = null; - const orig = new core.Rectangle( - 0, - 0, - frames[i].sourceSize.w / resolution, - frames[i].sourceSize.h / resolution - ); - - if (frames[i].rotated) - { - frame = new core.Rectangle( - rect.x / resolution, - rect.y / resolution, - rect.h / resolution, - rect.w / resolution - ); - } - else - { - frame = new core.Rectangle( - rect.x / resolution, - rect.y / resolution, - rect.w / resolution, - rect.h / resolution - ); - } - - // Check to see if the sprite is trimmed - if (frames[i].trimmed) - { - trim = new core.Rectangle( - frames[i].spriteSourceSize.x / resolution, - frames[i].spriteSourceSize.y / resolution, - rect.w / resolution, - rect.h / resolution - ); - } - - resource.textures[i] = new core.Texture( - baseTexture, - frame, - orig, - trim, - frames[i].rotated ? 2 : 0 - ); - - // lets also add the frame to pixi's global cache for fromFrame and fromImage functions - core.utils.TextureCache[i] = resource.textures[i]; - } - - frameIndex++; - } - } - - function shouldProcessNextBatch() - { - return batchIndex * BATCH_SIZE < frameKeys.length; - } - - function processNextBatch(done) - { - processFrames(batchIndex * BATCH_SIZE, BATCH_SIZE); - batchIndex++; - setTimeout(done, 0); - } - - function iteration() - { - processNextBatch(() => - { - if (shouldProcessNextBatch()) - { - iteration(); - } - else - { - next(); - } - }); - } - - if (frameKeys.length <= BATCH_SIZE) - { - processFrames(0, BATCH_SIZE); + resource.spritesheet = spritesheet; + resource.textures = spritesheet.textures; next(); - } - else - { - iteration(); - } + }); }); }; } diff --git a/src/loaders/textureParser.js b/src/loaders/textureParser.js index 5398a7f..480edc1 100644 --- a/src/loaders/textureParser.js +++ b/src/loaders/textureParser.js @@ -1,5 +1,5 @@ -import * as core from '../core'; import { Resource } from 'resource-loader'; +import Texture from '../core/textures/Texture'; export default function () { @@ -8,23 +8,12 @@ // create a new texture if the data is an Image object if (resource.data && resource.type === Resource.TYPE.IMAGE) { - const baseTexture = new core.BaseTexture(resource.data, null, core.utils.getResolutionOfUrl(resource.url)); - - baseTexture.imageUrl = resource.url; - resource.texture = new core.Texture(baseTexture); - - // lets also add the frame to pixi's global cache for fromFrame and fromImage fucntions - core.utils.BaseTextureCache[resource.name] = baseTexture; - core.utils.TextureCache[resource.name] = resource.texture; - - // also add references by url if they are different. - if (resource.name !== resource.url) - { - core.utils.BaseTextureCache[resource.url] = baseTexture; - core.utils.TextureCache[resource.url] = resource.texture; - } + resource.texture = Texture.fromLoader( + resource.data, + resource.url, + resource.name + ); } - next(); }; } diff --git a/test/core/Container.js b/test/core/Container.js index 8a10b40..d11be33 100644 --- a/test/core/Container.js +++ b/test/core/Container.js @@ -1,5 +1,33 @@ 'use strict'; +function testAddChild(fn) +{ + fn(function (container, obj) + { + container.addChild(obj); + }); + fn(function (container, obj) + { + container.addChildAt(obj); + }); +} + +function testRemoveChild(fn) +{ + fn(function (container, obj) + { + container.removeChild(obj); + }); + fn(function (container, obj) + { + container.removeChildAt(container.children.indexOf(obj)); + }); + fn(function (container, obj) + { + container.removeChildren(container.children.indexOf(obj), container.children.indexOf(obj) + 1); + }); +} + describe('PIXI.Container', function () { describe('parent', function () @@ -70,6 +98,49 @@ expect(spy).to.have.been.called; expect(spy).to.have.been.calledWith(0); }); + + it('should flag child transform and container bounds for recalculation', testAddChild(function (mockAddChild) + { + const container = new PIXI.Container(); + const child = new PIXI.Container(); + + container.getBounds(); + child.getBounds(); + + const boundsID = container._boundsID; + const childParentID = child.transform._parentID; + + mockAddChild(container, child); + + expect(boundsID).to.not.be.equals(container._boundsID); + expect(childParentID).to.not.be.equals(child.transform._parentID); + })); + + it('should recalculate added child correctly', testAddChild(function (mockAddChild) + { + const parent = new PIXI.Container(); + const container = new PIXI.Container(); + const graphics = new PIXI.Graphics(); + + parent.addChild(container); + + graphics.drawRect(0, 0, 10, 10); + container.position.set(100, 200); + container.updateTransform(); + + graphics.getBounds(); + // Oops, that can happen sometimes! + graphics.transform._parentID = container.transform._worldID + 1; + + mockAddChild(container, graphics); + + const bounds = graphics.getBounds(); + + expect(bounds.x).to.be.equal(100); + expect(bounds.y).to.be.equal(200); + expect(bounds.width).to.be.equal(10); + expect(bounds.height).to.be.equal(10); + })); }); describe('removeChildAt', function () @@ -202,6 +273,44 @@ expect(spy).to.have.been.called; expect(spy).to.have.been.calledWith(0); }); + + it('should flag transform for recalculation', testRemoveChild(function (mockRemoveChild) + { + const container = new PIXI.Container(); + const child = new PIXI.Container(); + + container.addChild(child); + container.getBounds(); + + const childParentID = child.transform._parentID; + const boundsID = container._boundsID; + + mockRemoveChild(container, child); + + expect(childParentID).to.not.be.equals(child.transform._parentID); + expect(boundsID).to.not.be.equals(container._boundsID); + })); + + it('should recalculate removed child correctly', testRemoveChild(function (mockRemoveChild) + { + const parent = new PIXI.Container(); + const container = new PIXI.Container(); + const graphics = new PIXI.Graphics(); + + parent.addChild(container); + + graphics.drawRect(0, 0, 10, 10); + container.position.set(100, 200); + container.addChild(graphics); + graphics.getBounds(); + + mockRemoveChild(container, graphics); + + const bounds = graphics.getBounds(); + + expect(bounds.x).to.be.equal(0); + expect(bounds.y).to.be.equal(0); + })); }); describe('getChildIndex', function () diff --git a/test/core/Graphics.js b/test/core/Graphics.js index 98c7584..11e47a0 100644 --- a/test/core/Graphics.js +++ b/test/core/Graphics.js @@ -1,5 +1,8 @@ 'use strict'; +const MockPointer = require('../interaction/MockPointer'); +const withGL = require('../withGL'); + describe('PIXI.Graphics', function () { describe('constructor', function () @@ -218,4 +221,153 @@ expect(spy).to.have.been.calledOnce; }); }); + + describe('mask', function () + { + it('should trigger interaction callback when no mask present', function () + { + const stage = new PIXI.Container(); + const pointer = new MockPointer(stage); + const graphics = new PIXI.Graphics(); + const mask = new PIXI.Graphics(); + const spy = sinon.spy(); + + graphics.interactive = true; + graphics.beginFill(0xFF0000); + graphics.drawRect(0, 0, 50, 50); + graphics.on('click', spy); + stage.addChild(graphics); + mask.beginFill(); + mask.drawRect(0, 0, 50, 50); + graphics.mask = mask; + + pointer.click(10, 10); + + expect(spy).to.have.been.calledOnce; + }); + it('should trigger interaction callback when mask uses beginFill', function () + { + const stage = new PIXI.Container(); + const pointer = new MockPointer(stage); + const graphics = new PIXI.Graphics(); + const mask = new PIXI.Graphics(); + const spy = sinon.spy(); + + graphics.interactive = true; + graphics.beginFill(0xFF0000); + graphics.drawRect(0, 0, 50, 50); + graphics.on('click', spy); + stage.addChild(graphics); + mask.beginFill(); + mask.drawRect(0, 0, 50, 50); + graphics.mask = mask; + + pointer.click(10, 10); + + expect(spy).to.have.been.calledOnce; + }); + + it('should not trigger interaction callback when mask doesn\'t use beginFill', function () + { + const stage = new PIXI.Container(); + const pointer = new MockPointer(stage); + const graphics = new PIXI.Graphics(); + const mask = new PIXI.Graphics(); + const spy = sinon.spy(); + + graphics.interactive = true; + graphics.beginFill(0xFF0000); + graphics.drawRect(0, 0, 50, 50); + graphics.on('click', spy); + stage.addChild(graphics); + mask.drawRect(0, 0, 50, 50); + graphics.mask = mask; + + pointer.click(10, 10); + + expect(spy).to.have.not.been.called; + }); + + it('should trigger interaction callback when mask doesn\'t use beginFill but hitArea is defined', function () + { + const stage = new PIXI.Container(); + const pointer = new MockPointer(stage); + const graphics = new PIXI.Graphics(); + const mask = new PIXI.Graphics(); + const spy = sinon.spy(); + + graphics.interactive = true; + graphics.beginFill(0xFF0000); + graphics.hitArea = new PIXI.Rectangle(0, 0, 50, 50); + graphics.drawRect(0, 0, 50, 50); + graphics.on('click', spy); + stage.addChild(graphics); + mask.drawRect(0, 0, 50, 50); + graphics.mask = mask; + + pointer.click(10, 10); + + expect(spy).to.have.been.calledOnce; + }); + + it('should trigger interaction callback when mask is a sprite', function () + { + const stage = new PIXI.Container(); + const pointer = new MockPointer(stage); + const graphics = new PIXI.Graphics(); + const mask = new PIXI.Graphics(); + const spy = sinon.spy(); + + graphics.interactive = true; + graphics.beginFill(0xFF0000); + graphics.drawRect(0, 0, 50, 50); + graphics.on('click', spy); + stage.addChild(graphics); + mask.drawRect(0, 0, 50, 50); + graphics.mask = new PIXI.Sprite(mask.generateCanvasTexture()); + + pointer.click(10, 10); + + expect(spy).to.have.been.calledOnce; + }); + + it('should calculate tint, alpha and blendMode of fastRect correctly', withGL(function () + { + const renderer = new PIXI.WebGLRenderer(200, 200, {}); + + try + { + const graphics = new PIXI.Graphics(); + + graphics.beginFill(0x102030, 0.6); + graphics.drawRect(2, 3, 100, 100); + graphics.endFill(); + graphics.tint = 0x101010; + graphics.blendMode = 2; + graphics.alpha = 0.3; + + renderer.render(graphics); + + expect(graphics.isFastRect()).to.be.true; + + const sprite = graphics._spriteRect; + + expect(sprite).to.not.be.equals(null); + expect(sprite.worldAlpha).to.equals(0.18); + expect(sprite.blendMode).to.equals(2); + expect(sprite.tint).to.equals(0x010203); + + const bounds = sprite.getBounds(); + + expect(bounds.x).to.equals(2); + expect(bounds.y).to.equals(3); + expect(bounds.width).to.equals(100); + expect(bounds.height).to.equals(100); + } + finally + { + renderer.destroy(); + } + })); + }); }); diff --git a/test/core/Spritesheet.js b/test/core/Spritesheet.js new file mode 100644 index 0000000..283adf5 --- /dev/null +++ b/test/core/Spritesheet.js @@ -0,0 +1,89 @@ +'use strict'; + +const path = require('path'); + +describe('PIXI.Spritesheet', function () +{ + before(function () + { + this.resources = path.join(__dirname, 'resources'); + this.validate = function (spritesheet, done) + { + spritesheet.parse(function (textures) + { + const id = 'goldmine_10_5.png'; + + expect(Object.keys(textures).length).to.equal(1); + expect(Object.keys(spritesheet.textures).length).to.equal(1); + expect(textures[id]).to.be.an.instanceof(PIXI.Texture); + expect(textures[id].width).to.equal(spritesheet.data.frames[id].frame.w / spritesheet.resolution); + expect(textures[id].height).to.equal(spritesheet.data.frames[id].frame.h / spritesheet.resolution); + spritesheet.destroy(true); + expect(spritesheet.textures).to.be.null; + expect(spritesheet.baseTexture).to.be.null; + done(); + }); + }; + }); + + it('should exist on PIXI', function () + { + expect(PIXI.Spritesheet).to.be.a.function; + expect(PIXI.Spritesheet.BATCH_SIZE).to.be.a.number; + }); + + it('should create an instance', function () + { + const baseTexture = new PIXI.BaseTexture(); + const data = { + frames: {}, + meta: {}, + }; + const spritesheet = new PIXI.Spritesheet(baseTexture, data); + + expect(spritesheet.data).to.equal(data); + expect(spritesheet.baseTexture).to.equal(baseTexture); + expect(spritesheet.resolution).to.equal(1); + + spritesheet.destroy(true); + }); + + it('should create instance with scale resolution', function (done) + { + const data = require(path.resolve(this.resources, 'building1.json')); // eslint-disable-line global-require + const image = new Image(); + + image.src = path.join(this.resources, data.meta.image); + image.onload = () => + { + const baseTexture = new PIXI.BaseTexture(image, null, 1); + const spritesheet = new PIXI.Spritesheet(baseTexture, data); + + expect(data).to.be.an.object; + expect(data.meta.image).to.equal('building1.png'); + expect(spritesheet.resolution).to.equal(0.5); + + this.validate(spritesheet, done); + }; + }); + + it('should create instance with filename resolution', function (done) + { + const uri = path.resolve(this.resources, 'building1@2x.json'); + const data = require(uri); // eslint-disable-line global-require + const image = new Image(); + + image.src = path.join(this.resources, data.meta.image); + image.onload = () => + { + const baseTexture = new PIXI.BaseTexture(image, null, 1); + const spritesheet = new PIXI.Spritesheet(baseTexture, data, uri); + + expect(data).to.be.an.object; + expect(data.meta.image).to.equal('building1@2x.png'); + expect(spritesheet.resolution).to.equal(2); + + this.validate(spritesheet, done); + }; + }); +}); diff --git a/test/core/Texture.js b/test/core/Texture.js new file mode 100644 index 0000000..8c4ef98 --- /dev/null +++ b/test/core/Texture.js @@ -0,0 +1,19 @@ +'use strict'; + +describe('PIXI.Texture', function () +{ + it('should register Texture from Loader', function () + { + const URL = 'foo.png'; + const NAME = 'bar'; + const image = new Image(); + + const texture = PIXI.Texture.fromLoader(image, URL, NAME); + + expect(texture.baseTexture.imageUrl).to.equal('foo.png'); + expect(PIXI.utils.TextureCache[NAME]).to.equal(texture); + expect(PIXI.utils.BaseTextureCache[NAME]).to.equal(texture.baseTexture); + expect(PIXI.utils.TextureCache[URL]).to.equal(texture); + expect(PIXI.utils.BaseTextureCache[URL]).to.equal(texture.baseTexture); + }); +}); diff --git a/test/core/WebGLRenderer.js b/test/core/WebGLRenderer.js index 9bc3bc2..3d6cf98 100644 --- a/test/core/WebGLRenderer.js +++ b/test/core/WebGLRenderer.js @@ -1,21 +1,21 @@ 'use strict'; -function isWebGLSupported(fn) -{ - return PIXI.utils.isWebGLSupported() ? fn : function () {}; // eslint-disable-line no-empty-function -} +const withGL = require('../withGL'); -describe('PIXI.WebGLRenderer', isWebGLSupported(function () +describe('PIXI.WebGLRenderer', function () { - it('setting option legacy should disable VAOs and set minimum SPRITE_MAX_TEXTURES to 1', function () + it('setting option legacy should disable VAOs and SPRITE_MAX_TEXTURES', withGL(function () { const renderer = new PIXI.WebGLRenderer(1, 1, { legacy: true }); - - expect(PIXI.glCore.VertexArrayObject.FORCE_NATIVE).to.equal(true); - expect(renderer.plugins.sprite.MAX_TEXTURES).to.equal(1); - - renderer.destroy(); - }); -})); - + try + { + expect(PIXI.glCore.VertexArrayObject.FORCE_NATIVE).to.equal(true); + expect(renderer.plugins.sprite.MAX_TEXTURES).to.equal(1); + } + finally + { + renderer.destroy(); + } + })); +}); diff --git a/test/core/filters.js b/test/core/filters.js new file mode 100644 index 0000000..df459f6 --- /dev/null +++ b/test/core/filters.js @@ -0,0 +1,21 @@ +'use strict'; + +describe('PIXI.filters', function () +{ + it('should correctly form uniformData', function () + { + const sprite = new PIXI.Sprite(PIXI.Texture.EMPTY); + const displ = new PIXI.filters.DisplacementFilter(sprite); + + expect(!!displ.uniformData.scale).to.be.true; + expect(!!displ.uniformData.filterMatrix).to.be.true; + expect(!!displ.uniformData.mapSampler).to.be.true; + // it does have filterClamp, but it is handled by FilterManager + expect(!!displ.uniformData.filterClamp).to.be.false; + + const fxaa = new PIXI.filters.FXAAFilter(); + + // it does have filterArea, but it is handled by FilterManager + expect(!!fxaa.uniformData.filterArea).to.be.false; + }); +}); diff --git a/test/core/index.js b/test/core/index.js index 63a8f21..8530131 100755 --- a/test/core/index.js +++ b/test/core/index.js @@ -7,6 +7,7 @@ require('./DisplayObject'); require('./getLocalBounds'); require('./Sprite'); +require('./Spritesheet'); require('./TilingSprite'); require('./TextStyle'); require('./Text'); @@ -25,3 +26,5 @@ require('./SpriteRenderer'); require('./WebGLRenderer'); require('./Ellipse'); +require('./Texture'); +require('./filters'); diff --git a/test/core/resources/building1.json b/test/core/resources/building1.json new file mode 100755 index 0000000..03fa5c1 --- /dev/null +++ b/test/core/resources/building1.json @@ -0,0 +1,21 @@ +{"frames": { + +"goldmine_10_5.png": +{ + "frame": {"x":1,"y":1,"w":95,"h":115}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":95,"h":115}, + "sourceSize": {"w":95,"h":115}, + "pivot": {"x":0.5,"y":0.5} +}}, +"meta": { + "app": "http://www.codeandweb.com/texturepacker", + "version": "1.0", + "image": "building1.png", + "format": "RGBA8888", + "size": {"w":128,"h":128}, + "scale": "0.5", + "smartupdate": "$TexturePacker:SmartUpdate:d7a5e54c8f8a3fecd508baf190c44807:99a0e3a4dc0f441e4aad77614191ab38:6046b8eb706ddefaa771c33ceb7cb6d5$" +} +} diff --git a/test/core/resources/building1.png b/test/core/resources/building1.png new file mode 100755 index 0000000..7e1e114 --- /dev/null +++ b/test/core/resources/building1.png Binary files differ diff --git a/test/core/resources/building1@2x.json b/test/core/resources/building1@2x.json new file mode 100755 index 0000000..24e25ff --- /dev/null +++ b/test/core/resources/building1@2x.json @@ -0,0 +1,21 @@ +{"frames": { + +"goldmine_10_5.png": +{ + "frame": {"x":1,"y":1,"w":190,"h":229}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":190,"h":229}, + "sourceSize": {"w":190,"h":229}, + "pivot": {"x":0.5,"y":0.5} +}}, +"meta": { + "app": "http://www.codeandweb.com/texturepacker", + "version": "1.0", + "image": "building1@2x.png", + "format": "RGBA8888", + "size": {"w":256,"h":256}, + "scale": "1", + "smartupdate": "$TexturePacker:SmartUpdate:d7a5e54c8f8a3fecd508baf190c44807:99a0e3a4dc0f441e4aad77614191ab38:6046b8eb706ddefaa771c33ceb7cb6d5$" +} +} diff --git a/test/core/resources/building1@2x.png b/test/core/resources/building1@2x.png new file mode 100755 index 0000000..d5ecd04 --- /dev/null +++ b/test/core/resources/building1@2x.png Binary files differ diff --git a/test/core/util.js b/test/core/util.js index dce6003..239aaeb 100755 --- a/test/core/util.js +++ b/test/core/util.js @@ -47,6 +47,11 @@ .to.be.a('function'); }); + it('should calculate correctly', function () + { + expect(PIXI.utils.rgb2hex([0.3, 0.2, 0.1])).to.equals(0x4c3319); + }); + // it('should properly convert rgb array to hex color string'); }); diff --git a/test/interaction/InteractionManager.js b/test/interaction/InteractionManager.js index 2cf6d9d..0e7e707 100644 --- a/test/interaction/InteractionManager.js +++ b/test/interaction/InteractionManager.js @@ -4,6 +4,281 @@ describe('PIXI.interaction.InteractionManager', function () { + describe('event basics', function () + { + it('should call mousedown handler', function () + { + const stage = new PIXI.Container(); + const graphics = new PIXI.Graphics(); + const eventSpy = sinon.spy(); + const pointer = new MockPointer(stage); + + stage.addChild(graphics); + graphics.beginFill(0xFFFFFF); + graphics.drawRect(0, 0, 50, 50); + graphics.interactive = true; + graphics.on('mousedown', eventSpy); + + pointer.mousedown(10, 10); + + expect(eventSpy).to.have.been.calledOnce; + }); + + it('should call mouseup handler', function () + { + const stage = new PIXI.Container(); + const graphics = new PIXI.Graphics(); + const eventSpy = sinon.spy(); + const pointer = new MockPointer(stage); + + stage.addChild(graphics); + graphics.beginFill(0xFFFFFF); + graphics.drawRect(0, 0, 50, 50); + graphics.interactive = true; + graphics.on('mouseup', eventSpy); + + pointer.click(10, 10); + + expect(eventSpy).to.have.been.called; + }); + + it('should call mouseupoutside handler', function () + { + const stage = new PIXI.Container(); + const graphics = new PIXI.Graphics(); + const eventSpy = sinon.spy(); + const pointer = new MockPointer(stage); + + stage.addChild(graphics); + graphics.beginFill(0xFFFFFF); + graphics.drawRect(0, 0, 50, 50); + graphics.interactive = true; + graphics.on('mouseupoutside', eventSpy); + + pointer.mousedown(10, 10); + pointer.mouseup(60, 60); + + expect(eventSpy).to.have.been.called; + }); + + it('should call mouseover handler', function () + { + const stage = new PIXI.Container(); + const graphics = new PIXI.Graphics(); + const eventSpy = sinon.spy(); + const pointer = new MockPointer(stage); + + stage.addChild(graphics); + graphics.beginFill(0xFFFFFF); + graphics.drawRect(0, 0, 50, 50); + graphics.interactive = true; + graphics.on('mouseover', eventSpy); + + pointer.mousemove(10, 10); + + expect(eventSpy).to.have.been.called; + }); + + it('should call mouseout handler', function () + { + const stage = new PIXI.Container(); + const graphics = new PIXI.Graphics(); + const eventSpy = sinon.spy(); + const pointer = new MockPointer(stage); + + stage.addChild(graphics); + graphics.beginFill(0xFFFFFF); + graphics.drawRect(0, 0, 50, 50); + graphics.interactive = true; + graphics.on('mouseout', eventSpy); + + pointer.mousemove(10, 10); + pointer.mousemove(60, 60); + + expect(eventSpy).to.have.been.called; + }); + }); + + describe('add/remove events', function () + { + let stub; + + before(function () + { + stub = sinon.stub(PIXI.interaction.InteractionManager.prototype, 'setTargetElement'); + }); + + after(function () + { + stub.restore(); + }); + + it('should add and remove pointer events to document', function () + { + const manager = new PIXI.interaction.InteractionManager(sinon.stub()); + const addSpy = sinon.spy(window.document, 'addEventListener'); + const removeSpy = sinon.spy(window.document, 'removeEventListener'); + + manager.interactionDOMElement = { style: {}, addEventListener: sinon.stub(), removeEventListener: sinon.stub() }; + manager.supportsPointerEvents = true; + + manager.addEvents(); + + expect(addSpy).to.have.been.calledOnce; + expect(addSpy).to.have.been.calledWith('pointermove'); + + manager.removeEvents(); + + expect(removeSpy).to.have.been.calledOnce; + expect(removeSpy).to.have.been.calledWith('pointermove'); + + addSpy.restore(); + removeSpy.restore(); + }); + + it('should add and remove pointer events to window', function () + { + const manager = new PIXI.interaction.InteractionManager(sinon.stub()); + const addSpy = sinon.spy(window, 'addEventListener'); + const removeSpy = sinon.spy(window, 'removeEventListener'); + + manager.interactionDOMElement = { style: {}, addEventListener: sinon.stub(), removeEventListener: sinon.stub() }; + manager.supportsPointerEvents = true; + + manager.addEvents(); + + expect(addSpy).to.have.been.calledTwice; + expect(addSpy).to.have.been.calledWith('pointercancel'); + expect(addSpy).to.have.been.calledWith('pointerup'); + + manager.removeEvents(); + + expect(removeSpy).to.have.been.calledTwice; + expect(removeSpy).to.have.been.calledWith('pointercancel'); + expect(removeSpy).to.have.been.calledWith('pointerup'); + + addSpy.restore(); + removeSpy.restore(); + }); + + it('should add and remove pointer events to element', function () + { + const manager = new PIXI.interaction.InteractionManager(sinon.stub()); + const element = { style: {}, addEventListener: sinon.stub(), removeEventListener: sinon.stub() }; + + manager.interactionDOMElement = element; + manager.supportsPointerEvents = true; + + manager.addEvents(); + + expect(element.addEventListener).to.have.been.calledThrice; + expect(element.addEventListener).to.have.been.calledWith('pointerdown'); + expect(element.addEventListener).to.have.been.calledWith('pointerleave'); + expect(element.addEventListener).to.have.been.calledWith('pointerover'); + + manager.removeEvents(); + + expect(element.removeEventListener).to.have.been.calledThrice; + expect(element.removeEventListener).to.have.been.calledWith('pointerdown'); + expect(element.removeEventListener).to.have.been.calledWith('pointerleave'); + expect(element.removeEventListener).to.have.been.calledWith('pointerover'); + }); + + it('should add and remove mouse events to document', function () + { + const manager = new PIXI.interaction.InteractionManager(sinon.stub()); + const addSpy = sinon.spy(window.document, 'addEventListener'); + const removeSpy = sinon.spy(window.document, 'removeEventListener'); + + manager.interactionDOMElement = { style: {}, addEventListener: sinon.stub(), removeEventListener: sinon.stub() }; + manager.supportsPointerEvents = false; + + manager.addEvents(); + + expect(addSpy).to.have.been.calledOnce; + expect(addSpy).to.have.been.calledWith('mousemove'); + + manager.removeEvents(); + + expect(removeSpy).to.have.been.calledOnce; + expect(removeSpy).to.have.been.calledWith('mousemove'); + + addSpy.restore(); + removeSpy.restore(); + }); + + it('should add and remove mouse events to window', function () + { + const manager = new PIXI.interaction.InteractionManager(sinon.stub()); + const addSpy = sinon.spy(window, 'addEventListener'); + const removeSpy = sinon.spy(window, 'removeEventListener'); + + manager.interactionDOMElement = { style: {}, addEventListener: sinon.stub(), removeEventListener: sinon.stub() }; + manager.supportsPointerEvents = false; + + manager.addEvents(); + + expect(addSpy).to.have.been.calledOnce; + expect(addSpy).to.have.been.calledWith('mouseup'); + + manager.removeEvents(); + + expect(removeSpy).to.have.been.calledOnce; + expect(removeSpy).to.have.been.calledWith('mouseup'); + + addSpy.restore(); + removeSpy.restore(); + }); + + it('should add and remove mouse events to element', function () + { + const manager = new PIXI.interaction.InteractionManager(sinon.stub()); + const element = { style: {}, addEventListener: sinon.stub(), removeEventListener: sinon.stub() }; + + manager.interactionDOMElement = element; + manager.supportsPointerEvents = false; + manager.supportsTouchEvents = false; + + manager.addEvents(); + + expect(element.addEventListener).to.have.been.calledThrice; + expect(element.addEventListener).to.have.been.calledWith('mousedown'); + expect(element.addEventListener).to.have.been.calledWith('mouseout'); + expect(element.addEventListener).to.have.been.calledWith('mouseover'); + + manager.removeEvents(); + + expect(element.removeEventListener).to.have.been.calledThrice; + expect(element.removeEventListener).to.have.been.calledWith('mousedown'); + expect(element.removeEventListener).to.have.been.calledWith('mouseout'); + expect(element.removeEventListener).to.have.been.calledWith('mouseover'); + }); + + it('should add and remove touch events to element', function () + { + const manager = new PIXI.interaction.InteractionManager(sinon.stub()); + const element = { style: {}, addEventListener: sinon.stub(), removeEventListener: sinon.stub() }; + + manager.interactionDOMElement = element; + manager.supportsPointerEvents = false; + manager.supportsTouchEvents = true; + + manager.addEvents(); + + expect(element.addEventListener).to.have.been.calledWith('touchstart'); + expect(element.addEventListener).to.have.been.calledWith('touchcancel'); + expect(element.addEventListener).to.have.been.calledWith('touchend'); + expect(element.addEventListener).to.have.been.calledWith('touchmove'); + + manager.removeEvents(); + + expect(element.removeEventListener).to.have.been.calledWith('touchstart'); + expect(element.removeEventListener).to.have.been.calledWith('touchcancel'); + expect(element.removeEventListener).to.have.been.calledWith('touchend'); + expect(element.removeEventListener).to.have.been.calledWith('touchmove'); + }); + }); + describe('onClick', function () { it('should call handler when inside', function () @@ -364,7 +639,6 @@ expect(scene.parentCallback).to.have.been.calledOnce; }); - /* TODO: Fix #3596 it('should callback parent and behind child when clicking overlap', function () { const stage = new PIXI.Container(); @@ -382,7 +656,6 @@ expect(scene.frontChildCallback).to.not.have.been.called; expect(scene.parentCallback).to.have.been.calledOnce; }); - */ it('should callback parent and behind child when clicking behind child', function () { @@ -461,4 +734,241 @@ }); }); }); + + describe('cursor changes', function () + { + it('cursor should be the cursor of interactive item', function () + { + const stage = new PIXI.Container(); + const graphics = new PIXI.Graphics(); + const pointer = new MockPointer(stage); + + stage.addChild(graphics); + graphics.beginFill(0xFFFFFF); + graphics.drawRect(0, 0, 50, 50); + graphics.interactive = true; + graphics.cursor = 'help'; + pointer.interaction.cursorStyles.help = 'help'; + + pointer.mousemove(10, 10); + + expect(pointer.renderer.view.style.cursor).to.equal('help'); + }); + + it('should return cursor to default on mouseout', function () + { + const stage = new PIXI.Container(); + const graphics = new PIXI.Graphics(); + const pointer = new MockPointer(stage); + + stage.addChild(graphics); + graphics.beginFill(0xFFFFFF); + graphics.drawRect(0, 0, 50, 50); + graphics.interactive = true; + graphics.cursor = 'help'; + pointer.interaction.cursorStyles.help = 'help'; + + pointer.mousemove(10, 10); + pointer.mousemove(60, 60); + + expect(pointer.renderer.view.style.cursor).to.equal(pointer.interaction.cursorStyles.default); + }); + + it('should still be the over cursor after a click', function () + { + const stage = new PIXI.Container(); + const graphics = new PIXI.Graphics(); + const pointer = new MockPointer(stage); + + stage.addChild(graphics); + graphics.beginFill(0xFFFFFF); + graphics.drawRect(0, 0, 50, 50); + graphics.interactive = true; + graphics.cursor = 'help'; + pointer.interaction.cursorStyles.help = 'help'; + + pointer.mousemove(10, 10); + pointer.click(10, 10); + + expect(pointer.renderer.view.style.cursor).to.equal('help'); + }); + + it('should return cursor to default when mouse leaves renderer', function () + { + const stage = new PIXI.Container(); + const graphics = new PIXI.Graphics(); + const pointer = new MockPointer(stage); + + stage.addChild(graphics); + graphics.beginFill(0xFFFFFF); + graphics.drawRect(0, 0, 50, 50); + graphics.interactive = true; + graphics.cursor = 'help'; + pointer.interaction.cursorStyles.help = 'help'; + + pointer.mousemove(10, 10); + pointer.mousemove(-10, 60); + + expect(pointer.renderer.view.style.cursor).to.equal(pointer.interaction.cursorStyles.default); + }); + + it('cursor callback should be called', function () + { + const stage = new PIXI.Container(); + const graphics = new PIXI.Graphics(); + const overSpy = sinon.spy(); + const defaultSpy = sinon.spy(); + const pointer = new MockPointer(stage); + + stage.addChild(graphics); + graphics.beginFill(0xFFFFFF); + graphics.drawRect(0, 0, 50, 50); + graphics.interactive = true; + graphics.cursor = 'help'; + pointer.interaction.cursorStyles.help = overSpy; + pointer.interaction.cursorStyles.default = defaultSpy; + + pointer.mousemove(10, 10); + pointer.mousemove(60, 60); + + expect(overSpy).to.have.been.called; + expect(defaultSpy).to.have.been.called; + }); + + it('cursor callback should only be called if the cursor actually changed', function () + { + const stage = new PIXI.Container(); + const graphics = new PIXI.Graphics(); + const defaultSpy = sinon.spy(); + const pointer = new MockPointer(stage); + + stage.addChild(graphics); + graphics.beginFill(0xFFFFFF); + graphics.drawRect(0, 0, 50, 50); + graphics.interactive = true; + graphics.cursor = null; + pointer.interaction.cursorStyles.default = defaultSpy; + + pointer.mousemove(10, 10); + pointer.mousemove(20, 20); + + expect(defaultSpy).to.have.been.calledOnce; + }); + + it('cursor style object should be fully applied', function () + { + const stage = new PIXI.Container(); + const graphics = new PIXI.Graphics(); + const pointer = new MockPointer(stage); + + stage.addChild(graphics); + graphics.beginFill(0xFFFFFF); + graphics.drawRect(0, 0, 50, 50); + graphics.interactive = true; + graphics.cursor = 'help'; + pointer.interaction.cursorStyles.help = { + cursor: 'none', + display: 'none', + }; + + pointer.mousemove(10, 10); + + expect(pointer.renderer.view.style.cursor).to.equal('none'); + expect(pointer.renderer.view.style.display).to.equal('none'); + }); + + it('should not change cursor style if no cursor style provided', function () + { + const stage = new PIXI.Container(); + const graphics = new PIXI.Graphics(); + const pointer = new MockPointer(stage); + + stage.addChild(graphics); + graphics.beginFill(0xFFFFFF); + graphics.drawRect(0, 0, 50, 50); + graphics.interactive = true; + graphics.cursor = 'pointer'; + pointer.interaction.cursorStyles.pointer = null; + pointer.interaction.cursorStyles.default = null; + + pointer.mousemove(10, 10); + expect(pointer.renderer.view.style.cursor).to.equal(''); + + pointer.mousemove(60, 60); + expect(pointer.renderer.view.style.cursor).to.equal(''); + }); + }); + + describe('recursive hitTesting', function () + { + function getScene() + { + const stage = new PIXI.Container(); + const behindChild = new PIXI.Graphics(); + const middleChild = new PIXI.Graphics(); + const frontChild = new PIXI.Graphics(); + + behindChild.beginFill(0xFF); + behindChild.drawRect(0, 0, 50, 50); + + middleChild.beginFill(0xFF00); + middleChild.drawRect(0, 0, 50, 50); + + frontChild.beginFill(0xFF0000); + frontChild.drawRect(0, 0, 50, 50); + + stage.addChild(behindChild, middleChild, frontChild); + + return { + behindChild, + middleChild, + frontChild, + stage, + }; + } + + describe('when frontChild is interactive', function () + { + it('should stop hitTesting after first hit', function () + { + const scene = getScene(); + const pointer = new MockPointer(scene.stage); + const frontHitTest = sinon.spy(scene.frontChild, 'containsPoint'); + const middleHitTest = sinon.spy(scene.middleChild, 'containsPoint'); + const behindHitTest = sinon.spy(scene.behindChild, 'containsPoint'); + + scene.frontChild.interactive = true; + scene.middleChild.interactive = true; + scene.behindChild.interactive = true; + + pointer.mousedown(25, 25); + + expect(frontHitTest).to.have.been.calledOnce; + expect(middleHitTest).to.not.have.been.called; + expect(behindHitTest).to.not.have.been.called; + }); + }); + + describe('when frontChild is not interactive', function () + { + it('should stop hitTesting after first hit', function () + { + const scene = getScene(); + const pointer = new MockPointer(scene.stage); + const frontHitTest = sinon.spy(scene.frontChild, 'containsPoint'); + const middleHitTest = sinon.spy(scene.middleChild, 'containsPoint'); + const behindHitTest = sinon.spy(scene.behindChild, 'containsPoint'); + + scene.frontChild.interactive = false; + scene.middleChild.interactive = true; + scene.behindChild.interactive = true; + + pointer.mousedown(25, 25); + + expect(frontHitTest).to.not.have.been.called; + expect(middleHitTest).to.have.been.calledOnce; + expect(behindHitTest).to.not.have.been.called; + }); + }); + }); }); diff --git a/package.json b/package.json index 6a366ba..71ade15 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pixi.js", - "version": "4.3.4", + "version": "4.3.5", "description": "Pixi.js is a fast lightweight 2D library that works across all devices.", "author": "Mat Groves", "contributors": [ @@ -61,7 +61,7 @@ "ismobilejs": "^0.4.0", "object-assign": "^4.0.1", "pixi-gl-core": "^1.0.3", - "resource-loader": "^2.0.4" + "resource-loader": "^2.0.6" }, "devDependencies": { "babel-cli": "^6.18.0", @@ -70,9 +70,9 @@ "babel-preset-es2015": "^6.14.0", "babelify": "^7.3.0", "del": "^2.2.0", - "electron-prebuilt": "^1.3.2", + "electron": "^1.4.15", "eslint": "^3.5.0", - "floss": "^1.2.0", + "floss": "^2.0.1", "gh-pages": "^0.11.0", "jaguarjs-jsdoc": "^1.0.1", "js-md5": "^0.4.1", diff --git a/src/accessibility/AccessibilityManager.js b/src/accessibility/AccessibilityManager.js index df2716a..f16bcb1 100644 --- a/src/accessibility/AccessibilityManager.js +++ b/src/accessibility/AccessibilityManager.js @@ -3,7 +3,7 @@ import accessibleTarget from './accessibleTarget'; // add some extra variables to the container.. -Object.assign( +core.utils.mixins.delayMixin( core.DisplayObject.prototype, accessibleTarget ); diff --git a/src/core/Application.js b/src/core/Application.js index ea0f058..5340578 100644 --- a/src/core/Application.js +++ b/src/core/Application.js @@ -1,6 +1,6 @@ import { autoDetectRenderer } from './autoDetectRenderer'; import Container from './display/Container'; -import Ticker from './ticker/Ticker'; +import { shared, Ticker } from './ticker'; /** * Convenience class to create a new PIXI application. @@ -35,9 +35,9 @@ * @param {boolean} [noWebGL=false] - prevents selection of WebGL renderer, even if such is present * @param {boolean} [options.legacy=false] - If true Pixi will aim to ensure compatibility * with older / less advanced devices. If you experience unexplained flickering try setting this to true. - + * @param {boolean} [useSharedTicker=false] - `true` to use PIXI.ticker.shared, `false` to create new ticker. */ - constructor(width, height, options, noWebGL) + constructor(width, height, options, noWebGL, useSharedTicker = false) { /** * WebGL renderer if available, otherwise CanvasRenderer @@ -46,23 +46,46 @@ this.renderer = autoDetectRenderer(width, height, options, noWebGL); /** - * The root display container that's renderered. + * The root display container that's rendered. * @member {PIXI.Container} */ this.stage = new Container(); /** + * Internal reference to the ticker + * @member {PIXI.ticker.Ticker} + * @private + */ + this._ticker = null; + + /** * Ticker for doing render updates. * @member {PIXI.ticker.Ticker} + * @default PIXI.ticker.shared */ - this.ticker = new Ticker(); - - this.ticker.add(this.render, this); + this.ticker = useSharedTicker ? shared : new Ticker(); // Start the rendering this.start(); } + set ticker(ticker) // eslint-disable-line require-jsdoc + { + if (this._ticker) + { + this._ticker.remove(this.render, this); + } + this._ticker = ticker; + if (ticker) + { + ticker.add(this.render, this); + } + } + get ticker() // eslint-disable-line require-jsdoc + { + return this._ticker; + } + /** * Render the current stage. */ @@ -76,7 +99,7 @@ */ stop() { - this.ticker.stop(); + this._ticker.stop(); } /** @@ -84,7 +107,7 @@ */ start() { - this.ticker.start(); + this._ticker.start(); } /** @@ -98,13 +121,22 @@ } /** + * Reference to the renderer's screen rectangle. Its safe to use as filterArea or hitArea for whole screen + * @member {PIXI.Rectangle} + * @readonly + */ + get screen() + { + return this.renderer.screen; + } + + /** * Destroy and don't use after this. * @param {Boolean} [removeView=false] Automatically remove canvas from DOM. */ destroy(removeView) { this.stop(); - this.ticker.remove(this.render, this); this.ticker = null; this.stage.destroy(); diff --git a/src/core/Shader.js b/src/core/Shader.js new file mode 100644 index 0000000..78b17e6 --- /dev/null +++ b/src/core/Shader.js @@ -0,0 +1,46 @@ +import { GLShader } from 'pixi-gl-core'; +import settings from './settings'; + +function checkPrecision(src, def) +{ + if (src instanceof Array) + { + if (src[0].substring(0, 9) !== 'precision') + { + const copy = src.slice(0); + + copy.unshift(`precision ${def} float;`); + + return copy; + } + } + else if (src.substring(0, 9) !== 'precision') + { + return `precision ${def} float;\n${src}`; + } + + return src; +} + +/** + * Wrapper class, webGL Shader for Pixi. + * Adds precision string if vertexSrc or fragmentSrc have no mention of it. + * + * @class + * @extends GLShader + * @memberof PIXI + */ +export default class Shader extends GLShader +{ + /** + * + * @param {WebGLRenderingContext} gl - The current WebGL rendering context + * @param {string|string[]} vertexSrc - The vertex shader source as an array of strings. + * @param {string|string[]} fragmentSrc - The fragment shader source as an array of strings. + */ + constructor(gl, vertexSrc, fragmentSrc) + { + super(gl, checkPrecision(vertexSrc, settings.PRECISION_VERTEX), + checkPrecision(fragmentSrc, settings.PRECISION_FRAGMENT)); + } +} diff --git a/src/core/const.js b/src/core/const.js index ef6f662..3de0efe 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -188,7 +188,7 @@ * The gc modes that are supported by pixi. * * The {@link PIXI.settings.GC_MODE} Garbage Collection mode for pixi textures is AUTO - * If set to GC_MODE, the renderer will occasianally check textures usage. If they are not + * If set to GC_MODE, the renderer will occasionally check textures usage. If they are not * used for a specified period of time they will be removed from the GPU. They will of course * be uploaded again when they are required. This is a silent behind the scenes process that * should ensure that the GPU does not get filled up. diff --git a/src/core/display/Container.js b/src/core/display/Container.js index ed8cd5f..17fde8d 100644 --- a/src/core/display/Container.js +++ b/src/core/display/Container.js @@ -73,11 +73,12 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.push(child); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -108,11 +109,12 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.splice(index, 0, child); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -225,10 +227,11 @@ if (index === -1) return null; child.parent = null; + // ensure child transform will be recalculated + child.transform._parentID = -1; removeItems(this.children, index, 1); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -249,11 +252,12 @@ { const child = this.getChildAt(index); + // ensure child transform will be recalculated.. child.parent = null; + child.transform._parentID = -1; removeItems(this.children, index, 1); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -284,13 +288,12 @@ for (let i = 0; i < removed.length; ++i) { removed[i].parent = null; + if (removed[i].transform) + { + removed[i].transform._parentID = -1; + } } - // ensure a transform will be recalculated.. - if (this.transform) - { - this.transform._parentID = -1; - } this._boundsID++; this.onChildrenChange(beginIndex); diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index f37f2f5..be4c4fb 100644 --- a/src/core/graphics/Graphics.js +++ b/src/core/graphics/Graphics.js @@ -28,8 +28,9 @@ { /** * + * @param {boolean} [nativeLines=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP */ - constructor() + constructor(nativeLines = false) { super(); @@ -50,6 +51,13 @@ this.lineWidth = 0; /** + * If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * + * @member {boolean} + */ + this.nativeLines = nativeLines; + + /** * The color of any lines drawn. * * @member {string} @@ -757,28 +765,14 @@ if (!this._spriteRect) { - if (!Graphics._SPRITE_TEXTURE) - { - Graphics._SPRITE_TEXTURE = RenderTexture.create(10, 10); - - const canvas = document.createElement('canvas'); - - canvas.width = 10; - canvas.height = 10; - - const context = canvas.getContext('2d'); - - context.fillStyle = 'white'; - context.fillRect(0, 0, 10, 10); - - Graphics._SPRITE_TEXTURE = Texture.fromCanvas(canvas); - } - - this._spriteRect = new Sprite(Graphics._SPRITE_TEXTURE); + this._spriteRect = new Sprite(new Texture(Texture.WHITE)); } + + const sprite = this._spriteRect; + if (this.tint === 0xffffff) { - this._spriteRect.tint = this.graphicsData[0].fillColor; + sprite.tint = this.graphicsData[0].fillColor; } else { @@ -792,20 +786,21 @@ t1[1] *= t2[1]; t1[2] *= t2[2]; - this._spriteRect.tint = rgb2hex(t1); + sprite.tint = rgb2hex(t1); } - this._spriteRect.alpha = this.graphicsData[0].fillAlpha; - this._spriteRect.worldAlpha = this.worldAlpha * this._spriteRect.alpha; + sprite.alpha = this.graphicsData[0].fillAlpha; + sprite.worldAlpha = this.worldAlpha * sprite.alpha; + sprite.blendMode = this.blendMode; - Graphics._SPRITE_TEXTURE._frame.width = rect.width; - Graphics._SPRITE_TEXTURE._frame.height = rect.height; + sprite.texture._frame.width = rect.width; + sprite.texture._frame.height = rect.height; - this._spriteRect.transform.worldTransform = this.transform.worldTransform; + sprite.transform.worldTransform = this.transform.worldTransform; - this._spriteRect.anchor.set(-rect.x / rect.width, -rect.y / rect.height); - this._spriteRect._onAnchorUpdate(); + sprite.anchor.set(-rect.x / rect.width, -rect.y / rect.height); + sprite._onAnchorUpdate(); - this._spriteRect._renderWebGL(renderer); + sprite._renderWebGL(renderer); } /** @@ -1031,6 +1026,7 @@ this.fillColor, this.fillAlpha, this.filling, + this.nativeLines, shape ); diff --git a/src/core/graphics/GraphicsData.js b/src/core/graphics/GraphicsData.js index f5a87ae..b6f03c0 100644 --- a/src/core/graphics/GraphicsData.js +++ b/src/core/graphics/GraphicsData.js @@ -14,14 +14,19 @@ * @param {number} fillColor - the color of the fill * @param {number} fillAlpha - the alpha of the fill * @param {boolean} fill - whether or not the shape is filled with a colour + * @param {boolean} nativeLines - the method for drawing lines * @param {PIXI.Circle|PIXI.Rectangle|PIXI.Ellipse|PIXI.Polygon} shape - The shape object to draw. */ - constructor(lineWidth, lineColor, lineAlpha, fillColor, fillAlpha, fill, shape) + constructor(lineWidth, lineColor, lineAlpha, fillColor, fillAlpha, fill, nativeLines, shape) { /** * @member {number} the width of the line to draw */ this.lineWidth = lineWidth; + /** + * @member {boolean} if true the liens will be draw using LINES instead of TRIANGLE_STRIP + */ + this.nativeLines = nativeLines; /** * @member {number} the color of the line to draw @@ -85,6 +90,7 @@ this.fillColor, this.fillAlpha, this.fill, + this.nativeLines, this.shape ); } diff --git a/src/core/graphics/webgl/GraphicsRenderer.js b/src/core/graphics/webgl/GraphicsRenderer.js index 14083fe..67f5e92 100644 --- a/src/core/graphics/webgl/GraphicsRenderer.js +++ b/src/core/graphics/webgl/GraphicsRenderer.js @@ -102,7 +102,15 @@ shaderTemp.uniforms.alpha = graphics.worldAlpha; renderer.bindVao(webGLData.vao); - webGLData.vao.draw(gl.TRIANGLE_STRIP, webGLData.indices.length); + + if (graphics.nativeLines) + { + gl.drawArrays(gl.LINES, 0, webGLData.points.length / 6); + } + else + { + webGLData.vao.draw(gl.TRIANGLE_STRIP, webGLData.indices.length); + } } } diff --git a/src/core/graphics/webgl/utils/buildLine.js b/src/core/graphics/webgl/utils/buildLine.js index ecf43f7..4d6862e 100644 --- a/src/core/graphics/webgl/utils/buildLine.js +++ b/src/core/graphics/webgl/utils/buildLine.js @@ -11,7 +11,29 @@ * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties * @param {object} webGLData - an object containing all the webGL-specific information to create this shape */ -export default function buildLine(graphicsData, webGLData) +export default function (graphicsData, webGLData) +{ + if (graphicsData.nativeLines) + { + buildNativeLine(graphicsData, webGLData); + } + else + { + buildLine(graphicsData, webGLData); + } +} + +/** + * Builds a line to draw using the poligon method. + * + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @private + * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties + * @param {object} webGLData - an object containing all the webGL-specific information to create this shape + */ +function buildLine(graphicsData, webGLData) { // TODO OPTIMISE! let points = graphicsData.points; @@ -224,3 +246,46 @@ indices.push(indexStart - 1); } + +/** + * Builds a line to draw using the gl.drawArrays(gl.LINES) method + * + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @private + * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties + * @param {object} webGLData - an object containing all the webGL-specific information to create this shape + */ +function buildNativeLine(graphicsData, webGLData) +{ + let i = 0; + const points = graphicsData.points; + + if (points.length === 0) return; + + const verts = webGLData.points; + const length = points.length / 2; + + // sort color + const color = hex2rgb(graphicsData.lineColor); + const alpha = graphicsData.lineAlpha; + const r = color[0] * alpha; + const g = color[1] * alpha; + const b = color[2] * alpha; + + for (i = 1; i < length; i++) + { + const p1x = points[(i - 1) * 2]; + const p1y = points[((i - 1) * 2) + 1]; + + const p2x = points[i * 2]; + const p2y = points[(i * 2) + 1]; + + verts.push(p1x, p1y); + verts.push(r, g, b, alpha); + + verts.push(p2x, p2y); + verts.push(r, g, b, alpha); + } +} diff --git a/src/core/graphics/webgl/utils/buildRoundedRectangle.js b/src/core/graphics/webgl/utils/buildRoundedRectangle.js index 9abc807..a6c8461 100644 --- a/src/core/graphics/webgl/utils/buildRoundedRectangle.js +++ b/src/core/graphics/webgl/utils/buildRoundedRectangle.js @@ -77,6 +77,26 @@ } /** + * Calculate a single point for a quadratic bezier curve. + * Utility function used by quadraticBezierCurve. + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @private + * @param {number} n1 - first number + * @param {number} n2 - second number + * @param {number} perc - percentage + * @return {number} the result + * + */ +function getPt(n1, n2, perc) +{ + const diff = n2 - n1; + + return n1 + (diff * perc); +} + +/** * Calculate the points for a quadratic bezier curve. (helper function..) * Based on: https://stackoverflow.com/questions/785097/how-do-i-implement-a-bezier-curve-in-c * @@ -105,13 +125,6 @@ let x = 0; let y = 0; - function getPt(n1, n2, perc) - { - const diff = n2 - n1; - - return n1 + (diff * perc); - } - for (let i = 0, j = 0; i <= n; ++i) { j = i / n; diff --git a/src/core/index.js b/src/core/index.js index f7a817c..4f414e8 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -30,6 +30,7 @@ export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; export { default as CanvasGraphicsRenderer } from './graphics/canvas/CanvasGraphicsRenderer'; +export { default as Spritesheet } from './textures/Spritesheet'; export { default as Texture } from './textures/Texture'; export { default as BaseTexture } from './textures/BaseTexture'; export { default as RenderTexture } from './textures/RenderTexture'; diff --git a/src/core/math/Matrix.js b/src/core/math/Matrix.js index 0b77477..3679860 100644 --- a/src/core/math/Matrix.js +++ b/src/core/math/Matrix.js @@ -13,45 +13,50 @@ export default class Matrix { /** - * + * @param {number} [a=1] - x scale + * @param {number} [b=0] - y skew + * @param {number} [c=0] - x skew + * @param {number} [d=1] - y scale + * @param {number} [tx=0] - x translation + * @param {number} [ty=0] - y translation */ - constructor() + constructor(a = 1, b = 0, c = 0, d = 1, tx = 0, ty = 0) { /** * @member {number} * @default 1 */ - this.a = 1; + this.a = a; /** * @member {number} * @default 0 */ - this.b = 0; + this.b = b; /** * @member {number} * @default 0 */ - this.c = 0; + this.c = c; /** * @member {number} * @default 1 */ - this.d = 1; + this.d = d; /** * @member {number} * @default 0 */ - this.tx = 0; + this.tx = tx; /** * @member {number} * @default 0 */ - this.ty = 0; + this.ty = ty; this.array = null; } diff --git a/src/core/renderers/SystemRenderer.js b/src/core/renderers/SystemRenderer.js index fa116a6..a24c775 100644 --- a/src/core/renderers/SystemRenderer.js +++ b/src/core/renderers/SystemRenderer.js @@ -1,5 +1,5 @@ import { sayHello, hex2string, hex2rgb } from '../utils'; -import { Matrix } from '../math'; +import { Matrix, Rectangle } from '../math'; import { RENDERER_TYPE } from '../const'; import settings from '../settings'; import Container from '../display/Container'; @@ -21,8 +21,8 @@ { /** * @param {string} system - The name of the system this renderer is for. - * @param {number} [width=800] - the width of the canvas view - * @param {number} [height=600] - the height of the canvas view + * @param {number} [screenWidth=800] - the width of the screen + * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false @@ -37,7 +37,7 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(system, width, height, options) + constructor(system, screenWidth, screenHeight, options) { super(); @@ -69,20 +69,13 @@ this.type = RENDERER_TYPE.UNKNOWN; /** - * The width of the canvas view + * Measurements of the screen. (0, 0, screenWidth, screenHeight) * - * @member {number} - * @default 800 - */ - this.width = width || 800; - - /** - * The height of the canvas view + * Its safe to use as filterArea or hitArea for whole stage * - * @member {number} - * @default 600 + * @member {PIXI.Rectangle} */ - this.height = height || 600; + this.screen = new Rectangle(0, 0, screenWidth || 800, screenHeight || 600); /** * The canvas element that everything is drawn to @@ -107,7 +100,7 @@ this.transparent = options.transparent; /** - * Whether the render view should be resized automatically + * Whether css dimensions of canvas view should be resized to screen dimensions automatically * * @member {boolean} */ @@ -192,23 +185,48 @@ } /** - * Resizes the canvas view to the specified width and height + * Same as view.width, actual number of pixels in the canvas by horizontal * - * @param {number} width - the new width of the canvas view - * @param {number} height - the new height of the canvas view + * @member {number} + * @readonly + * @default 800 */ - resize(width, height) + get width() { - this.width = width * this.resolution; - this.height = height * this.resolution; + return this.view.width; + } - this.view.width = this.width; - this.view.height = this.height; + /** + * Same as view.height, actual number of pixels in the canvas by vertical + * + * @member {number} + * @readonly + * @default 600 + */ + get height() + { + return this.view.height; + } + + /** + * Resizes the screen and canvas to the specified width and height + * Canvas dimensions are multiplied by resolution + * + * @param {number} screenWidth - the new width of the screen + * @param {number} screenHeight - the new height of the screen + */ + resize(screenWidth, screenHeight) + { + this.screen.width = screenWidth; + this.screen.height = screenHeight; + + this.view.width = screenWidth * this.resolution; + this.view.height = screenHeight * this.resolution; if (this.autoResize) { - this.view.style.width = `${this.width / this.resolution}px`; - this.view.style.height = `${this.height / this.resolution}px`; + this.view.style.width = `${screenWidth}px`; + this.view.style.height = `${screenHeight}px`; } } @@ -249,11 +267,10 @@ this.type = RENDERER_TYPE.UNKNOWN; - this.width = 0; - this.height = 0; - this.view = null; + this.screen = null; + this.resolution = 0; this.transparent = false; diff --git a/src/core/renderers/canvas/CanvasRenderer.js b/src/core/renderers/canvas/CanvasRenderer.js index 1bd2d5e..e84e49c 100644 --- a/src/core/renderers/canvas/CanvasRenderer.js +++ b/src/core/renderers/canvas/CanvasRenderer.js @@ -18,8 +18,8 @@ export default class CanvasRenderer extends SystemRenderer { /** - * @param {number} [width=800] - the width of the canvas view - * @param {number} [height=600] - the height of the canvas view + * @param {number} [screenWidth=800] - the width of the screen + * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false @@ -34,9 +34,9 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(width, height, options = {}) + constructor(screenWidth, screenHeight, options = {}) { - super('Canvas', width, height, options); + super('Canvas', screenWidth, screenHeight, options); this.type = RENDERER_TYPE.CANVAS; @@ -96,7 +96,7 @@ this.context = null; this.renderingToScreen = false; - this.resize(width, height); + this.resize(screenWidth, screenHeight); } /** @@ -283,12 +283,12 @@ * * @extends PIXI.SystemRenderer#resize * - * @param {number} width - The new width of the canvas view - * @param {number} height - The new height of the canvas view + * @param {number} screenWidth - the new width of the screen + * @param {number} screenHeight - the new height of the screen */ - resize(width, height) + resize(screenWidth, screenHeight) { - super.resize(width, height); + super.resize(screenWidth, screenHeight); // reset the scale mode.. oddly this seems to be reset when the canvas is resized. // surely a browser bug?? Let pixi fix that for you.. diff --git a/src/core/renderers/webgl/TextureManager.js b/src/core/renderers/webgl/TextureManager.js index b685c1a..8227289 100644 --- a/src/core/renderers/webgl/TextureManager.js +++ b/src/core/renderers/webgl/TextureManager.js @@ -61,7 +61,7 @@ * Updates and/or Creates a WebGL texture for the renderer's context. * * @param {PIXI.BaseTexture|PIXI.Texture} texture - the texture to update - * @param {Number} location - the location the texture will be bound to. + * @param {number} location - the location the texture will be bound to. * @return {GLTexture} The gl texture. */ updateTexture(texture, location) diff --git a/src/core/renderers/webgl/WebGLRenderer.js b/src/core/renderers/webgl/WebGLRenderer.js index e4cd8a0..830b382 100644 --- a/src/core/renderers/webgl/WebGLRenderer.js +++ b/src/core/renderers/webgl/WebGLRenderer.js @@ -31,8 +31,8 @@ { /** * - * @param {number} [width=0] - the width of the canvas view - * @param {number} [height=0] - the height of the canvas view + * @param {number} [screenWidth=800] - the width of the screen + * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false @@ -53,9 +53,9 @@ * @param {boolean} [options.legacy=false] - If true Pixi will aim to ensure compatibility * with older / less advanced devices. If you experiance unexplained flickering try setting this to true. */ - constructor(width, height, options = {}) + constructor(screenWidth, screenHeight, options = {}) { - super('WebGL', width, height, options); + super('WebGL', screenWidth, screenHeight, options); this.legacy = !!options.legacy; @@ -245,7 +245,7 @@ glCore._testingContext = gl; // setup the width/height properties and gl viewport - this.resize(this.width, this.height); + this.resize(this.screen.width, this.screen.height); } /** @@ -343,16 +343,16 @@ /** * Resizes the webGL view to the specified width and height. * - * @param {number} width - the new width of the webGL view - * @param {number} height - the new height of the webGL view + * @param {number} screenWidth - the new width of the screen + * @param {number} screenHeight - the new height of the screen */ - resize(width, height) + resize(screenWidth, screenHeight) { // if(width * this.resolution === this.width && height * this.resolution === this.height)return; - SystemRenderer.prototype.resize.call(this, width, height); + SystemRenderer.prototype.resize.call(this, screenWidth, screenHeight); - this.rootRenderTarget.resize(width, height); + this.rootRenderTarget.resize(screenWidth, screenHeight); if (this._activeRenderTarget === this.rootRenderTarget) { @@ -396,6 +396,26 @@ } /** + * Erases the render texture and fills the drawing area with a colour + * + * @param {PIXI.RenderTexture} renderTexture - The render texture to clear + * @param {number} [clearColor] - The colour + * @return {PIXI.WebGLRenderer} Returns itself. + */ + clearRenderTexture(renderTexture, clearColor) + { + const baseTexture = renderTexture.baseTexture; + const renderTarget = baseTexture._glRenderTargets[this.CONTEXT_UID]; + + if (renderTarget) + { + renderTarget.clear(clearColor); + } + + return this; + } + + /** * Binds a render texture for rendering * * @param {PIXI.RenderTexture} renderTexture - The render texture to render @@ -474,9 +494,10 @@ * Changes the current GLShader to the one given in parameter * * @param {PIXI.glCore.Shader} shader - the new glShader + * @param {boolean} [autoProject=true] - Whether automatically set the projection matrix * @return {PIXI.WebGLRenderer} Returns itself. */ - _bindGLShader(shader) + _bindGLShader(shader, autoProject) { // TODO cache if (this._activeShader !== shader) @@ -484,8 +505,14 @@ this._activeShader = shader; shader.bind(); - // automatically set the projection matrix - shader.uniforms.projectionMatrix = this._activeRenderTarget.projectionMatrix.toArray(true); + // `autoProject` normally would be a default parameter set to true + // but because of how Babel transpiles default parameters + // it hinders the performance of this method. + if (autoProject !== false) + { + // automatically set the projection matrix + shader.uniforms.projectionMatrix = this._activeRenderTarget.projectionMatrix.toArray(true); + } } return this; diff --git a/src/core/renderers/webgl/WebGLState.js b/src/core/renderers/webgl/WebGLState.js index ba6ecc5..6c846d5 100755 --- a/src/core/renderers/webgl/WebGLState.js +++ b/src/core/renderers/webgl/WebGLState.js @@ -82,18 +82,20 @@ push() { // next state.. - let state = this.stack[++this.stackIndex]; + let state = this.stack[this.stackIndex]; if (!state) { state = this.stack[this.stackIndex] = new Uint8Array(16); } + ++this.stackIndex; + // copy state.. // set active state so we can force overrides of gl state for (let i = 0; i < this.activeState.length; i++) { - this.activeState[i] = state[i]; + state[i] = this.activeState[i]; } } diff --git a/src/core/renderers/webgl/filters/Filter.js b/src/core/renderers/webgl/filters/Filter.js index 2c3eaad..4eb6128 100644 --- a/src/core/renderers/webgl/filters/Filter.js +++ b/src/core/renderers/webgl/filters/Filter.js @@ -9,7 +9,7 @@ * @memberof PIXI * @extends PIXI.Shader */ -class Filter extends Shader +export default class Filter extends Shader { /** * @param {string} [vertexSrc] - The source of the vertex shader. @@ -56,15 +56,16 @@ * @param {PIXI.RenderTarget} input - The input render target. * @param {PIXI.RenderTarget} output - The target to output to. * @param {boolean} clear - Should the output be cleared before rendering to it + * @param {object} [currentState] - It's current state of filter. + * There are some useful properties in the currentState : + * target, filters, sourceFrame, destinationFrame, renderTarget, resolution */ - apply(filterManager, input, output, clear) + apply(filterManager, input, output, clear, currentState) // eslint-disable-line no-unused-vars { // do as you please! - filterManager.applyFilter(this, input, output, clear); + filterManager.applyFilter(this, input, output, clear, currentState); // or just do a regular render.. } } - -export default Filter; diff --git a/src/core/renderers/webgl/filters/extractUniformsFromSrc.js b/src/core/renderers/webgl/filters/extractUniformsFromSrc.js index d78a0b1..4893214 100644 --- a/src/core/renderers/webgl/filters/extractUniformsFromSrc.js +++ b/src/core/renderers/webgl/filters/extractUniformsFromSrc.js @@ -12,7 +12,7 @@ function extractUniformsFromString(string) { - const maskRegex = new RegExp('^(projectionMatrix|uSampler|filterArea)$'); + const maskRegex = new RegExp('^(projectionMatrix|uSampler|filterArea|filterClamp)$'); const uniforms = {}; let nameSplit; diff --git a/src/core/renderers/webgl/managers/FilterManager.js b/src/core/renderers/webgl/managers/FilterManager.js index 43fc869..6c7422d 100644 --- a/src/core/renderers/webgl/managers/FilterManager.js +++ b/src/core/renderers/webgl/managers/FilterManager.js @@ -2,6 +2,7 @@ import RenderTarget from '../utils/RenderTarget'; import Quad from '../utils/Quad'; import { Rectangle } from '../../../math'; +import Shader from '../../../Shader'; import * as filterTransforms from '../filters/filterTransforms'; import bitTwiddle from 'bit-twiddle'; @@ -22,8 +23,6 @@ this.filters = []; this.target = null; this.resolution = 1; - - this.firstRun = true; } } @@ -155,7 +154,7 @@ if (filters.length === 1) { - filters[0].apply(this, currentState.renderTarget, lastState.renderTarget, false); + filters[0].apply(this, currentState.renderTarget, lastState.renderTarget, false, currentState); this.freePotRenderTarget(currentState.renderTarget); } else @@ -177,7 +176,7 @@ for (i = 0; i < filters.length - 1; ++i) { - filters[i].apply(this, flip, flop, true); + filters[i].apply(this, flip, flop, true, currentState); const t = flip; @@ -185,7 +184,7 @@ flop = t; } - filters[i].apply(this, flip, lastState.renderTarget, false); + filters[i].apply(this, flip, lastState.renderTarget, false, currentState); this.freePotRenderTarget(flip); this.freePotRenderTarget(flop); @@ -212,16 +211,28 @@ const renderer = this.renderer; const gl = renderer.gl; - renderer.bindShader(filter, true); + let shader = filter.glShaders[renderer.CONTEXT_UID]; - // TODO theres a better way! - // quad can be a mesh and we then should be able to pull this off without accessing the shader directly - const shader = renderer.shaderManager.getGLShader(); - - // TODO change quad to mesh.. - if (!this.firstRun) + // cacheing.. + if (!shader) { - this.firstRun = true; + if (filter.glShaderKey) + { + shader = this.shaderCache[filter.glShaderKey]; + + if (!shader) + { + shader = new Shader(this.gl, filter.vertexSrc, filter.fragmentSrc); + + filter.glShaders[renderer.CONTEXT_UID] = this.shaderCache[filter.glShaderKey] = shader; + } + } + else + { + shader = filter.glShaders[renderer.CONTEXT_UID] = new Shader(this.gl, filter.vertexSrc, filter.fragmentSrc); + } + + // TODO - this only needs to be done once? renderer.bindVao(null); this.quad.initVao(shader); @@ -244,58 +255,146 @@ renderer.maskManager.pushScissorMask(null, renderer.maskManager.scissorData); } - // renderer._bindGLShader(shader); + renderer.bindShader(shader); - let currentState; - const uniforms = filter.uniforms; + // free unit 0 for us, doesn't matter what was there + // don't try to restore it, because syncUniforms can upload it to another slot + // and it'll be a problem + const tex = this.renderer.emptyTextures[0]; - if (uniforms.filterArea) - { - currentState = this.filterData.stack[this.filterData.index]; - - const filterArea = uniforms.filterArea; - - filterArea[0] = currentState.renderTarget.size.width; - filterArea[1] = currentState.renderTarget.size.height; - filterArea[2] = currentState.sourceFrame.x; - filterArea[3] = currentState.sourceFrame.y; - - filter.uniforms.filterArea = filterArea; - } - - // use this to clamp displaced texture coords so they belong to filterArea - // see displacementFilter fragment shader for an example - if (uniforms.filterClamp) - { - currentState = this.filterData.stack[this.filterData.index]; - - const filterClamp = uniforms.filterClamp; - - filterClamp[0] = 0; - filterClamp[1] = 0; - filterClamp[2] = (currentState.sourceFrame.width - 1) / currentState.renderTarget.size.width; - filterClamp[3] = (currentState.sourceFrame.height - 1) / currentState.renderTarget.size.height; - - filter.uniforms.filterClamp = filterClamp; - } - - renderer.shaderManager.setUniforms(filter.uniforms); + this.renderer.boundTextures[0] = tex; + // this syncs the pixi filters uniforms with glsl uniforms + this.syncUniforms(shader, filter); renderer.state.setBlendMode(filter.blendMode); - // temporary bypass cache.. - const tex = this.renderer.boundTextures[0]; - gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, input.texture.texture); this.quad.vao.draw(this.renderer.gl.TRIANGLES, 6, 0); - // restore cache. gl.bindTexture(gl.TEXTURE_2D, tex._glTextures[this.renderer.CONTEXT_UID].texture); } /** + * Uploads the uniforms of the filter. + * + * @param {GLShader} shader - The underlying gl shader. + * @param {PIXI.Filter} filter - The filter we are synchronizing. + */ + syncUniforms(shader, filter) + { + const uniformData = filter.uniformData; + const uniforms = filter.uniforms; + + // 0 is reserved for the pixi texture so we start at 1! + let textureCount = 1; + let currentState; + + // filterArea and filterClamp that are handled by FilterManager directly + // they must not appear in uniformData + + if (shader.uniforms.filterArea) + { + currentState = this.filterData.stack[this.filterData.index]; + + const filterArea = shader.uniforms.filterArea; + + filterArea[0] = currentState.renderTarget.size.width; + filterArea[1] = currentState.renderTarget.size.height; + filterArea[2] = currentState.sourceFrame.x; + filterArea[3] = currentState.sourceFrame.y; + + shader.uniforms.filterArea = filterArea; + } + + // use this to clamp displaced texture coords so they belong to filterArea + // see displacementFilter fragment shader for an example + if (shader.uniforms.filterClamp) + { + currentState = currentState || this.filterData.stack[this.filterData.index]; + + const filterClamp = shader.uniforms.filterClamp; + + filterClamp[0] = 0; + filterClamp[1] = 0; + filterClamp[2] = (currentState.sourceFrame.width - 1) / currentState.renderTarget.size.width; + filterClamp[3] = (currentState.sourceFrame.height - 1) / currentState.renderTarget.size.height; + + shader.uniforms.filterClamp = filterClamp; + } + + // TODO Cacheing layer.. + for (const i in uniformData) + { + if (uniformData[i].type === 'sampler2D' && uniforms[i] !== 0) + { + if (uniforms[i].baseTexture) + { + shader.uniforms[i] = this.renderer.bindTexture(uniforms[i].baseTexture, textureCount); + } + else + { + shader.uniforms[i] = textureCount; + + // TODO + // this is helpful as renderTargets can also be set. + // Although thinking about it, we could probably + // make the filter texture cache return a RenderTexture + // rather than a renderTarget + const gl = this.renderer.gl; + + this.renderer.boundTextures[textureCount] = this.renderer.emptyTextures[textureCount]; + gl.activeTexture(gl.TEXTURE0 + textureCount); + + uniforms[i].texture.bind(); + } + + textureCount++; + } + else if (uniformData[i].type === 'mat3') + { + // check if its pixi matrix.. + if (uniforms[i].a !== undefined) + { + shader.uniforms[i] = uniforms[i].toArray(true); + } + else + { + shader.uniforms[i] = uniforms[i]; + } + } + else if (uniformData[i].type === 'vec2') + { + // check if its a point.. + if (uniforms[i].x !== undefined) + { + const val = shader.uniforms[i] || new Float32Array(2); + + val[0] = uniforms[i].x; + val[1] = uniforms[i].y; + shader.uniforms[i] = val; + } + else + { + shader.uniforms[i] = uniforms[i]; + } + } + else if (uniformData[i].type === 'float') + { + if (shader.uniforms.data[i].value !== uniformData[i]) + { + shader.uniforms[i] = uniforms[i]; + } + } + else + { + shader.uniforms[i] = uniforms[i]; + } + } + } + + /** * Gets a render target from the pool, or creates a new one. * * @param {boolean} clear - Should we clear the render texture when we get it? @@ -442,7 +541,6 @@ renderTarget.resolution = resolution; renderTarget.defaultFrame.width = renderTarget.size.width = minWidth / resolution; renderTarget.defaultFrame.height = renderTarget.size.height = minHeight / resolution; - renderTarget.attachStencilBuffer(); return renderTarget; } diff --git a/src/core/settings.js b/src/core/settings.js index bb08cb0..f4dd087 100644 --- a/src/core/settings.js +++ b/src/core/settings.js @@ -77,11 +77,11 @@ * * @static * @memberof PIXI.settings - * @type {RegExp|string} + * @type {RegExp} * @example `@2x` - * @default /@(.+)x/ + * @default /@([0-9\.]+)x/ */ - RETINA_PREFIX: /@(.+)x/, + RETINA_PREFIX: /@([0-9\.]+)x/, /** * The default render options if none are supplied to {@link PIXI.WebGLRenderer} @@ -175,14 +175,24 @@ SCALE_MODE: 0, /** - * Default specify float precision in shaders. + * Default specify float precision in vertex shader. + * + * @static + * @memberof PIXI.settings + * @type {PIXI.PRECISION} + * @default PIXI.PRECISION.HIGH + */ + PRECISION_VERTEX: 'highp', + + /** + * Default specify float precision in fragment shader. * * @static * @memberof PIXI.settings * @type {PIXI.PRECISION} * @default PIXI.PRECISION.MEDIUM */ - PRECISION: 'mediump', + PRECISION_FRAGMENT: 'mediump', /** * Can we upload the same buffer in a single frame? diff --git a/src/core/sprites/Sprite.js b/src/core/sprites/Sprite.js index 28b0b18..121bf58 100644 --- a/src/core/sprites/Sprite.js +++ b/src/core/sprites/Sprite.js @@ -212,11 +212,11 @@ } else { - w0 = orig.width * (1 - anchor._x); - w1 = orig.width * -anchor._x; + w1 = -anchor._x * orig.width; + w0 = w1 + orig.width; - h0 = orig.height * (1 - anchor._y); - h1 = orig.height * -anchor._y; + h1 = -anchor._y * orig.height; + h0 = h1 + orig.height; } // xy @@ -269,11 +269,11 @@ const tx = wt.tx; const ty = wt.ty; - const w0 = (orig.width) * (1 - anchor._x); - const w1 = (orig.width) * -anchor._x; + const w1 = -anchor._x * orig.width; + const w0 = w1 + orig.width; - const h0 = orig.height * (1 - anchor._y); - const h1 = orig.height * -anchor._y; + const h1 = -anchor._y * orig.height; + const h0 = h1 + orig.height; // xy vertexData[0] = (a * w1) + (c * h1) + tx; @@ -440,7 +440,7 @@ * * @static * @param {number|string|PIXI.BaseTexture|HTMLCanvasElement|HTMLVideoElement} source Source to create texture from - * @return {PIXI.Sprite} The newly created texture + * @return {PIXI.Sprite} The newly created sprite */ static from(source) { @@ -538,8 +538,8 @@ } /** - * The tint applied to the sprite. This is a hex value. A value of - * 0xFFFFFF will remove any tint effect. + * The tint applied to the sprite. This is a hex value. + * A value of 0xFFFFFF will remove any tint effect. * * @member {number} * @default 0xFFFFFF diff --git a/src/core/sprites/canvas/CanvasTinter.js b/src/core/sprites/canvas/CanvasTinter.js index 1cf316b..27e5e0c 100644 --- a/src/core/sprites/canvas/CanvasTinter.js +++ b/src/core/sprites/canvas/CanvasTinter.js @@ -75,8 +75,8 @@ crop.width *= resolution; crop.height *= resolution; - canvas.width = crop.width; - canvas.height = crop.height; + canvas.width = Math.ceil(crop.width); + canvas.height = Math.ceil(crop.height); context.fillStyle = `#${(`00000${(color | 0).toString(16)}`).substr(-6)}`; @@ -130,8 +130,8 @@ crop.width *= resolution; crop.height *= resolution; - canvas.width = crop.width; - canvas.height = crop.height; + canvas.width = Math.ceil(crop.width); + canvas.height = Math.ceil(crop.height); context.globalCompositeOperation = 'copy'; context.fillStyle = `#${(`00000${(color | 0).toString(16)}`).substr(-6)}`; @@ -172,8 +172,8 @@ crop.width *= resolution; crop.height *= resolution; - canvas.width = crop.width; - canvas.height = crop.height; + canvas.width = Math.ceil(crop.width); + canvas.height = Math.ceil(crop.height); context.globalCompositeOperation = 'copy'; context.drawImage( diff --git a/src/core/sprites/webgl/SpriteRenderer.js b/src/core/sprites/webgl/SpriteRenderer.js index 2affcd5..7c352e2 100644 --- a/src/core/sprites/webgl/SpriteRenderer.js +++ b/src/core/sprites/webgl/SpriteRenderer.js @@ -30,7 +30,7 @@ /** * Number of values sent in the vertex buffer. - * positionX, positionY, colorR, colorG, colorB = 5 + * aVertexPosition(2), aTextureCoord(1), aColor(1), aTextureId(1) = 5 * * @member {number} */ @@ -75,7 +75,6 @@ this.shader = null; this.currentIndex = 0; - TICK = 0; this.groups = []; for (let k = 0; k < this.size; k++) diff --git a/src/core/textures/BaseTexture.js b/src/core/textures/BaseTexture.js index 853de7e..aeccf39 100644 --- a/src/core/textures/BaseTexture.js +++ b/src/core/textures/BaseTexture.js @@ -232,16 +232,24 @@ this.realWidth = this.source.naturalWidth || this.source.videoWidth || this.source.width; this.realHeight = this.source.naturalHeight || this.source.videoHeight || this.source.height; - this.width = this.realWidth / this.resolution; - this.height = this.realHeight / this.resolution; - - this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); + this._updateDimensions(); } this.emit('update', this); } /** + * Update dimensions from real values + */ + _updateDimensions() + { + this.width = this.realWidth / this.resolution; + this.height = this.realHeight / this.resolution; + + this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); + } + + /** * Load a source. * * If the source is not-immediately-available, such as an image that needs to be @@ -525,11 +533,7 @@ this.realWidth = Math.round(svgWidth * this.sourceScale); this.realHeight = Math.round(svgHeight * this.sourceScale); - this.width = this.realWidth / this.resolution; - this.height = this.realHeight / this.resolution; - - // Check pow2 after scale - this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); + this._updateDimensions(); // Create a canvas element const canvas = document.createElement('canvas'); @@ -690,4 +694,52 @@ return baseTexture; } + + /** + * Helper function that creates a base texture based on the source you provide. + * The source can be - image url, image element, canvas element. + * + * @static + * @param {string|HTMLImageElement|HTMLCanvasElement} source - The source to create base texture from. + * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values + * @param {number} [sourceScale=(auto)] - Scale for the original image, used with Svg images. + * @return {PIXI.BaseTexture} The new base texture. + */ + static from(source, scaleMode, sourceScale) + { + if (typeof source === 'string') + { + return BaseTexture.fromImage(source, undefined, scaleMode, sourceScale); + } + else if (source instanceof HTMLImageElement) + { + const imageUrl = source.src; + let baseTexture = BaseTextureCache[imageUrl]; + + if (!baseTexture) + { + baseTexture = new BaseTexture(source, scaleMode); + baseTexture.imageUrl = imageUrl; + + if (sourceScale) + { + baseTexture.sourceScale = sourceScale; + } + + // if there is an @2x at the end of the url we are going to assume its a highres image + baseTexture.resolution = getResolutionOfUrl(imageUrl); + + BaseTextureCache[imageUrl] = baseTexture; + } + + return baseTexture; + } + else if (source instanceof HTMLCanvasElement) + { + return BaseTexture.fromCanvas(source, scaleMode); + } + + // lets assume its a base texture! + return source; + } } diff --git a/src/core/textures/Spritesheet.js b/src/core/textures/Spritesheet.js new file mode 100644 index 0000000..d469c62 --- /dev/null +++ b/src/core/textures/Spritesheet.js @@ -0,0 +1,274 @@ +import { Rectangle, Texture } from '../'; +import { getResolutionOfUrl, TextureCache } from '../utils'; + +/** + * Utility class for maintaining reference to a collection + * of Textures on a single Spritesheet. + * + * @class + * @memberof PIXI + */ +export default class Spritesheet +{ + /** + * The maximum number of Textures to build per process. + * + * @type {number} + * @default 1000 + */ + static get BATCH_SIZE() + { + return 1000; + } + + /** + * @param {PIXI.BaseTexture} baseTexture Reference to the source BaseTexture object. + * @param {Object} data - Spritesheet image data. + * @param {string} [resolutionFilename] - The filename to consider when determining + * the resolution of the spritesheet. If not provided, the imageUrl will + * be used on the BaseTexture. + */ + constructor(baseTexture, data, resolutionFilename = null) + { + /** + * Reference to ths source texture + * @type {PIXI.BaseTexture} + */ + this.baseTexture = baseTexture; + + /** + * Map of spritesheet textures. + * @type {Object} + */ + this.textures = {}; + + /** + * Reference to the original JSON data. + * @type {Object} + */ + this.data = data; + + /** + * The resolution of the spritesheet. + * @type {number} + */ + this.resolution = this._updateResolution( + resolutionFilename || this.baseTexture.imageUrl + ); + + /** + * Map of spritesheet frames. + * @type {Object} + * @private + */ + this._frames = this.data.frames; + + /** + * Collection of frame names. + * @type {string[]} + * @private + */ + this._frameKeys = Object.keys(this._frames); + + /** + * Current batch index being processed. + * @type {number} + * @private + */ + this._batchIndex = 0; + + /** + * Callback when parse is completed. + * @type {Function} + * @private + */ + this._callback = null; + } + + /** + * Generate the resolution from the filename or fallback + * to the meta.scale field of the JSON data. + * + * @private + * @param {string} resolutionFilename - The filename to use for resolving + * the default resolution. + * @return {number} Resolution to use for spritesheet. + */ + _updateResolution(resolutionFilename) + { + const scale = this.data.meta.scale; + + // Use a defaultValue of `null` to check if a url-based resolution is set + let resolution = getResolutionOfUrl(resolutionFilename, null); + + // No resolution found via URL + if (resolution === null) + { + // Use the scale value or default to 1 + resolution = scale !== undefined ? parseFloat(scale) : 1; + } + + // For non-1 resolutions, update baseTexture + if (resolution !== 1) + { + this.baseTexture.resolution = resolution; + this.baseTexture.update(); + } + + return resolution; + } + + /** + * Parser spritesheet from loaded data. This is done asynchronously + * to prevent creating too many Texture within a single process. + * + * @param {Function} callback - Callback when complete returns + * a map of the Textures for this spritesheet. + */ + parse(callback) + { + this._batchIndex = 0; + this._callback = callback; + + if (this._frameKeys.length <= Spritesheet.BATCH_SIZE) + { + this._processFrames(0); + this._parseComplete(); + } + else + { + this._nextBatch(); + } + } + + /** + * Process a batch of frames + * + * @private + * @param {number} initialFrameIndex - The index of frame to start. + */ + _processFrames(initialFrameIndex) + { + let frameIndex = initialFrameIndex; + const maxFrames = Spritesheet.BATCH_SIZE; + + while (frameIndex - initialFrameIndex < maxFrames && frameIndex < this._frameKeys.length) + { + const i = this._frameKeys[frameIndex]; + const rect = this._frames[i].frame; + + if (rect) + { + let frame = null; + let trim = null; + const orig = new Rectangle( + 0, + 0, + this._frames[i].sourceSize.w / this.resolution, + this._frames[i].sourceSize.h / this.resolution + ); + + if (this._frames[i].rotated) + { + frame = new Rectangle( + rect.x / this.resolution, + rect.y / this.resolution, + rect.h / this.resolution, + rect.w / this.resolution + ); + } + else + { + frame = new Rectangle( + rect.x / this.resolution, + rect.y / this.resolution, + rect.w / this.resolution, + rect.h / this.resolution + ); + } + + // Check to see if the sprite is trimmed + if (this._frames[i].trimmed) + { + trim = new Rectangle( + this._frames[i].spriteSourceSize.x / this.resolution, + this._frames[i].spriteSourceSize.y / this.resolution, + rect.w / this.resolution, + rect.h / this.resolution + ); + } + + this.textures[i] = new Texture( + this.baseTexture, + frame, + orig, + trim, + this._frames[i].rotated ? 2 : 0 + ); + + // lets also add the frame to pixi's global cache for fromFrame and fromImage functions + TextureCache[i] = this.textures[i]; + } + + frameIndex++; + } + } + + /** + * The parse has completed. + * + * @private + */ + _parseComplete() + { + const callback = this._callback; + + this._callback = null; + this._batchIndex = 0; + callback.call(this, this.textures); + } + + /** + * Begin the next batch of textures. + * + * @private + */ + _nextBatch() + { + this._processFrames(this._batchIndex * Spritesheet.BATCH_SIZE); + this._batchIndex++; + setTimeout(() => + { + if (this._batchIndex * Spritesheet.BATCH_SIZE < this._frameKeys.length) + { + this._nextBatch(); + } + else + { + this._parseComplete(); + } + }, 0); + } + + /** + * Destroy Spritesheet and don't use after this. + * + * @param {boolean} [destroyBase=false] Whether to destroy the base texture as well + */ + destroy(destroyBase = false) + { + for (const i in this.textures) + { + this.textures[i].destroy(); + } + this._frames = null; + this._frameKeys = null; + this.data = null; + this.textures = null; + if (destroyBase) + { + this.baseTexture.destroy(); + } + this.baseTexture = null; + } +} diff --git a/src/core/textures/Texture.js b/src/core/textures/Texture.js index 6403d4f..afe4254 100644 --- a/src/core/textures/Texture.js +++ b/src/core/textures/Texture.js @@ -3,7 +3,7 @@ import TextureUvs from './TextureUvs'; import EventEmitter from 'eventemitter3'; import { Rectangle } from '../math'; -import { TextureCache, BaseTextureCache } from '../utils'; +import { TextureCache, BaseTextureCache, getResolutionOfUrl } from '../utils'; /** * A texture stores the information that represents an image or part of an image. It cannot be added @@ -365,7 +365,8 @@ * The source can be - frame id, image url, video url, canvas element, video element, base texture * * @static - * @param {number|string|PIXI.BaseTexture|HTMLCanvasElement|HTMLVideoElement} source - Source to create texture from + * @param {number|string|HTMLImageElement|HTMLCanvasElement|HTMLVideoElement|PIXI.BaseTexture} + * source - Source to create texture from * @return {PIXI.Texture} The newly created texture */ static from(source) @@ -393,7 +394,7 @@ } else if (source instanceof HTMLImageElement) { - return new Texture(new BaseTexture(source)); + return new Texture(BaseTexture.from(source)); } else if (source instanceof HTMLCanvasElement) { @@ -413,6 +414,43 @@ } /** + * Create a texture from a source and add to the cache. + * + * @static + * @param {HTMLImageElement|HTMLCanvasElement} source - The input source. + * @param {String} imageUrl - File name of texture, for cache and resolving resolution. + * @param {String} [name] - Human readible name for the texture cache. If no name is + * specified, only `imageUrl` will be used as the cache ID. + * @return {PIXI.Texture} Output texture + */ + static fromLoader(source, imageUrl, name) + { + const baseTexture = new BaseTexture(source, null, getResolutionOfUrl(imageUrl)); + const texture = new Texture(baseTexture); + + baseTexture.imageUrl = imageUrl; + + // No name, use imageUrl instead + if (!name) + { + name = imageUrl; + } + + // lets also add the frame to pixi's global cache for fromFrame and fromImage fucntions + BaseTextureCache[name] = baseTexture; + TextureCache[name] = texture; + + // also add references by url if they are different. + if (name !== imageUrl) + { + BaseTextureCache[imageUrl] = baseTexture; + TextureCache[imageUrl] = texture; + } + + return texture; + } + + /** * Adds a texture to the global TextureCache. This cache is shared across the whole PIXI object. * * @static @@ -522,6 +560,29 @@ } } +function createWhiteTexture() +{ + const canvas = document.createElement('canvas'); + + canvas.width = 10; + canvas.height = 10; + + const context = canvas.getContext('2d'); + + context.fillStyle = 'white'; + context.fillRect(0, 0, 10, 10); + + return new Texture(new BaseTexture(canvas)); +} + +function removeAllHandlers(tex) +{ + tex.destroy = function _emptyDestroy() { /* empty */ }; + tex.on = function _emptyOn() { /* empty */ }; + tex.once = function _emptyOnce() { /* empty */ }; + tex.emit = function _emptyEmit() { /* empty */ }; +} + /** * An empty texture, used often to not have to create multiple empty textures. * Can not be destroyed. @@ -530,7 +591,14 @@ * @constant */ Texture.EMPTY = new Texture(new BaseTexture()); -Texture.EMPTY.destroy = function _emptyDestroy() { /* empty */ }; -Texture.EMPTY.on = function _emptyOn() { /* empty */ }; -Texture.EMPTY.once = function _emptyOnce() { /* empty */ }; -Texture.EMPTY.emit = function _emptyEmit() { /* empty */ }; +removeAllHandlers(Texture.EMPTY); + +/** + * A white texture of 10x10 size, used for graphics and other things + * Can not be destroyed. + * + * @static + * @constant + */ +Texture.WHITE = createWhiteTexture(); +removeAllHandlers(Texture.WHITE); diff --git a/src/core/ticker/Ticker.js b/src/core/ticker/Ticker.js index d98a1f0..17a1517 100644 --- a/src/core/ticker/Ticker.js +++ b/src/core/ticker/Ticker.js @@ -67,11 +67,12 @@ * is based, this value is neither capped nor scaled. * If the platform supports DOMHighResTimeStamp, * this value will have a precision of 1 µs. + * Defaults to target frame time * * @member {number} - * @default 1 / TARGET_FPMS + * @default 16.66 */ - this.elapsedMS = 1 / settings.TARGET_FPMS; // default to target frame time + this.elapsedMS = 1 / settings.TARGET_FPMS; /** * The last time {@link PIXI.ticker.Ticker#update} was invoked. diff --git a/src/core/utils/index.js b/src/core/utils/index.js index 6ff609f..be44750 100644 --- a/src/core/utils/index.js +++ b/src/core/utils/index.js @@ -2,6 +2,7 @@ import settings from '../settings'; import EventEmitter from 'eventemitter3'; import pluginTarget from './pluginTarget'; +import * as mixins from './mixin'; import * as isMobile from 'ismobilejs'; let nextUid = 0; @@ -33,6 +34,7 @@ * @type {mixin} */ pluginTarget, + mixins, }; /** @@ -93,7 +95,7 @@ */ export function rgb2hex(rgb) { - return (((rgb[0] * 255) << 16) + ((rgb[1] * 255) << 8) + (rgb[2] * 255)); + return (((rgb[0] * 255) << 16) + ((rgb[1] * 255) << 8) + (rgb[2] * 255 | 0)); } /** @@ -368,3 +370,43 @@ * @private */ export const ProgramCache = {}; + +/** + * Destroys all texture in the cache + * + * @memberof PIXI.utils + * @function destroyTextureCache + */ +export function destroyTextureCache() +{ + let key; + + for (key in TextureCache) + { + TextureCache[key].destroy(); + } + for (key in BaseTextureCache) + { + BaseTextureCache[key].destroy(); + } +} + +/** + * Removes all textures from cache, but does not destroy them + * + * @memberof PIXI.utils + * @function clearTextureCache + */ +export function clearTextureCache() +{ + let key; + + for (key in TextureCache) + { + delete TextureCache[key]; + } + for (key in BaseTextureCache) + { + delete BaseTextureCache[key]; + } +} diff --git a/src/core/utils/mixin.js b/src/core/utils/mixin.js new file mode 100644 index 0000000..114de2a --- /dev/null +++ b/src/core/utils/mixin.js @@ -0,0 +1,59 @@ +/** + * Mixes all enumerable properties and methods from a source object to a target object. + * + * @memberof PIXI.utils.mixins + * @function mixin + * @param {object} target The prototype or instance that properties and methods should be added to. + * @param {object} source The source of properties and methods to mix in. + */ +export function mixin(target, source) +{ + if (!target || !source) return; + // in ES8/ES2017, this would be really easy: + // Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); + + // get all the enumerable property keys + const keys = Object.keys(source); + + // loop through properties + for (let i = 0; i < keys.length; ++i) + { + const propertyName = keys[i]; + + // Set the property using the property descriptor - this works for accessors and normal value properties + Object.defineProperty(target, propertyName, Object.getOwnPropertyDescriptor(source, propertyName)); + } +} + +const mixins = []; + +/** + * Queues a mixin to be handled towards the end of the initialization of PIXI, so that deprecation + * can take effect. + * + * @memberof PIXI.utils.mixins + * @function delayMixin + * @private + * @param {object} target The prototype or instance that properties and methods should be added to. + * @param {object} source The source of properties and methods to mix in. + */ +export function delayMixin(target, source) +{ + mixins.push(target, source); +} + +/** + * Handles all mixins queued via delayMixin(). + * + * @memberof PIXI.utils.mixins + * @function performMixins + * @private + */ +export function performMixins() +{ + for (let i = 0; i < mixins.length; i += 2) + { + mixin(mixins[i], mixins[i + 1]); + } + mixins.length = 0; +} diff --git a/src/deprecation.js b/src/deprecation.js index 045a19f..b9c73b3 100644 --- a/src/deprecation.js +++ b/src/deprecation.js @@ -5,6 +5,7 @@ import * as filters from './filters'; import * as prepare from './prepare'; import * as loaders from './loaders'; +import * as interaction from './interaction'; // provide method to give a stack track for warnings // useful for tracking-down where deprecated methods/properties/classes @@ -534,7 +535,7 @@ { parent: 'GC_MODES', target: 'GC_MODE' }, { parent: 'WRAP_MODES', target: 'WRAP_MODE' }, { parent: 'SCALE_MODES', target: 'SCALE_MODE' }, - { parent: 'PRECISION', target: 'PRECISION' }, + { parent: 'PRECISION', target: 'PRECISION_FRAGMENT' }, ]; for (let i = 0; i < defaults.length; i++) @@ -558,6 +559,32 @@ }); } +Object.defineProperties(core.settings, { + + /** + * @static + * @name PRECISION + * @memberof PIXI.settings + * @see PIXI.PRECISION + * @deprecated since version 4.4.0 + */ + PRECISION: { + enumerable: true, + get() + { + warn('PIXI.settings.PRECISION has been deprecated, please use PIXI.settings.PRECISION_FRAGMENT'); + + return core.settings.PRECISION_FRAGMENT; + }, + set(value) + { + warn('PIXI.settings.PRECISION has been deprecated, please use PIXI.settings.PRECISION_FRAGMENT'); + + core.settings.PRECISION_FRAGMENT = value; + }, + }, +}); + Object.defineProperties(extras, { /** @@ -694,7 +721,7 @@ warn('determineFontProperties is now deprecated, please use the static calculateFontProperties method, ' + 'e.g : Text.calculateFontProperties(fontStyle);'); - return Text.calculateFontProperties(fontStyle); + return core.Text.calculateFontProperties(fontStyle); }; Object.defineProperties(core.TextStyle.prototype, { @@ -1003,3 +1030,69 @@ }, }, }); + +/** + * @name PIXI.interaction.interactiveTarget#defaultCursor + * @static + * @type {number} + * @see PIXI.interaction.interactiveTarget#cursor + * @deprecated since 4.3.0 + */ +Object.defineProperty(interaction.interactiveTarget, 'defaultCursor', { + set(value) + { + warn('Property defaultCursor has been replaced with \'cursor\'. '); + this.cursor = value; + }, + get() + { + warn('Property defaultCursor has been replaced with \'cursor\'. '); + + return this.cursor; + }, + enumerable: true, +}); + +/** + * @name PIXI.interaction.InteractionManager#defaultCursorStyle + * @static + * @type {string} + * @see PIXI.interaction.InteractionManager#cursorStyles + * @deprecated since 4.3.0 + */ +Object.defineProperty(interaction.InteractionManager, 'defaultCursorStyle', { + set(value) + { + warn('Property defaultCursorStyle has been replaced with \'cursorStyles.default\'. '); + this.cursorStyles.default = value; + }, + get() + { + warn('Property defaultCursorStyle has been replaced with \'cursorStyles.default\'. '); + + return this.cursorStyles.default; + }, +}); + +/** + * @name PIXI.interaction.InteractionManager#currentCursorStyle + * @static + * @type {string} + * @see PIXI.interaction.InteractionManager#cursorStyles + * @deprecated since 4.3.0 + */ +Object.defineProperty(interaction.InteractionManager, 'currentCursorStyle', { + set(value) + { + warn('Property currentCursorStyle has been removed.' + + 'See the currentCursorMode property, which works differently.'); + this.currentCursorMode = value; + }, + get() + { + warn('Property currentCursorStyle has been removed.' + + 'See the currentCursorMode property, which works differently.'); + + return this.currentCursorMode; + }, +}); diff --git a/src/extras/BitmapText.js b/src/extras/BitmapText.js index fb6c343..0a0e150 100644 --- a/src/extras/BitmapText.js +++ b/src/extras/BitmapText.js @@ -455,6 +455,73 @@ return this._textHeight; } + + /** + * Register a bitmap font with data and a texture. + * + * @static + * @param {XMLDocument} xml - The XML document data. + * @param {PIXI.Texture} texture - Texture with all symbols. + * @return {Object} Result font object with font, size, lineHeight and char fields. + */ + static registerFont(xml, texture) + { + const data = {}; + const info = xml.getElementsByTagName('info')[0]; + const common = xml.getElementsByTagName('common')[0]; + + data.font = info.getAttribute('face'); + data.size = parseInt(info.getAttribute('size'), 10); + data.lineHeight = parseInt(common.getAttribute('lineHeight'), 10); + data.chars = {}; + + // parse letters + const letters = xml.getElementsByTagName('char'); + + for (let i = 0; i < letters.length; i++) + { + const letter = letters[i]; + const charCode = parseInt(letter.getAttribute('id'), 10); + + const textureRect = new core.Rectangle( + parseInt(letter.getAttribute('x'), 10) + texture.frame.x, + parseInt(letter.getAttribute('y'), 10) + texture.frame.y, + parseInt(letter.getAttribute('width'), 10), + parseInt(letter.getAttribute('height'), 10) + ); + + data.chars[charCode] = { + xOffset: parseInt(letter.getAttribute('xoffset'), 10), + yOffset: parseInt(letter.getAttribute('yoffset'), 10), + xAdvance: parseInt(letter.getAttribute('xadvance'), 10), + kerning: {}, + texture: new core.Texture(texture.baseTexture, textureRect), + + }; + } + + // parse kernings + const kernings = xml.getElementsByTagName('kerning'); + + for (let i = 0; i < kernings.length; i++) + { + const kerning = kernings[i]; + const first = parseInt(kerning.getAttribute('first'), 10); + const second = parseInt(kerning.getAttribute('second'), 10); + const amount = parseInt(kerning.getAttribute('amount'), 10); + + if (data.chars[second]) + { + data.chars[second].kerning[first] = amount; + } + } + + // I'm leaving this as a temporary fix so we can test the bitmap fonts in v3 + // but it's very likely to change + BitmapText.fonts[data.font] = data; + + return data; + } } BitmapText.fonts = {}; diff --git a/src/index.js b/src/index.js index 59356cf..c553427 100644 --- a/src/index.js +++ b/src/index.js @@ -16,6 +16,10 @@ import * as particles from './particles'; import * as prepare from './prepare'; +// handle mixins now, after all code has been added, including deprecation +import { utils } from './core'; +utils.mixins.performMixins(); + export { accessibility, extract, diff --git a/src/interaction/InteractionManager.js b/src/interaction/InteractionManager.js index 7d51134..8baf13a 100644 --- a/src/interaction/InteractionManager.js +++ b/src/interaction/InteractionManager.js @@ -1,18 +1,20 @@ 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'; -// Mix interactiveTarget into core.DisplayObject.prototype -Object.assign( +// Mix interactiveTarget into core.DisplayObject.prototype, after deprecation has been handled +core.utils.mixins.delayMixin( core.DisplayObject.prototype, 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 +70,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 +101,6 @@ this.eventData = new InteractionEvent(); /** - * Tiny little interactiveData pool ! - * - * @member {PIXI.interaction.InteractionData[]} - */ - this.interactiveDataPool = []; - - /** * The DOM element to bind to. * * @private @@ -153,66 +155,12 @@ */ this.supportsPointerEvents = !!window.PointerEvent; - /** - * Are touch events being 'normalized' and converted into pointer events if pointer events are not supported - * For example, on a touch screen mobile device, a touchstart would also be emitted as a pointerdown - * - * @private - * @readonly - * @member {boolean} - */ - this.normalizeTouchEvents = !this.supportsPointerEvents && this.supportsTouchEvents; - - /** - * Are mouse events being 'normalized' and converted into pointer events if pointer events are not supported - * For example, on a desktop pc, a mousedown would also be emitted as a pointerdown - * - * @private - * @readonly - * @member {boolean} - */ - this.normalizeMouseEvents = !this.supportsPointerEvents && !MobileDevice.any; - // this will make it so that you don't have to call bind all the time /** * @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 +168,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,41 +199,32 @@ this.onPointerOver = this.onPointerOver.bind(this); /** - * @private - * @member {Function} + * Dictionary of how different cursor modes are handled. Strings are handled as CSS cursor + * values, objects are handled as dictionaries of CSS values for interactionDOMElement, + * and functions are called instead of changing the CSS. + * Default CSS cursor values are provided for 'default' and 'pointer' modes. + * @member {Object.)>} */ - this.onTouchStart = this.onTouchStart.bind(this); - this.processTouchStart = this.processTouchStart.bind(this); + this.cursorStyles = { + default: 'inherit', + pointer: 'pointer', + }; /** - * @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. - * - * @member {string} - * @default 'inherit' - */ - this.defaultCursorStyle = 'inherit'; - - /** - * The css style of the cursor that is being used. + * The mode of the cursor that is being used. + * The value of this is a key from the cursorStyles dictionary. * * @member {string} */ - this.currentCursorStyle = 'inherit'; + this.currentCursorMode = null; + + /** + * Internal cached let. + * + * @private + * @member {string} + */ + this.cursor = null; /** * Internal cached let. @@ -303,6 +249,7 @@ * object. * * @event mousedown + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -311,6 +258,7 @@ * on the display object. * * @event rightdown + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -319,6 +267,7 @@ * object. * * @event mouseup + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -327,6 +276,7 @@ * over the display object. * * @event rightup + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -335,6 +285,7 @@ * the display object. * * @event click + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -343,6 +294,7 @@ * and released on the display object. * * @event rightclick + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -352,6 +304,7 @@ * [mousedown]{@link PIXI.interaction.InteractionManager#event:mousedown}. * * @event mouseupoutside + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -361,6 +314,7 @@ * [rightdown]{@link PIXI.interaction.InteractionManager#event:rightdown}. * * @event rightupoutside + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -368,6 +322,7 @@ * Fired when a pointer device (usually a mouse) is moved while over the display object * * @event mousemove + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -375,6 +330,7 @@ * Fired when a pointer device (usually a mouse) is moved onto the display object * * @event mouseover + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -382,6 +338,7 @@ * Fired when a pointer device (usually a mouse) is moved off the display object * * @event mouseout + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -389,6 +346,7 @@ * Fired when a pointer device button is pressed on the display object. * * @event pointerdown + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -396,6 +354,14 @@ * Fired when a pointer device button is released over the display object. * * @event pointerup + * @type {PIXI.interaction.InteractionData} + * @memberof PIXI.interaction.InteractionManager# + */ + + /** + * Fired when the operating system cancels a pointer event + * + * @event pointercancel * @memberof PIXI.interaction.InteractionManager# */ @@ -403,6 +369,7 @@ * Fired when a pointer device button is pressed and released on the display object. * * @event pointertap + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -411,6 +378,7 @@ * registered a [pointerdown]{@link PIXI.interaction.InteractionManager#event:pointerdown}. * * @event pointerupoutside + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -418,6 +386,7 @@ * Fired when a pointer device is moved while over the display object * * @event pointermove + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -425,6 +394,7 @@ * Fired when a pointer device is moved onto the display object * * @event pointerover + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -432,6 +402,7 @@ * Fired when a pointer device is moved off the display object * * @event pointerout + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -439,6 +410,7 @@ * Fired when a touch point is placed on the display object. * * @event touchstart + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -446,6 +418,14 @@ * Fired when a touch point is removed from the display object. * * @event touchend + * @type {PIXI.interaction.InteractionData} + * @memberof PIXI.interaction.InteractionManager# + */ + + /** + * Fired when the operating system cancels a touch + * + * @event touchcancel * @memberof PIXI.interaction.InteractionManager# */ @@ -453,6 +433,7 @@ * Fired when a touch point is placed and removed from the display object. * * @event tap + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -461,6 +442,7 @@ * registered a [touchstart]{@link PIXI.interaction.InteractionManager#event:touchstart}. * * @event touchendoutside + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -468,6 +450,7 @@ * Fired when a touch point is moved along the display object. * * @event touchmove + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ } @@ -524,45 +507,30 @@ { window.document.addEventListener('pointermove', this.onPointerMove, true); this.interactionDOMElement.addEventListener('pointerdown', this.onPointerDown, true); - this.interactionDOMElement.addEventListener('pointerout', this.onPointerOut, true); + // pointerout is fired in addition to pointerup (for touch events) and pointercancel + // we already handle those, so for the purposes of what we do in onPointerOut, we only + // care about the pointerleave event + this.interactionDOMElement.addEventListener('pointerleave', 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; @@ -596,45 +564,26 @@ { window.document.removeEventListener('pointermove', this.onPointerMove, true); this.interactionDOMElement.removeEventListener('pointerdown', this.onPointerDown, true); - this.interactionDOMElement.removeEventListener('pointerout', this.onPointerOut, true); + this.interactionDOMElement.removeEventListener('pointerleave', 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) + 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.onPointerDown, true); + this.interactionDOMElement.removeEventListener('touchcancel', this.onPointerCancel, 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); - - 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 = null; @@ -664,7 +613,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; @@ -672,25 +621,80 @@ return; } - this.cursor = this.defaultCursorStyle; + this.cursor = null; // 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(); - - this.processInteractive(this.mouse.global, this.renderer._lastObjectRendered, this.processMouseOverOut, true); - - if (this.currentCursorStyle !== this.cursor) + for (const k in this.activeInteractionData) { - this.currentCursorStyle = this.cursor; - this.interactionDOMElement.style.cursor = this.cursor; + // eslint-disable-next-line no-prototype-builtins + if (this.activeInteractionData.hasOwnProperty(k)) + { + const interactionData = this.activeInteractionData[k]; + + if (interactionData.originalEvent && interactionData.pointerType !== 'touch') + { + const interactionEvent = this.configureInteractionEventForDOMEvent( + this.eventData, + interactionData.originalEvent, + interactionData + ); + + this.processInteractive( + interactionEvent, + this.renderer._lastObjectRendered, + this.processPointerOverOut, + true + ); + } + } } + this.setCursorMode(this.cursor); + // TODO } /** + * Sets the current cursor mode, handling any callbacks or CSS style changes. + * + * @param {string} mode - cursor mode, a key from the cursorStyles dictionary + */ + setCursorMode(mode) + { + mode = mode || 'default'; + // if the mode didn't actually change, bail early + if (this.currentCursorMode === mode) + { + return; + } + this.currentCursorMode = mode; + const style = this.cursorStyles[mode]; + + // only do things if there is a cursor style for it + if (style) + { + switch (typeof style) + { + case 'string': + // string styles are handled as cursor CSS + this.interactionDOMElement.style.cursor = style; + break; + case 'function': + // functions are just called, and passed the cursor mode + style(mode); + break; + case 'object': + // if it is an object, assume that it is a dictionary of CSS styles, + // apply it to the interactionDOMElement + Object.assign(this.interactionDOMElement.style, style); + break; + } + } + } + + /** * Dispatches an event on the display object that was interacted with * * @param {PIXI.Container|PIXI.Sprite|PIXI.extras.TilingSprite} displayObject - the display object in question @@ -748,22 +752,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 @@ -787,9 +795,8 @@ { interactiveParent = false; } - - // it has a mask! Then lets hit test that before continuing.. - if (hitTest && displayObject._mask) + // it has a mask! Then lets hit test that before continuing + else if (hitTest && displayObject._mask) { if (!displayObject._mask.containsPoint(point)) { @@ -797,14 +804,7 @@ } } - // it has a filterArea! Same as mask but easier, its a rectangle - if (hitTest && displayObject.filterArea) - { - if (!displayObject.filterArea.contains(point.x, point.y)) - { - hitTest = false; - } - } + let keepHitTestingAfterChildren = hitTest; // ** FREE TIP **! If an object is not interactive or has no buttons in it // (such as a game scene!) set interactiveChildren to false for that displayObject. @@ -818,7 +818,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 @@ -838,15 +838,20 @@ // This means we no longer need to hit test anything else. We still need to run // through all objects, but we don't need to perform any hit tests. - // { - hitTest = false; - // } + keepHitTestingAfterChildren = false; + + if (child.interactive) + { + hitTest = false; + } // we can break now as we have hit an object. } } } + hitTest = keepHitTestingAfterChildren; + // no point running this if the item is not interactive or does not have an interactive parent. if (interactive) { @@ -868,14 +873,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 +886,175 @@ } /** - * 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].isNormalized) { - 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); + if (!displayObject.trackedPointers[id]) + { + displayObject.trackedPointers[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.trackedPointers[id].rightDown = true; + } + else + { + displayObject.trackedPointers[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.trackedPointers[id] !== undefined) + { + delete displayObject.trackedPointers[id]; + this.dispatchEvent(displayObject, 'pointercancel', interactionEvent); + + if (e.type === 'touchcancel' || e.pointerType === 'touch') + { + this.dispatchEvent(displayObject, 'touchcancel', interactionEvent); + } } } @@ -1159,42 +1066,94 @@ */ 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) { - if (hit) - { - this.dispatchEvent(displayObject, 'pointerup', this.eventData); + const e = interactionEvent.data.originalEvent; - if (displayObject._pointerDown) + const id = interactionEvent.data.identifier; + + const trackingData = displayObject.trackedPointers[id]; + + const isTouch = (e.type === 'touchend' || e.pointerType === 'touch'); + + const isMouse = (e.type.indexOf('mouse') === 0 || e.pointerType === 'mouse'); + + // 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) { - displayObject._pointerDown = false; - this.dispatchEvent(displayObject, 'pointertap', this.eventData); + 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); + } + // update the down state of the tracking data + if (trackingData) + { + if (isRightButton) + { + trackingData.rightDown = hit; + } + else + { + trackingData.leftDown = hit; + } } } - else if (displayObject._pointerDown) + + // Pointers and Touches, and Mouse + if (hit) { - displayObject._pointerDown = false; - this.dispatchEvent(displayObject, 'pointerupoutside', this.eventData); + this.dispatchEvent(displayObject, 'pointerup', interactionEvent); + if (isTouch) this.dispatchEvent(displayObject, 'touchend', interactionEvent); + + if (trackingData) + { + this.dispatchEvent(displayObject, 'pointertap', interactionEvent); + if (isTouch) + { + this.dispatchEvent(displayObject, 'tap', interactionEvent); + // touches are no longer over (if they ever were) when we get the touchend + // so we should ensure that we don't keep pretending that they are + trackingData.over = false; + } + } + } + else if (trackingData) + { + this.dispatchEvent(displayObject, 'pointerupoutside', interactionEvent); + if (isTouch) this.dispatchEvent(displayObject, 'touchendoutside', interactionEvent); + } + // Only remove the tracking data if there is no over/down state still associated with it + if (trackingData && trackingData.none) + { + delete displayObject.trackedPointers[id]; } } @@ -1202,39 +1161,78 @@ * 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 = null; + } - 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') + { + this.setCursorMode(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 (isMouse) { - 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 +1240,94 @@ * 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.setCursorMode(null); + } - 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'); + + let trackingData = displayObject.trackedPointers[id]; + + // if we just moused over the display object, then we need to track that state + if (hit && !trackingData) + { + trackingData = displayObject.trackedPointers[id] = new InteractionTrackingData(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); + } + } + + // only change the cursor if it has not already been changed (by something deeper in the + // display tree) + if (isMouse && this.cursor === null) + { + this.cursor = displayObject.cursor; } } - 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); + } + // if there is no mouse down information for the pointer, then it is safe to delete + if (trackingData.none) + { + delete displayObject.trackedPointers[id]; + } } } @@ -1287,253 +1335,154 @@ * 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 && pointerEvent.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; + + // mark the touch as normalized, just so that we know we did it + touch.isNormalized = true; + + normalizedEvents.push(touch); + } } - else if (this.normalizeMouseEvents) + // apparently PointerEvent subclasses MouseEvent, so yay + else if (event instanceof MouseEvent && (!this.supportsPointerEvents || !(event instanceof window.PointerEvent))) { if (typeof event.isPrimary === 'undefined') event.isPrimary = true; if (typeof event.width === 'undefined') event.width = 1; @@ -1541,10 +1490,21 @@ 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; + + // mark the mouse event as normalized, just so that we know we did it + event.isNormalized = true; + + normalizedEvents.push(event); } + else + { + normalizedEvents.push(event); + } + + return normalizedEvents; } /** @@ -1563,30 +1523,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 +1542,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..6a1f69e --- /dev/null +++ b/src/interaction/InteractionTrackingData.js @@ -0,0 +1,147 @@ +/** + * 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 inactive (not over or down)? + * + * @member {number} + * @memberof PIXI.interaction.InteractionTrackingData# + */ + get none() + { + return this._flags === this.constructor.FLAGS.NONE; + } + + /** + * 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..d252fba 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 * ); */ @@ -38,68 +38,56 @@ hitArea: null, /** - * If enabled, the mouse cursor will change when hovered over the displayObject if it is interactive + * If enabled, the mouse cursor use the pointer behavior when hovered over the displayObject if it is interactive + * Setting this changes the 'cursor' property to `'pointer'`. * - * @inner {boolean} + * @member {boolean} + * @memberof PIXI.interaction.interactiveTarget# */ - buttonMode: false, + get buttonMode() + { + return this.cursor === 'pointer'; + }, + set buttonMode(value) + { + if (value) + { + this.cursor = 'pointer'; + } + else if (this.cursor === 'pointer') + { + this.cursor = null; + } + }, /** - * If buttonMode is enabled, this defines what CSS cursor property is used when the mouse cursor - * is hovered over the displayObject + * This defines what cursor mode is used when the mouse cursor + * is hovered over the displayObject. * * @see https://developer.mozilla.org/en/docs/Web/CSS/cursor * * @inner {string} */ - defaultCursor: 'pointer', - - // some internal checks.. - /** - * Internal check to detect if the mouse cursor is hovered over the displayObject - * - * @inner {boolean} - * @private - */ - _over: false, + cursor: null, /** - * Internal check to detect if the left mouse button is pressed on the displayObject + * Internal set of all active pointers, by identifier * - * @inner {boolean} + * @member {Map} + * @memberof PIXI.interaction.interactiveTarget# * @private */ - _isLeftDown: false, + get trackedPointers() + { + if (this._trackedPointers === undefined) this._trackedPointers = {}; + + return this._trackedPointers; + }, /** - * Internal check to detect if the right mouse button is pressed on the displayObject + * Map of all tracked pointers, by identifier. Use trackedPointers to access. * - * @inner {boolean} - * @private + * @private {Map} */ - _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, + _trackedPointers: undefined, }; diff --git a/src/loaders/bitmapFontParser.js b/src/loaders/bitmapFontParser.js index cc2618c..f4cf996 100644 --- a/src/loaders/bitmapFontParser.js +++ b/src/loaders/bitmapFontParser.js @@ -1,63 +1,19 @@ import * as path from 'path'; -import { Rectangle, Texture, utils } from '../core'; +import { utils } from '../core'; import { Resource } from 'resource-loader'; import { BitmapText } from '../extras'; +/** + * Register a BitmapText font from loader resource. + * + * @function parseBitmapFontData + * @memberof PIXI.loaders + * @param {PIXI.loaders.Resource} resource - Loader resource. + * @param {PIXI.Texture} texture - Reference to texture. + */ export function parse(resource, texture) { - const data = {}; - const info = resource.data.getElementsByTagName('info')[0]; - const common = resource.data.getElementsByTagName('common')[0]; - - data.font = info.getAttribute('face'); - data.size = parseInt(info.getAttribute('size'), 10); - data.lineHeight = parseInt(common.getAttribute('lineHeight'), 10); - data.chars = {}; - - // parse letters - const letters = resource.data.getElementsByTagName('char'); - - for (let i = 0; i < letters.length; i++) - { - const charCode = parseInt(letters[i].getAttribute('id'), 10); - - const textureRect = new Rectangle( - parseInt(letters[i].getAttribute('x'), 10) + texture.frame.x, - parseInt(letters[i].getAttribute('y'), 10) + texture.frame.y, - parseInt(letters[i].getAttribute('width'), 10), - parseInt(letters[i].getAttribute('height'), 10) - ); - - data.chars[charCode] = { - xOffset: parseInt(letters[i].getAttribute('xoffset'), 10), - yOffset: parseInt(letters[i].getAttribute('yoffset'), 10), - xAdvance: parseInt(letters[i].getAttribute('xadvance'), 10), - kerning: {}, - texture: new Texture(texture.baseTexture, textureRect), - - }; - } - - // parse kernings - const kernings = resource.data.getElementsByTagName('kerning'); - - for (let i = 0; i < kernings.length; i++) - { - const first = parseInt(kernings[i].getAttribute('first'), 10); - const second = parseInt(kernings[i].getAttribute('second'), 10); - const amount = parseInt(kernings[i].getAttribute('amount'), 10); - - if (data.chars[second]) - { - data.chars[second].kerning[first] = amount; - } - } - - resource.bitmapFont = data; - - // I'm leaving this as a temporary fix so we can test the bitmap fonts in v3 - // but it's very likely to change - BitmapText.fonts[data.font] = data; + resource.bitmapFont = BitmapText.registerFont(resource.data, texture); } export default function () diff --git a/src/loaders/index.js b/src/loaders/index.js index 8942436..5189793 100644 --- a/src/loaders/index.js +++ b/src/loaders/index.js @@ -5,4 +5,11 @@ export { default as bitmapFontParser, parse as parseBitmapFontData } from './bitmapFontParser'; export { default as spritesheetParser } from './spritesheetParser'; export { default as textureParser } from './textureParser'; + +/** + * Reference to **resource-loader**'s Resource class. + * See https://github.com/englercj/resource-loader + * @class Resource + * @memberof PIXI.loaders + */ export { Resource } from 'resource-loader'; diff --git a/src/loaders/loader.js b/src/loaders/loader.js index 0a670cb..767a411 100644 --- a/src/loaders/loader.js +++ b/src/loaders/loader.js @@ -7,20 +7,47 @@ /** * - * The new loader, extends Resource Loader by Chad Engler : https://github.com/englercj/resource-loader + * The new loader, extends Resource Loader by Chad Engler: https://github.com/englercj/resource-loader * * ```js - * let loader = PIXI.loader; // pixi exposes a premade instance for you to use. + * const loader = PIXI.loader; // pixi exposes a premade instance for you to use. * //or - * let loader = new PIXI.loaders.Loader(); // you can also create your own if you want + * const loader = new PIXI.loaders.Loader(); // you can also create your own if you want * - * loader.add('bunny', 'data/bunny.png'); - * loader.add('spaceship', 'assets/spritesheet.json'); + * const sprites = {}; + * + * // Chainable `add` to enqueue a resource + * loader.add('bunny', 'data/bunny.png') + * .add('spaceship', 'assets/spritesheet.json'); * loader.add('scoreFont', 'assets/score.fnt'); * - * loader.once('complete',onAssetsLoaded); + * // Chainable `pre` to add a middleware that runs for each resource, *before* loading that resource. + * // This is useful to implement custom caching modules (using filesystem, indexeddb, memory, etc). + * loader.pre(cachingMiddleware); * - * loader.load(); + * // Chainable `use` to add a middleware that runs for each resource, *after* loading that resource. + * // This is useful to implement custom parsing modules (like spritesheet parsers, spine parser, etc). + * loader.use(parsingMiddleware); + * + * // The `load` method loads the queue of resources, and calls the passed in callback called once all + * // resources have loaded. + * loader.load((loader, resources) => { + * // resources is an object where the key is the name of the resource loaded and the value is the resource object. + * // They have a couple default properties: + * // - `url`: The URL that the resource was loaded from + * // - `error`: The error that happened when trying to load (if any) + * // - `data`: The raw data that was loaded + * // also may contain other properties based on the middleware that runs. + * sprites.bunny = new PIXI.TilingSprite(resources.bunny.texture); + * sprites.spaceship = new PIXI.TilingSprite(resources.spaceship.texture); + * sprites.scoreFont = new PIXI.TilingSprite(resources.scoreFont.texture); + * }); + * + * // throughout the process multiple signals can be dispatched. + * loader.onProgress.add(() => {}); // called once per loaded/errored file + * loader.onError.add(() => {}); // called once per errored file + * loader.onLoad.add(() => {}); // called once per loaded file + * loader.onComplete.add(() => {}); // called once when the queued resources all load. * ``` * * @see https://github.com/englercj/resource-loader diff --git a/src/loaders/spritesheetParser.js b/src/loaders/spritesheetParser.js index 7570332..e57dfb2 100644 --- a/src/loaders/spritesheetParser.js +++ b/src/loaders/spritesheetParser.js @@ -1,8 +1,6 @@ import { Resource } from 'resource-loader'; import path from 'path'; -import * as core from '../core'; - -const BATCH_SIZE = 1000; +import { Spritesheet } from '../core'; export default function () { @@ -43,134 +41,18 @@ // load the image for this sheet this.add(imageResourceName, resourcePath, loadOptions, function onImageLoad(res) { - resource.textures = {}; + const spritesheet = new Spritesheet( + res.texture.baseTexture, + resource.data, + resource.url + ); - const frames = resource.data.frames; - const frameKeys = Object.keys(frames); - const baseTexture = res.texture.baseTexture; - const scale = resource.data.meta.scale; - - // Use a defaultValue of `null` to check if a url-based resolution is set - let resolution = core.utils.getResolutionOfUrl(resource.url, null); - - // No resolution found via URL - if (resolution === null) + spritesheet.parse(() => { - // Use the scale value or default to 1 - resolution = scale !== undefined ? scale : 1; - } - - // For non-1 resolutions, update baseTexture - if (resolution !== 1) - { - baseTexture.resolution = resolution; - baseTexture.update(); - } - - let batchIndex = 0; - - function processFrames(initialFrameIndex, maxFrames) - { - let frameIndex = initialFrameIndex; - - while (frameIndex - initialFrameIndex < maxFrames && frameIndex < frameKeys.length) - { - const i = frameKeys[frameIndex]; - const rect = frames[i].frame; - - if (rect) - { - let frame = null; - let trim = null; - const orig = new core.Rectangle( - 0, - 0, - frames[i].sourceSize.w / resolution, - frames[i].sourceSize.h / resolution - ); - - if (frames[i].rotated) - { - frame = new core.Rectangle( - rect.x / resolution, - rect.y / resolution, - rect.h / resolution, - rect.w / resolution - ); - } - else - { - frame = new core.Rectangle( - rect.x / resolution, - rect.y / resolution, - rect.w / resolution, - rect.h / resolution - ); - } - - // Check to see if the sprite is trimmed - if (frames[i].trimmed) - { - trim = new core.Rectangle( - frames[i].spriteSourceSize.x / resolution, - frames[i].spriteSourceSize.y / resolution, - rect.w / resolution, - rect.h / resolution - ); - } - - resource.textures[i] = new core.Texture( - baseTexture, - frame, - orig, - trim, - frames[i].rotated ? 2 : 0 - ); - - // lets also add the frame to pixi's global cache for fromFrame and fromImage functions - core.utils.TextureCache[i] = resource.textures[i]; - } - - frameIndex++; - } - } - - function shouldProcessNextBatch() - { - return batchIndex * BATCH_SIZE < frameKeys.length; - } - - function processNextBatch(done) - { - processFrames(batchIndex * BATCH_SIZE, BATCH_SIZE); - batchIndex++; - setTimeout(done, 0); - } - - function iteration() - { - processNextBatch(() => - { - if (shouldProcessNextBatch()) - { - iteration(); - } - else - { - next(); - } - }); - } - - if (frameKeys.length <= BATCH_SIZE) - { - processFrames(0, BATCH_SIZE); + resource.spritesheet = spritesheet; + resource.textures = spritesheet.textures; next(); - } - else - { - iteration(); - } + }); }); }; } diff --git a/src/loaders/textureParser.js b/src/loaders/textureParser.js index 5398a7f..480edc1 100644 --- a/src/loaders/textureParser.js +++ b/src/loaders/textureParser.js @@ -1,5 +1,5 @@ -import * as core from '../core'; import { Resource } from 'resource-loader'; +import Texture from '../core/textures/Texture'; export default function () { @@ -8,23 +8,12 @@ // create a new texture if the data is an Image object if (resource.data && resource.type === Resource.TYPE.IMAGE) { - const baseTexture = new core.BaseTexture(resource.data, null, core.utils.getResolutionOfUrl(resource.url)); - - baseTexture.imageUrl = resource.url; - resource.texture = new core.Texture(baseTexture); - - // lets also add the frame to pixi's global cache for fromFrame and fromImage fucntions - core.utils.BaseTextureCache[resource.name] = baseTexture; - core.utils.TextureCache[resource.name] = resource.texture; - - // also add references by url if they are different. - if (resource.name !== resource.url) - { - core.utils.BaseTextureCache[resource.url] = baseTexture; - core.utils.TextureCache[resource.url] = resource.texture; - } + resource.texture = Texture.fromLoader( + resource.data, + resource.url, + resource.name + ); } - next(); }; } diff --git a/test/core/Container.js b/test/core/Container.js index 8a10b40..d11be33 100644 --- a/test/core/Container.js +++ b/test/core/Container.js @@ -1,5 +1,33 @@ 'use strict'; +function testAddChild(fn) +{ + fn(function (container, obj) + { + container.addChild(obj); + }); + fn(function (container, obj) + { + container.addChildAt(obj); + }); +} + +function testRemoveChild(fn) +{ + fn(function (container, obj) + { + container.removeChild(obj); + }); + fn(function (container, obj) + { + container.removeChildAt(container.children.indexOf(obj)); + }); + fn(function (container, obj) + { + container.removeChildren(container.children.indexOf(obj), container.children.indexOf(obj) + 1); + }); +} + describe('PIXI.Container', function () { describe('parent', function () @@ -70,6 +98,49 @@ expect(spy).to.have.been.called; expect(spy).to.have.been.calledWith(0); }); + + it('should flag child transform and container bounds for recalculation', testAddChild(function (mockAddChild) + { + const container = new PIXI.Container(); + const child = new PIXI.Container(); + + container.getBounds(); + child.getBounds(); + + const boundsID = container._boundsID; + const childParentID = child.transform._parentID; + + mockAddChild(container, child); + + expect(boundsID).to.not.be.equals(container._boundsID); + expect(childParentID).to.not.be.equals(child.transform._parentID); + })); + + it('should recalculate added child correctly', testAddChild(function (mockAddChild) + { + const parent = new PIXI.Container(); + const container = new PIXI.Container(); + const graphics = new PIXI.Graphics(); + + parent.addChild(container); + + graphics.drawRect(0, 0, 10, 10); + container.position.set(100, 200); + container.updateTransform(); + + graphics.getBounds(); + // Oops, that can happen sometimes! + graphics.transform._parentID = container.transform._worldID + 1; + + mockAddChild(container, graphics); + + const bounds = graphics.getBounds(); + + expect(bounds.x).to.be.equal(100); + expect(bounds.y).to.be.equal(200); + expect(bounds.width).to.be.equal(10); + expect(bounds.height).to.be.equal(10); + })); }); describe('removeChildAt', function () @@ -202,6 +273,44 @@ expect(spy).to.have.been.called; expect(spy).to.have.been.calledWith(0); }); + + it('should flag transform for recalculation', testRemoveChild(function (mockRemoveChild) + { + const container = new PIXI.Container(); + const child = new PIXI.Container(); + + container.addChild(child); + container.getBounds(); + + const childParentID = child.transform._parentID; + const boundsID = container._boundsID; + + mockRemoveChild(container, child); + + expect(childParentID).to.not.be.equals(child.transform._parentID); + expect(boundsID).to.not.be.equals(container._boundsID); + })); + + it('should recalculate removed child correctly', testRemoveChild(function (mockRemoveChild) + { + const parent = new PIXI.Container(); + const container = new PIXI.Container(); + const graphics = new PIXI.Graphics(); + + parent.addChild(container); + + graphics.drawRect(0, 0, 10, 10); + container.position.set(100, 200); + container.addChild(graphics); + graphics.getBounds(); + + mockRemoveChild(container, graphics); + + const bounds = graphics.getBounds(); + + expect(bounds.x).to.be.equal(0); + expect(bounds.y).to.be.equal(0); + })); }); describe('getChildIndex', function () diff --git a/test/core/Graphics.js b/test/core/Graphics.js index 98c7584..11e47a0 100644 --- a/test/core/Graphics.js +++ b/test/core/Graphics.js @@ -1,5 +1,8 @@ 'use strict'; +const MockPointer = require('../interaction/MockPointer'); +const withGL = require('../withGL'); + describe('PIXI.Graphics', function () { describe('constructor', function () @@ -218,4 +221,153 @@ expect(spy).to.have.been.calledOnce; }); }); + + describe('mask', function () + { + it('should trigger interaction callback when no mask present', function () + { + const stage = new PIXI.Container(); + const pointer = new MockPointer(stage); + const graphics = new PIXI.Graphics(); + const mask = new PIXI.Graphics(); + const spy = sinon.spy(); + + graphics.interactive = true; + graphics.beginFill(0xFF0000); + graphics.drawRect(0, 0, 50, 50); + graphics.on('click', spy); + stage.addChild(graphics); + mask.beginFill(); + mask.drawRect(0, 0, 50, 50); + graphics.mask = mask; + + pointer.click(10, 10); + + expect(spy).to.have.been.calledOnce; + }); + it('should trigger interaction callback when mask uses beginFill', function () + { + const stage = new PIXI.Container(); + const pointer = new MockPointer(stage); + const graphics = new PIXI.Graphics(); + const mask = new PIXI.Graphics(); + const spy = sinon.spy(); + + graphics.interactive = true; + graphics.beginFill(0xFF0000); + graphics.drawRect(0, 0, 50, 50); + graphics.on('click', spy); + stage.addChild(graphics); + mask.beginFill(); + mask.drawRect(0, 0, 50, 50); + graphics.mask = mask; + + pointer.click(10, 10); + + expect(spy).to.have.been.calledOnce; + }); + + it('should not trigger interaction callback when mask doesn\'t use beginFill', function () + { + const stage = new PIXI.Container(); + const pointer = new MockPointer(stage); + const graphics = new PIXI.Graphics(); + const mask = new PIXI.Graphics(); + const spy = sinon.spy(); + + graphics.interactive = true; + graphics.beginFill(0xFF0000); + graphics.drawRect(0, 0, 50, 50); + graphics.on('click', spy); + stage.addChild(graphics); + mask.drawRect(0, 0, 50, 50); + graphics.mask = mask; + + pointer.click(10, 10); + + expect(spy).to.have.not.been.called; + }); + + it('should trigger interaction callback when mask doesn\'t use beginFill but hitArea is defined', function () + { + const stage = new PIXI.Container(); + const pointer = new MockPointer(stage); + const graphics = new PIXI.Graphics(); + const mask = new PIXI.Graphics(); + const spy = sinon.spy(); + + graphics.interactive = true; + graphics.beginFill(0xFF0000); + graphics.hitArea = new PIXI.Rectangle(0, 0, 50, 50); + graphics.drawRect(0, 0, 50, 50); + graphics.on('click', spy); + stage.addChild(graphics); + mask.drawRect(0, 0, 50, 50); + graphics.mask = mask; + + pointer.click(10, 10); + + expect(spy).to.have.been.calledOnce; + }); + + it('should trigger interaction callback when mask is a sprite', function () + { + const stage = new PIXI.Container(); + const pointer = new MockPointer(stage); + const graphics = new PIXI.Graphics(); + const mask = new PIXI.Graphics(); + const spy = sinon.spy(); + + graphics.interactive = true; + graphics.beginFill(0xFF0000); + graphics.drawRect(0, 0, 50, 50); + graphics.on('click', spy); + stage.addChild(graphics); + mask.drawRect(0, 0, 50, 50); + graphics.mask = new PIXI.Sprite(mask.generateCanvasTexture()); + + pointer.click(10, 10); + + expect(spy).to.have.been.calledOnce; + }); + + it('should calculate tint, alpha and blendMode of fastRect correctly', withGL(function () + { + const renderer = new PIXI.WebGLRenderer(200, 200, {}); + + try + { + const graphics = new PIXI.Graphics(); + + graphics.beginFill(0x102030, 0.6); + graphics.drawRect(2, 3, 100, 100); + graphics.endFill(); + graphics.tint = 0x101010; + graphics.blendMode = 2; + graphics.alpha = 0.3; + + renderer.render(graphics); + + expect(graphics.isFastRect()).to.be.true; + + const sprite = graphics._spriteRect; + + expect(sprite).to.not.be.equals(null); + expect(sprite.worldAlpha).to.equals(0.18); + expect(sprite.blendMode).to.equals(2); + expect(sprite.tint).to.equals(0x010203); + + const bounds = sprite.getBounds(); + + expect(bounds.x).to.equals(2); + expect(bounds.y).to.equals(3); + expect(bounds.width).to.equals(100); + expect(bounds.height).to.equals(100); + } + finally + { + renderer.destroy(); + } + })); + }); }); diff --git a/test/core/Spritesheet.js b/test/core/Spritesheet.js new file mode 100644 index 0000000..283adf5 --- /dev/null +++ b/test/core/Spritesheet.js @@ -0,0 +1,89 @@ +'use strict'; + +const path = require('path'); + +describe('PIXI.Spritesheet', function () +{ + before(function () + { + this.resources = path.join(__dirname, 'resources'); + this.validate = function (spritesheet, done) + { + spritesheet.parse(function (textures) + { + const id = 'goldmine_10_5.png'; + + expect(Object.keys(textures).length).to.equal(1); + expect(Object.keys(spritesheet.textures).length).to.equal(1); + expect(textures[id]).to.be.an.instanceof(PIXI.Texture); + expect(textures[id].width).to.equal(spritesheet.data.frames[id].frame.w / spritesheet.resolution); + expect(textures[id].height).to.equal(spritesheet.data.frames[id].frame.h / spritesheet.resolution); + spritesheet.destroy(true); + expect(spritesheet.textures).to.be.null; + expect(spritesheet.baseTexture).to.be.null; + done(); + }); + }; + }); + + it('should exist on PIXI', function () + { + expect(PIXI.Spritesheet).to.be.a.function; + expect(PIXI.Spritesheet.BATCH_SIZE).to.be.a.number; + }); + + it('should create an instance', function () + { + const baseTexture = new PIXI.BaseTexture(); + const data = { + frames: {}, + meta: {}, + }; + const spritesheet = new PIXI.Spritesheet(baseTexture, data); + + expect(spritesheet.data).to.equal(data); + expect(spritesheet.baseTexture).to.equal(baseTexture); + expect(spritesheet.resolution).to.equal(1); + + spritesheet.destroy(true); + }); + + it('should create instance with scale resolution', function (done) + { + const data = require(path.resolve(this.resources, 'building1.json')); // eslint-disable-line global-require + const image = new Image(); + + image.src = path.join(this.resources, data.meta.image); + image.onload = () => + { + const baseTexture = new PIXI.BaseTexture(image, null, 1); + const spritesheet = new PIXI.Spritesheet(baseTexture, data); + + expect(data).to.be.an.object; + expect(data.meta.image).to.equal('building1.png'); + expect(spritesheet.resolution).to.equal(0.5); + + this.validate(spritesheet, done); + }; + }); + + it('should create instance with filename resolution', function (done) + { + const uri = path.resolve(this.resources, 'building1@2x.json'); + const data = require(uri); // eslint-disable-line global-require + const image = new Image(); + + image.src = path.join(this.resources, data.meta.image); + image.onload = () => + { + const baseTexture = new PIXI.BaseTexture(image, null, 1); + const spritesheet = new PIXI.Spritesheet(baseTexture, data, uri); + + expect(data).to.be.an.object; + expect(data.meta.image).to.equal('building1@2x.png'); + expect(spritesheet.resolution).to.equal(2); + + this.validate(spritesheet, done); + }; + }); +}); diff --git a/test/core/Texture.js b/test/core/Texture.js new file mode 100644 index 0000000..8c4ef98 --- /dev/null +++ b/test/core/Texture.js @@ -0,0 +1,19 @@ +'use strict'; + +describe('PIXI.Texture', function () +{ + it('should register Texture from Loader', function () + { + const URL = 'foo.png'; + const NAME = 'bar'; + const image = new Image(); + + const texture = PIXI.Texture.fromLoader(image, URL, NAME); + + expect(texture.baseTexture.imageUrl).to.equal('foo.png'); + expect(PIXI.utils.TextureCache[NAME]).to.equal(texture); + expect(PIXI.utils.BaseTextureCache[NAME]).to.equal(texture.baseTexture); + expect(PIXI.utils.TextureCache[URL]).to.equal(texture); + expect(PIXI.utils.BaseTextureCache[URL]).to.equal(texture.baseTexture); + }); +}); diff --git a/test/core/WebGLRenderer.js b/test/core/WebGLRenderer.js index 9bc3bc2..3d6cf98 100644 --- a/test/core/WebGLRenderer.js +++ b/test/core/WebGLRenderer.js @@ -1,21 +1,21 @@ 'use strict'; -function isWebGLSupported(fn) -{ - return PIXI.utils.isWebGLSupported() ? fn : function () {}; // eslint-disable-line no-empty-function -} +const withGL = require('../withGL'); -describe('PIXI.WebGLRenderer', isWebGLSupported(function () +describe('PIXI.WebGLRenderer', function () { - it('setting option legacy should disable VAOs and set minimum SPRITE_MAX_TEXTURES to 1', function () + it('setting option legacy should disable VAOs and SPRITE_MAX_TEXTURES', withGL(function () { const renderer = new PIXI.WebGLRenderer(1, 1, { legacy: true }); - - expect(PIXI.glCore.VertexArrayObject.FORCE_NATIVE).to.equal(true); - expect(renderer.plugins.sprite.MAX_TEXTURES).to.equal(1); - - renderer.destroy(); - }); -})); - + try + { + expect(PIXI.glCore.VertexArrayObject.FORCE_NATIVE).to.equal(true); + expect(renderer.plugins.sprite.MAX_TEXTURES).to.equal(1); + } + finally + { + renderer.destroy(); + } + })); +}); diff --git a/test/core/filters.js b/test/core/filters.js new file mode 100644 index 0000000..df459f6 --- /dev/null +++ b/test/core/filters.js @@ -0,0 +1,21 @@ +'use strict'; + +describe('PIXI.filters', function () +{ + it('should correctly form uniformData', function () + { + const sprite = new PIXI.Sprite(PIXI.Texture.EMPTY); + const displ = new PIXI.filters.DisplacementFilter(sprite); + + expect(!!displ.uniformData.scale).to.be.true; + expect(!!displ.uniformData.filterMatrix).to.be.true; + expect(!!displ.uniformData.mapSampler).to.be.true; + // it does have filterClamp, but it is handled by FilterManager + expect(!!displ.uniformData.filterClamp).to.be.false; + + const fxaa = new PIXI.filters.FXAAFilter(); + + // it does have filterArea, but it is handled by FilterManager + expect(!!fxaa.uniformData.filterArea).to.be.false; + }); +}); diff --git a/test/core/index.js b/test/core/index.js index 63a8f21..8530131 100755 --- a/test/core/index.js +++ b/test/core/index.js @@ -7,6 +7,7 @@ require('./DisplayObject'); require('./getLocalBounds'); require('./Sprite'); +require('./Spritesheet'); require('./TilingSprite'); require('./TextStyle'); require('./Text'); @@ -25,3 +26,5 @@ require('./SpriteRenderer'); require('./WebGLRenderer'); require('./Ellipse'); +require('./Texture'); +require('./filters'); diff --git a/test/core/resources/building1.json b/test/core/resources/building1.json new file mode 100755 index 0000000..03fa5c1 --- /dev/null +++ b/test/core/resources/building1.json @@ -0,0 +1,21 @@ +{"frames": { + +"goldmine_10_5.png": +{ + "frame": {"x":1,"y":1,"w":95,"h":115}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":95,"h":115}, + "sourceSize": {"w":95,"h":115}, + "pivot": {"x":0.5,"y":0.5} +}}, +"meta": { + "app": "http://www.codeandweb.com/texturepacker", + "version": "1.0", + "image": "building1.png", + "format": "RGBA8888", + "size": {"w":128,"h":128}, + "scale": "0.5", + "smartupdate": "$TexturePacker:SmartUpdate:d7a5e54c8f8a3fecd508baf190c44807:99a0e3a4dc0f441e4aad77614191ab38:6046b8eb706ddefaa771c33ceb7cb6d5$" +} +} diff --git a/test/core/resources/building1.png b/test/core/resources/building1.png new file mode 100755 index 0000000..7e1e114 --- /dev/null +++ b/test/core/resources/building1.png Binary files differ diff --git a/test/core/resources/building1@2x.json b/test/core/resources/building1@2x.json new file mode 100755 index 0000000..24e25ff --- /dev/null +++ b/test/core/resources/building1@2x.json @@ -0,0 +1,21 @@ +{"frames": { + +"goldmine_10_5.png": +{ + "frame": {"x":1,"y":1,"w":190,"h":229}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":190,"h":229}, + "sourceSize": {"w":190,"h":229}, + "pivot": {"x":0.5,"y":0.5} +}}, +"meta": { + "app": "http://www.codeandweb.com/texturepacker", + "version": "1.0", + "image": "building1@2x.png", + "format": "RGBA8888", + "size": {"w":256,"h":256}, + "scale": "1", + "smartupdate": "$TexturePacker:SmartUpdate:d7a5e54c8f8a3fecd508baf190c44807:99a0e3a4dc0f441e4aad77614191ab38:6046b8eb706ddefaa771c33ceb7cb6d5$" +} +} diff --git a/test/core/resources/building1@2x.png b/test/core/resources/building1@2x.png new file mode 100755 index 0000000..d5ecd04 --- /dev/null +++ b/test/core/resources/building1@2x.png Binary files differ diff --git a/test/core/util.js b/test/core/util.js index dce6003..239aaeb 100755 --- a/test/core/util.js +++ b/test/core/util.js @@ -47,6 +47,11 @@ .to.be.a('function'); }); + it('should calculate correctly', function () + { + expect(PIXI.utils.rgb2hex([0.3, 0.2, 0.1])).to.equals(0x4c3319); + }); + // it('should properly convert rgb array to hex color string'); }); diff --git a/test/interaction/InteractionManager.js b/test/interaction/InteractionManager.js index 2cf6d9d..0e7e707 100644 --- a/test/interaction/InteractionManager.js +++ b/test/interaction/InteractionManager.js @@ -4,6 +4,281 @@ describe('PIXI.interaction.InteractionManager', function () { + describe('event basics', function () + { + it('should call mousedown handler', function () + { + const stage = new PIXI.Container(); + const graphics = new PIXI.Graphics(); + const eventSpy = sinon.spy(); + const pointer = new MockPointer(stage); + + stage.addChild(graphics); + graphics.beginFill(0xFFFFFF); + graphics.drawRect(0, 0, 50, 50); + graphics.interactive = true; + graphics.on('mousedown', eventSpy); + + pointer.mousedown(10, 10); + + expect(eventSpy).to.have.been.calledOnce; + }); + + it('should call mouseup handler', function () + { + const stage = new PIXI.Container(); + const graphics = new PIXI.Graphics(); + const eventSpy = sinon.spy(); + const pointer = new MockPointer(stage); + + stage.addChild(graphics); + graphics.beginFill(0xFFFFFF); + graphics.drawRect(0, 0, 50, 50); + graphics.interactive = true; + graphics.on('mouseup', eventSpy); + + pointer.click(10, 10); + + expect(eventSpy).to.have.been.called; + }); + + it('should call mouseupoutside handler', function () + { + const stage = new PIXI.Container(); + const graphics = new PIXI.Graphics(); + const eventSpy = sinon.spy(); + const pointer = new MockPointer(stage); + + stage.addChild(graphics); + graphics.beginFill(0xFFFFFF); + graphics.drawRect(0, 0, 50, 50); + graphics.interactive = true; + graphics.on('mouseupoutside', eventSpy); + + pointer.mousedown(10, 10); + pointer.mouseup(60, 60); + + expect(eventSpy).to.have.been.called; + }); + + it('should call mouseover handler', function () + { + const stage = new PIXI.Container(); + const graphics = new PIXI.Graphics(); + const eventSpy = sinon.spy(); + const pointer = new MockPointer(stage); + + stage.addChild(graphics); + graphics.beginFill(0xFFFFFF); + graphics.drawRect(0, 0, 50, 50); + graphics.interactive = true; + graphics.on('mouseover', eventSpy); + + pointer.mousemove(10, 10); + + expect(eventSpy).to.have.been.called; + }); + + it('should call mouseout handler', function () + { + const stage = new PIXI.Container(); + const graphics = new PIXI.Graphics(); + const eventSpy = sinon.spy(); + const pointer = new MockPointer(stage); + + stage.addChild(graphics); + graphics.beginFill(0xFFFFFF); + graphics.drawRect(0, 0, 50, 50); + graphics.interactive = true; + graphics.on('mouseout', eventSpy); + + pointer.mousemove(10, 10); + pointer.mousemove(60, 60); + + expect(eventSpy).to.have.been.called; + }); + }); + + describe('add/remove events', function () + { + let stub; + + before(function () + { + stub = sinon.stub(PIXI.interaction.InteractionManager.prototype, 'setTargetElement'); + }); + + after(function () + { + stub.restore(); + }); + + it('should add and remove pointer events to document', function () + { + const manager = new PIXI.interaction.InteractionManager(sinon.stub()); + const addSpy = sinon.spy(window.document, 'addEventListener'); + const removeSpy = sinon.spy(window.document, 'removeEventListener'); + + manager.interactionDOMElement = { style: {}, addEventListener: sinon.stub(), removeEventListener: sinon.stub() }; + manager.supportsPointerEvents = true; + + manager.addEvents(); + + expect(addSpy).to.have.been.calledOnce; + expect(addSpy).to.have.been.calledWith('pointermove'); + + manager.removeEvents(); + + expect(removeSpy).to.have.been.calledOnce; + expect(removeSpy).to.have.been.calledWith('pointermove'); + + addSpy.restore(); + removeSpy.restore(); + }); + + it('should add and remove pointer events to window', function () + { + const manager = new PIXI.interaction.InteractionManager(sinon.stub()); + const addSpy = sinon.spy(window, 'addEventListener'); + const removeSpy = sinon.spy(window, 'removeEventListener'); + + manager.interactionDOMElement = { style: {}, addEventListener: sinon.stub(), removeEventListener: sinon.stub() }; + manager.supportsPointerEvents = true; + + manager.addEvents(); + + expect(addSpy).to.have.been.calledTwice; + expect(addSpy).to.have.been.calledWith('pointercancel'); + expect(addSpy).to.have.been.calledWith('pointerup'); + + manager.removeEvents(); + + expect(removeSpy).to.have.been.calledTwice; + expect(removeSpy).to.have.been.calledWith('pointercancel'); + expect(removeSpy).to.have.been.calledWith('pointerup'); + + addSpy.restore(); + removeSpy.restore(); + }); + + it('should add and remove pointer events to element', function () + { + const manager = new PIXI.interaction.InteractionManager(sinon.stub()); + const element = { style: {}, addEventListener: sinon.stub(), removeEventListener: sinon.stub() }; + + manager.interactionDOMElement = element; + manager.supportsPointerEvents = true; + + manager.addEvents(); + + expect(element.addEventListener).to.have.been.calledThrice; + expect(element.addEventListener).to.have.been.calledWith('pointerdown'); + expect(element.addEventListener).to.have.been.calledWith('pointerleave'); + expect(element.addEventListener).to.have.been.calledWith('pointerover'); + + manager.removeEvents(); + + expect(element.removeEventListener).to.have.been.calledThrice; + expect(element.removeEventListener).to.have.been.calledWith('pointerdown'); + expect(element.removeEventListener).to.have.been.calledWith('pointerleave'); + expect(element.removeEventListener).to.have.been.calledWith('pointerover'); + }); + + it('should add and remove mouse events to document', function () + { + const manager = new PIXI.interaction.InteractionManager(sinon.stub()); + const addSpy = sinon.spy(window.document, 'addEventListener'); + const removeSpy = sinon.spy(window.document, 'removeEventListener'); + + manager.interactionDOMElement = { style: {}, addEventListener: sinon.stub(), removeEventListener: sinon.stub() }; + manager.supportsPointerEvents = false; + + manager.addEvents(); + + expect(addSpy).to.have.been.calledOnce; + expect(addSpy).to.have.been.calledWith('mousemove'); + + manager.removeEvents(); + + expect(removeSpy).to.have.been.calledOnce; + expect(removeSpy).to.have.been.calledWith('mousemove'); + + addSpy.restore(); + removeSpy.restore(); + }); + + it('should add and remove mouse events to window', function () + { + const manager = new PIXI.interaction.InteractionManager(sinon.stub()); + const addSpy = sinon.spy(window, 'addEventListener'); + const removeSpy = sinon.spy(window, 'removeEventListener'); + + manager.interactionDOMElement = { style: {}, addEventListener: sinon.stub(), removeEventListener: sinon.stub() }; + manager.supportsPointerEvents = false; + + manager.addEvents(); + + expect(addSpy).to.have.been.calledOnce; + expect(addSpy).to.have.been.calledWith('mouseup'); + + manager.removeEvents(); + + expect(removeSpy).to.have.been.calledOnce; + expect(removeSpy).to.have.been.calledWith('mouseup'); + + addSpy.restore(); + removeSpy.restore(); + }); + + it('should add and remove mouse events to element', function () + { + const manager = new PIXI.interaction.InteractionManager(sinon.stub()); + const element = { style: {}, addEventListener: sinon.stub(), removeEventListener: sinon.stub() }; + + manager.interactionDOMElement = element; + manager.supportsPointerEvents = false; + manager.supportsTouchEvents = false; + + manager.addEvents(); + + expect(element.addEventListener).to.have.been.calledThrice; + expect(element.addEventListener).to.have.been.calledWith('mousedown'); + expect(element.addEventListener).to.have.been.calledWith('mouseout'); + expect(element.addEventListener).to.have.been.calledWith('mouseover'); + + manager.removeEvents(); + + expect(element.removeEventListener).to.have.been.calledThrice; + expect(element.removeEventListener).to.have.been.calledWith('mousedown'); + expect(element.removeEventListener).to.have.been.calledWith('mouseout'); + expect(element.removeEventListener).to.have.been.calledWith('mouseover'); + }); + + it('should add and remove touch events to element', function () + { + const manager = new PIXI.interaction.InteractionManager(sinon.stub()); + const element = { style: {}, addEventListener: sinon.stub(), removeEventListener: sinon.stub() }; + + manager.interactionDOMElement = element; + manager.supportsPointerEvents = false; + manager.supportsTouchEvents = true; + + manager.addEvents(); + + expect(element.addEventListener).to.have.been.calledWith('touchstart'); + expect(element.addEventListener).to.have.been.calledWith('touchcancel'); + expect(element.addEventListener).to.have.been.calledWith('touchend'); + expect(element.addEventListener).to.have.been.calledWith('touchmove'); + + manager.removeEvents(); + + expect(element.removeEventListener).to.have.been.calledWith('touchstart'); + expect(element.removeEventListener).to.have.been.calledWith('touchcancel'); + expect(element.removeEventListener).to.have.been.calledWith('touchend'); + expect(element.removeEventListener).to.have.been.calledWith('touchmove'); + }); + }); + describe('onClick', function () { it('should call handler when inside', function () @@ -364,7 +639,6 @@ expect(scene.parentCallback).to.have.been.calledOnce; }); - /* TODO: Fix #3596 it('should callback parent and behind child when clicking overlap', function () { const stage = new PIXI.Container(); @@ -382,7 +656,6 @@ expect(scene.frontChildCallback).to.not.have.been.called; expect(scene.parentCallback).to.have.been.calledOnce; }); - */ it('should callback parent and behind child when clicking behind child', function () { @@ -461,4 +734,241 @@ }); }); }); + + describe('cursor changes', function () + { + it('cursor should be the cursor of interactive item', function () + { + const stage = new PIXI.Container(); + const graphics = new PIXI.Graphics(); + const pointer = new MockPointer(stage); + + stage.addChild(graphics); + graphics.beginFill(0xFFFFFF); + graphics.drawRect(0, 0, 50, 50); + graphics.interactive = true; + graphics.cursor = 'help'; + pointer.interaction.cursorStyles.help = 'help'; + + pointer.mousemove(10, 10); + + expect(pointer.renderer.view.style.cursor).to.equal('help'); + }); + + it('should return cursor to default on mouseout', function () + { + const stage = new PIXI.Container(); + const graphics = new PIXI.Graphics(); + const pointer = new MockPointer(stage); + + stage.addChild(graphics); + graphics.beginFill(0xFFFFFF); + graphics.drawRect(0, 0, 50, 50); + graphics.interactive = true; + graphics.cursor = 'help'; + pointer.interaction.cursorStyles.help = 'help'; + + pointer.mousemove(10, 10); + pointer.mousemove(60, 60); + + expect(pointer.renderer.view.style.cursor).to.equal(pointer.interaction.cursorStyles.default); + }); + + it('should still be the over cursor after a click', function () + { + const stage = new PIXI.Container(); + const graphics = new PIXI.Graphics(); + const pointer = new MockPointer(stage); + + stage.addChild(graphics); + graphics.beginFill(0xFFFFFF); + graphics.drawRect(0, 0, 50, 50); + graphics.interactive = true; + graphics.cursor = 'help'; + pointer.interaction.cursorStyles.help = 'help'; + + pointer.mousemove(10, 10); + pointer.click(10, 10); + + expect(pointer.renderer.view.style.cursor).to.equal('help'); + }); + + it('should return cursor to default when mouse leaves renderer', function () + { + const stage = new PIXI.Container(); + const graphics = new PIXI.Graphics(); + const pointer = new MockPointer(stage); + + stage.addChild(graphics); + graphics.beginFill(0xFFFFFF); + graphics.drawRect(0, 0, 50, 50); + graphics.interactive = true; + graphics.cursor = 'help'; + pointer.interaction.cursorStyles.help = 'help'; + + pointer.mousemove(10, 10); + pointer.mousemove(-10, 60); + + expect(pointer.renderer.view.style.cursor).to.equal(pointer.interaction.cursorStyles.default); + }); + + it('cursor callback should be called', function () + { + const stage = new PIXI.Container(); + const graphics = new PIXI.Graphics(); + const overSpy = sinon.spy(); + const defaultSpy = sinon.spy(); + const pointer = new MockPointer(stage); + + stage.addChild(graphics); + graphics.beginFill(0xFFFFFF); + graphics.drawRect(0, 0, 50, 50); + graphics.interactive = true; + graphics.cursor = 'help'; + pointer.interaction.cursorStyles.help = overSpy; + pointer.interaction.cursorStyles.default = defaultSpy; + + pointer.mousemove(10, 10); + pointer.mousemove(60, 60); + + expect(overSpy).to.have.been.called; + expect(defaultSpy).to.have.been.called; + }); + + it('cursor callback should only be called if the cursor actually changed', function () + { + const stage = new PIXI.Container(); + const graphics = new PIXI.Graphics(); + const defaultSpy = sinon.spy(); + const pointer = new MockPointer(stage); + + stage.addChild(graphics); + graphics.beginFill(0xFFFFFF); + graphics.drawRect(0, 0, 50, 50); + graphics.interactive = true; + graphics.cursor = null; + pointer.interaction.cursorStyles.default = defaultSpy; + + pointer.mousemove(10, 10); + pointer.mousemove(20, 20); + + expect(defaultSpy).to.have.been.calledOnce; + }); + + it('cursor style object should be fully applied', function () + { + const stage = new PIXI.Container(); + const graphics = new PIXI.Graphics(); + const pointer = new MockPointer(stage); + + stage.addChild(graphics); + graphics.beginFill(0xFFFFFF); + graphics.drawRect(0, 0, 50, 50); + graphics.interactive = true; + graphics.cursor = 'help'; + pointer.interaction.cursorStyles.help = { + cursor: 'none', + display: 'none', + }; + + pointer.mousemove(10, 10); + + expect(pointer.renderer.view.style.cursor).to.equal('none'); + expect(pointer.renderer.view.style.display).to.equal('none'); + }); + + it('should not change cursor style if no cursor style provided', function () + { + const stage = new PIXI.Container(); + const graphics = new PIXI.Graphics(); + const pointer = new MockPointer(stage); + + stage.addChild(graphics); + graphics.beginFill(0xFFFFFF); + graphics.drawRect(0, 0, 50, 50); + graphics.interactive = true; + graphics.cursor = 'pointer'; + pointer.interaction.cursorStyles.pointer = null; + pointer.interaction.cursorStyles.default = null; + + pointer.mousemove(10, 10); + expect(pointer.renderer.view.style.cursor).to.equal(''); + + pointer.mousemove(60, 60); + expect(pointer.renderer.view.style.cursor).to.equal(''); + }); + }); + + describe('recursive hitTesting', function () + { + function getScene() + { + const stage = new PIXI.Container(); + const behindChild = new PIXI.Graphics(); + const middleChild = new PIXI.Graphics(); + const frontChild = new PIXI.Graphics(); + + behindChild.beginFill(0xFF); + behindChild.drawRect(0, 0, 50, 50); + + middleChild.beginFill(0xFF00); + middleChild.drawRect(0, 0, 50, 50); + + frontChild.beginFill(0xFF0000); + frontChild.drawRect(0, 0, 50, 50); + + stage.addChild(behindChild, middleChild, frontChild); + + return { + behindChild, + middleChild, + frontChild, + stage, + }; + } + + describe('when frontChild is interactive', function () + { + it('should stop hitTesting after first hit', function () + { + const scene = getScene(); + const pointer = new MockPointer(scene.stage); + const frontHitTest = sinon.spy(scene.frontChild, 'containsPoint'); + const middleHitTest = sinon.spy(scene.middleChild, 'containsPoint'); + const behindHitTest = sinon.spy(scene.behindChild, 'containsPoint'); + + scene.frontChild.interactive = true; + scene.middleChild.interactive = true; + scene.behindChild.interactive = true; + + pointer.mousedown(25, 25); + + expect(frontHitTest).to.have.been.calledOnce; + expect(middleHitTest).to.not.have.been.called; + expect(behindHitTest).to.not.have.been.called; + }); + }); + + describe('when frontChild is not interactive', function () + { + it('should stop hitTesting after first hit', function () + { + const scene = getScene(); + const pointer = new MockPointer(scene.stage); + const frontHitTest = sinon.spy(scene.frontChild, 'containsPoint'); + const middleHitTest = sinon.spy(scene.middleChild, 'containsPoint'); + const behindHitTest = sinon.spy(scene.behindChild, 'containsPoint'); + + scene.frontChild.interactive = false; + scene.middleChild.interactive = true; + scene.behindChild.interactive = true; + + pointer.mousedown(25, 25); + + expect(frontHitTest).to.not.have.been.called; + expect(middleHitTest).to.have.been.calledOnce; + expect(behindHitTest).to.not.have.been.called; + }); + }); + }); }); diff --git a/test/interaction/MockPointer.js b/test/interaction/MockPointer.js index 61fb0d8..67878af 100644 --- a/test/interaction/MockPointer.js +++ b/test/interaction/MockPointer.js @@ -46,6 +46,45 @@ * @param {number} x - pointer x position * @param {number} y - pointer y position */ + mousemove(x, y) + { + const mouseEvent = new MouseEvent('mousemove', { + clientX: x, + clientY: y, + preventDefault: sinon.stub(), + }); + + this.setPosition(x, y); + this.render(); + // mouseOverRenderer state should be correct, so mouse position to view rect + const rect = new PIXI.Rectangle(0, 0, this.renderer.width, this.renderer.height); + + if (rect.contains(x, y)) + { + if (!this.interaction.mouseOverRenderer) + { + this.interaction.onPointerOver(new MouseEvent('mouseover', { + clientX: x, + clientY: y, + preventDefault: sinon.stub(), + })); + } + this.interaction.onPointerMove(mouseEvent); + } + else + { + this.interaction.onPointerOut(new MouseEvent('mouseout', { + clientX: x, + clientY: y, + preventDefault: sinon.stub(), + })); + } + } + + /** + * @param {number} x - pointer x position + * @param {number} y - pointer y position + */ click(x, y) { this.mousedown(x, y); diff --git a/package.json b/package.json index 6a366ba..71ade15 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pixi.js", - "version": "4.3.4", + "version": "4.3.5", "description": "Pixi.js is a fast lightweight 2D library that works across all devices.", "author": "Mat Groves", "contributors": [ @@ -61,7 +61,7 @@ "ismobilejs": "^0.4.0", "object-assign": "^4.0.1", "pixi-gl-core": "^1.0.3", - "resource-loader": "^2.0.4" + "resource-loader": "^2.0.6" }, "devDependencies": { "babel-cli": "^6.18.0", @@ -70,9 +70,9 @@ "babel-preset-es2015": "^6.14.0", "babelify": "^7.3.0", "del": "^2.2.0", - "electron-prebuilt": "^1.3.2", + "electron": "^1.4.15", "eslint": "^3.5.0", - "floss": "^1.2.0", + "floss": "^2.0.1", "gh-pages": "^0.11.0", "jaguarjs-jsdoc": "^1.0.1", "js-md5": "^0.4.1", diff --git a/src/accessibility/AccessibilityManager.js b/src/accessibility/AccessibilityManager.js index df2716a..f16bcb1 100644 --- a/src/accessibility/AccessibilityManager.js +++ b/src/accessibility/AccessibilityManager.js @@ -3,7 +3,7 @@ import accessibleTarget from './accessibleTarget'; // add some extra variables to the container.. -Object.assign( +core.utils.mixins.delayMixin( core.DisplayObject.prototype, accessibleTarget ); diff --git a/src/core/Application.js b/src/core/Application.js index ea0f058..5340578 100644 --- a/src/core/Application.js +++ b/src/core/Application.js @@ -1,6 +1,6 @@ import { autoDetectRenderer } from './autoDetectRenderer'; import Container from './display/Container'; -import Ticker from './ticker/Ticker'; +import { shared, Ticker } from './ticker'; /** * Convenience class to create a new PIXI application. @@ -35,9 +35,9 @@ * @param {boolean} [noWebGL=false] - prevents selection of WebGL renderer, even if such is present * @param {boolean} [options.legacy=false] - If true Pixi will aim to ensure compatibility * with older / less advanced devices. If you experience unexplained flickering try setting this to true. - + * @param {boolean} [useSharedTicker=false] - `true` to use PIXI.ticker.shared, `false` to create new ticker. */ - constructor(width, height, options, noWebGL) + constructor(width, height, options, noWebGL, useSharedTicker = false) { /** * WebGL renderer if available, otherwise CanvasRenderer @@ -46,23 +46,46 @@ this.renderer = autoDetectRenderer(width, height, options, noWebGL); /** - * The root display container that's renderered. + * The root display container that's rendered. * @member {PIXI.Container} */ this.stage = new Container(); /** + * Internal reference to the ticker + * @member {PIXI.ticker.Ticker} + * @private + */ + this._ticker = null; + + /** * Ticker for doing render updates. * @member {PIXI.ticker.Ticker} + * @default PIXI.ticker.shared */ - this.ticker = new Ticker(); - - this.ticker.add(this.render, this); + this.ticker = useSharedTicker ? shared : new Ticker(); // Start the rendering this.start(); } + set ticker(ticker) // eslint-disable-line require-jsdoc + { + if (this._ticker) + { + this._ticker.remove(this.render, this); + } + this._ticker = ticker; + if (ticker) + { + ticker.add(this.render, this); + } + } + get ticker() // eslint-disable-line require-jsdoc + { + return this._ticker; + } + /** * Render the current stage. */ @@ -76,7 +99,7 @@ */ stop() { - this.ticker.stop(); + this._ticker.stop(); } /** @@ -84,7 +107,7 @@ */ start() { - this.ticker.start(); + this._ticker.start(); } /** @@ -98,13 +121,22 @@ } /** + * Reference to the renderer's screen rectangle. Its safe to use as filterArea or hitArea for whole screen + * @member {PIXI.Rectangle} + * @readonly + */ + get screen() + { + return this.renderer.screen; + } + + /** * Destroy and don't use after this. * @param {Boolean} [removeView=false] Automatically remove canvas from DOM. */ destroy(removeView) { this.stop(); - this.ticker.remove(this.render, this); this.ticker = null; this.stage.destroy(); diff --git a/src/core/Shader.js b/src/core/Shader.js new file mode 100644 index 0000000..78b17e6 --- /dev/null +++ b/src/core/Shader.js @@ -0,0 +1,46 @@ +import { GLShader } from 'pixi-gl-core'; +import settings from './settings'; + +function checkPrecision(src, def) +{ + if (src instanceof Array) + { + if (src[0].substring(0, 9) !== 'precision') + { + const copy = src.slice(0); + + copy.unshift(`precision ${def} float;`); + + return copy; + } + } + else if (src.substring(0, 9) !== 'precision') + { + return `precision ${def} float;\n${src}`; + } + + return src; +} + +/** + * Wrapper class, webGL Shader for Pixi. + * Adds precision string if vertexSrc or fragmentSrc have no mention of it. + * + * @class + * @extends GLShader + * @memberof PIXI + */ +export default class Shader extends GLShader +{ + /** + * + * @param {WebGLRenderingContext} gl - The current WebGL rendering context + * @param {string|string[]} vertexSrc - The vertex shader source as an array of strings. + * @param {string|string[]} fragmentSrc - The fragment shader source as an array of strings. + */ + constructor(gl, vertexSrc, fragmentSrc) + { + super(gl, checkPrecision(vertexSrc, settings.PRECISION_VERTEX), + checkPrecision(fragmentSrc, settings.PRECISION_FRAGMENT)); + } +} diff --git a/src/core/const.js b/src/core/const.js index ef6f662..3de0efe 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -188,7 +188,7 @@ * The gc modes that are supported by pixi. * * The {@link PIXI.settings.GC_MODE} Garbage Collection mode for pixi textures is AUTO - * If set to GC_MODE, the renderer will occasianally check textures usage. If they are not + * If set to GC_MODE, the renderer will occasionally check textures usage. If they are not * used for a specified period of time they will be removed from the GPU. They will of course * be uploaded again when they are required. This is a silent behind the scenes process that * should ensure that the GPU does not get filled up. diff --git a/src/core/display/Container.js b/src/core/display/Container.js index ed8cd5f..17fde8d 100644 --- a/src/core/display/Container.js +++ b/src/core/display/Container.js @@ -73,11 +73,12 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.push(child); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -108,11 +109,12 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.splice(index, 0, child); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -225,10 +227,11 @@ if (index === -1) return null; child.parent = null; + // ensure child transform will be recalculated + child.transform._parentID = -1; removeItems(this.children, index, 1); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -249,11 +252,12 @@ { const child = this.getChildAt(index); + // ensure child transform will be recalculated.. child.parent = null; + child.transform._parentID = -1; removeItems(this.children, index, 1); - // ensure a transform will be recalculated.. - this.transform._parentID = -1; + // ensure bounds will be recalculated this._boundsID++; // TODO - lets either do all callbacks or all events.. not both! @@ -284,13 +288,12 @@ for (let i = 0; i < removed.length; ++i) { removed[i].parent = null; + if (removed[i].transform) + { + removed[i].transform._parentID = -1; + } } - // ensure a transform will be recalculated.. - if (this.transform) - { - this.transform._parentID = -1; - } this._boundsID++; this.onChildrenChange(beginIndex); diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index f37f2f5..be4c4fb 100644 --- a/src/core/graphics/Graphics.js +++ b/src/core/graphics/Graphics.js @@ -28,8 +28,9 @@ { /** * + * @param {boolean} [nativeLines=false] - If true the lines will be draw using LINES instead of TRIANGLE_STRIP */ - constructor() + constructor(nativeLines = false) { super(); @@ -50,6 +51,13 @@ this.lineWidth = 0; /** + * If true the lines will be draw using LINES instead of TRIANGLE_STRIP + * + * @member {boolean} + */ + this.nativeLines = nativeLines; + + /** * The color of any lines drawn. * * @member {string} @@ -757,28 +765,14 @@ if (!this._spriteRect) { - if (!Graphics._SPRITE_TEXTURE) - { - Graphics._SPRITE_TEXTURE = RenderTexture.create(10, 10); - - const canvas = document.createElement('canvas'); - - canvas.width = 10; - canvas.height = 10; - - const context = canvas.getContext('2d'); - - context.fillStyle = 'white'; - context.fillRect(0, 0, 10, 10); - - Graphics._SPRITE_TEXTURE = Texture.fromCanvas(canvas); - } - - this._spriteRect = new Sprite(Graphics._SPRITE_TEXTURE); + this._spriteRect = new Sprite(new Texture(Texture.WHITE)); } + + const sprite = this._spriteRect; + if (this.tint === 0xffffff) { - this._spriteRect.tint = this.graphicsData[0].fillColor; + sprite.tint = this.graphicsData[0].fillColor; } else { @@ -792,20 +786,21 @@ t1[1] *= t2[1]; t1[2] *= t2[2]; - this._spriteRect.tint = rgb2hex(t1); + sprite.tint = rgb2hex(t1); } - this._spriteRect.alpha = this.graphicsData[0].fillAlpha; - this._spriteRect.worldAlpha = this.worldAlpha * this._spriteRect.alpha; + sprite.alpha = this.graphicsData[0].fillAlpha; + sprite.worldAlpha = this.worldAlpha * sprite.alpha; + sprite.blendMode = this.blendMode; - Graphics._SPRITE_TEXTURE._frame.width = rect.width; - Graphics._SPRITE_TEXTURE._frame.height = rect.height; + sprite.texture._frame.width = rect.width; + sprite.texture._frame.height = rect.height; - this._spriteRect.transform.worldTransform = this.transform.worldTransform; + sprite.transform.worldTransform = this.transform.worldTransform; - this._spriteRect.anchor.set(-rect.x / rect.width, -rect.y / rect.height); - this._spriteRect._onAnchorUpdate(); + sprite.anchor.set(-rect.x / rect.width, -rect.y / rect.height); + sprite._onAnchorUpdate(); - this._spriteRect._renderWebGL(renderer); + sprite._renderWebGL(renderer); } /** @@ -1031,6 +1026,7 @@ this.fillColor, this.fillAlpha, this.filling, + this.nativeLines, shape ); diff --git a/src/core/graphics/GraphicsData.js b/src/core/graphics/GraphicsData.js index f5a87ae..b6f03c0 100644 --- a/src/core/graphics/GraphicsData.js +++ b/src/core/graphics/GraphicsData.js @@ -14,14 +14,19 @@ * @param {number} fillColor - the color of the fill * @param {number} fillAlpha - the alpha of the fill * @param {boolean} fill - whether or not the shape is filled with a colour + * @param {boolean} nativeLines - the method for drawing lines * @param {PIXI.Circle|PIXI.Rectangle|PIXI.Ellipse|PIXI.Polygon} shape - The shape object to draw. */ - constructor(lineWidth, lineColor, lineAlpha, fillColor, fillAlpha, fill, shape) + constructor(lineWidth, lineColor, lineAlpha, fillColor, fillAlpha, fill, nativeLines, shape) { /** * @member {number} the width of the line to draw */ this.lineWidth = lineWidth; + /** + * @member {boolean} if true the liens will be draw using LINES instead of TRIANGLE_STRIP + */ + this.nativeLines = nativeLines; /** * @member {number} the color of the line to draw @@ -85,6 +90,7 @@ this.fillColor, this.fillAlpha, this.fill, + this.nativeLines, this.shape ); } diff --git a/src/core/graphics/webgl/GraphicsRenderer.js b/src/core/graphics/webgl/GraphicsRenderer.js index 14083fe..67f5e92 100644 --- a/src/core/graphics/webgl/GraphicsRenderer.js +++ b/src/core/graphics/webgl/GraphicsRenderer.js @@ -102,7 +102,15 @@ shaderTemp.uniforms.alpha = graphics.worldAlpha; renderer.bindVao(webGLData.vao); - webGLData.vao.draw(gl.TRIANGLE_STRIP, webGLData.indices.length); + + if (graphics.nativeLines) + { + gl.drawArrays(gl.LINES, 0, webGLData.points.length / 6); + } + else + { + webGLData.vao.draw(gl.TRIANGLE_STRIP, webGLData.indices.length); + } } } diff --git a/src/core/graphics/webgl/utils/buildLine.js b/src/core/graphics/webgl/utils/buildLine.js index ecf43f7..4d6862e 100644 --- a/src/core/graphics/webgl/utils/buildLine.js +++ b/src/core/graphics/webgl/utils/buildLine.js @@ -11,7 +11,29 @@ * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties * @param {object} webGLData - an object containing all the webGL-specific information to create this shape */ -export default function buildLine(graphicsData, webGLData) +export default function (graphicsData, webGLData) +{ + if (graphicsData.nativeLines) + { + buildNativeLine(graphicsData, webGLData); + } + else + { + buildLine(graphicsData, webGLData); + } +} + +/** + * Builds a line to draw using the poligon method. + * + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @private + * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties + * @param {object} webGLData - an object containing all the webGL-specific information to create this shape + */ +function buildLine(graphicsData, webGLData) { // TODO OPTIMISE! let points = graphicsData.points; @@ -224,3 +246,46 @@ indices.push(indexStart - 1); } + +/** + * Builds a line to draw using the gl.drawArrays(gl.LINES) method + * + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @private + * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties + * @param {object} webGLData - an object containing all the webGL-specific information to create this shape + */ +function buildNativeLine(graphicsData, webGLData) +{ + let i = 0; + const points = graphicsData.points; + + if (points.length === 0) return; + + const verts = webGLData.points; + const length = points.length / 2; + + // sort color + const color = hex2rgb(graphicsData.lineColor); + const alpha = graphicsData.lineAlpha; + const r = color[0] * alpha; + const g = color[1] * alpha; + const b = color[2] * alpha; + + for (i = 1; i < length; i++) + { + const p1x = points[(i - 1) * 2]; + const p1y = points[((i - 1) * 2) + 1]; + + const p2x = points[i * 2]; + const p2y = points[(i * 2) + 1]; + + verts.push(p1x, p1y); + verts.push(r, g, b, alpha); + + verts.push(p2x, p2y); + verts.push(r, g, b, alpha); + } +} diff --git a/src/core/graphics/webgl/utils/buildRoundedRectangle.js b/src/core/graphics/webgl/utils/buildRoundedRectangle.js index 9abc807..a6c8461 100644 --- a/src/core/graphics/webgl/utils/buildRoundedRectangle.js +++ b/src/core/graphics/webgl/utils/buildRoundedRectangle.js @@ -77,6 +77,26 @@ } /** + * Calculate a single point for a quadratic bezier curve. + * Utility function used by quadraticBezierCurve. + * Ignored from docs since it is not directly exposed. + * + * @ignore + * @private + * @param {number} n1 - first number + * @param {number} n2 - second number + * @param {number} perc - percentage + * @return {number} the result + * + */ +function getPt(n1, n2, perc) +{ + const diff = n2 - n1; + + return n1 + (diff * perc); +} + +/** * Calculate the points for a quadratic bezier curve. (helper function..) * Based on: https://stackoverflow.com/questions/785097/how-do-i-implement-a-bezier-curve-in-c * @@ -105,13 +125,6 @@ let x = 0; let y = 0; - function getPt(n1, n2, perc) - { - const diff = n2 - n1; - - return n1 + (diff * perc); - } - for (let i = 0, j = 0; i <= n; ++i) { j = i / n; diff --git a/src/core/index.js b/src/core/index.js index f7a817c..4f414e8 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -30,6 +30,7 @@ export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; export { default as CanvasGraphicsRenderer } from './graphics/canvas/CanvasGraphicsRenderer'; +export { default as Spritesheet } from './textures/Spritesheet'; export { default as Texture } from './textures/Texture'; export { default as BaseTexture } from './textures/BaseTexture'; export { default as RenderTexture } from './textures/RenderTexture'; diff --git a/src/core/math/Matrix.js b/src/core/math/Matrix.js index 0b77477..3679860 100644 --- a/src/core/math/Matrix.js +++ b/src/core/math/Matrix.js @@ -13,45 +13,50 @@ export default class Matrix { /** - * + * @param {number} [a=1] - x scale + * @param {number} [b=0] - y skew + * @param {number} [c=0] - x skew + * @param {number} [d=1] - y scale + * @param {number} [tx=0] - x translation + * @param {number} [ty=0] - y translation */ - constructor() + constructor(a = 1, b = 0, c = 0, d = 1, tx = 0, ty = 0) { /** * @member {number} * @default 1 */ - this.a = 1; + this.a = a; /** * @member {number} * @default 0 */ - this.b = 0; + this.b = b; /** * @member {number} * @default 0 */ - this.c = 0; + this.c = c; /** * @member {number} * @default 1 */ - this.d = 1; + this.d = d; /** * @member {number} * @default 0 */ - this.tx = 0; + this.tx = tx; /** * @member {number} * @default 0 */ - this.ty = 0; + this.ty = ty; this.array = null; } diff --git a/src/core/renderers/SystemRenderer.js b/src/core/renderers/SystemRenderer.js index fa116a6..a24c775 100644 --- a/src/core/renderers/SystemRenderer.js +++ b/src/core/renderers/SystemRenderer.js @@ -1,5 +1,5 @@ import { sayHello, hex2string, hex2rgb } from '../utils'; -import { Matrix } from '../math'; +import { Matrix, Rectangle } from '../math'; import { RENDERER_TYPE } from '../const'; import settings from '../settings'; import Container from '../display/Container'; @@ -21,8 +21,8 @@ { /** * @param {string} system - The name of the system this renderer is for. - * @param {number} [width=800] - the width of the canvas view - * @param {number} [height=600] - the height of the canvas view + * @param {number} [screenWidth=800] - the width of the screen + * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false @@ -37,7 +37,7 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(system, width, height, options) + constructor(system, screenWidth, screenHeight, options) { super(); @@ -69,20 +69,13 @@ this.type = RENDERER_TYPE.UNKNOWN; /** - * The width of the canvas view + * Measurements of the screen. (0, 0, screenWidth, screenHeight) * - * @member {number} - * @default 800 - */ - this.width = width || 800; - - /** - * The height of the canvas view + * Its safe to use as filterArea or hitArea for whole stage * - * @member {number} - * @default 600 + * @member {PIXI.Rectangle} */ - this.height = height || 600; + this.screen = new Rectangle(0, 0, screenWidth || 800, screenHeight || 600); /** * The canvas element that everything is drawn to @@ -107,7 +100,7 @@ this.transparent = options.transparent; /** - * Whether the render view should be resized automatically + * Whether css dimensions of canvas view should be resized to screen dimensions automatically * * @member {boolean} */ @@ -192,23 +185,48 @@ } /** - * Resizes the canvas view to the specified width and height + * Same as view.width, actual number of pixels in the canvas by horizontal * - * @param {number} width - the new width of the canvas view - * @param {number} height - the new height of the canvas view + * @member {number} + * @readonly + * @default 800 */ - resize(width, height) + get width() { - this.width = width * this.resolution; - this.height = height * this.resolution; + return this.view.width; + } - this.view.width = this.width; - this.view.height = this.height; + /** + * Same as view.height, actual number of pixels in the canvas by vertical + * + * @member {number} + * @readonly + * @default 600 + */ + get height() + { + return this.view.height; + } + + /** + * Resizes the screen and canvas to the specified width and height + * Canvas dimensions are multiplied by resolution + * + * @param {number} screenWidth - the new width of the screen + * @param {number} screenHeight - the new height of the screen + */ + resize(screenWidth, screenHeight) + { + this.screen.width = screenWidth; + this.screen.height = screenHeight; + + this.view.width = screenWidth * this.resolution; + this.view.height = screenHeight * this.resolution; if (this.autoResize) { - this.view.style.width = `${this.width / this.resolution}px`; - this.view.style.height = `${this.height / this.resolution}px`; + this.view.style.width = `${screenWidth}px`; + this.view.style.height = `${screenHeight}px`; } } @@ -249,11 +267,10 @@ this.type = RENDERER_TYPE.UNKNOWN; - this.width = 0; - this.height = 0; - this.view = null; + this.screen = null; + this.resolution = 0; this.transparent = false; diff --git a/src/core/renderers/canvas/CanvasRenderer.js b/src/core/renderers/canvas/CanvasRenderer.js index 1bd2d5e..e84e49c 100644 --- a/src/core/renderers/canvas/CanvasRenderer.js +++ b/src/core/renderers/canvas/CanvasRenderer.js @@ -18,8 +18,8 @@ export default class CanvasRenderer extends SystemRenderer { /** - * @param {number} [width=800] - the width of the canvas view - * @param {number} [height=600] - the height of the canvas view + * @param {number} [screenWidth=800] - the width of the screen + * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false @@ -34,9 +34,9 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(width, height, options = {}) + constructor(screenWidth, screenHeight, options = {}) { - super('Canvas', width, height, options); + super('Canvas', screenWidth, screenHeight, options); this.type = RENDERER_TYPE.CANVAS; @@ -96,7 +96,7 @@ this.context = null; this.renderingToScreen = false; - this.resize(width, height); + this.resize(screenWidth, screenHeight); } /** @@ -283,12 +283,12 @@ * * @extends PIXI.SystemRenderer#resize * - * @param {number} width - The new width of the canvas view - * @param {number} height - The new height of the canvas view + * @param {number} screenWidth - the new width of the screen + * @param {number} screenHeight - the new height of the screen */ - resize(width, height) + resize(screenWidth, screenHeight) { - super.resize(width, height); + super.resize(screenWidth, screenHeight); // reset the scale mode.. oddly this seems to be reset when the canvas is resized. // surely a browser bug?? Let pixi fix that for you.. diff --git a/src/core/renderers/webgl/TextureManager.js b/src/core/renderers/webgl/TextureManager.js index b685c1a..8227289 100644 --- a/src/core/renderers/webgl/TextureManager.js +++ b/src/core/renderers/webgl/TextureManager.js @@ -61,7 +61,7 @@ * Updates and/or Creates a WebGL texture for the renderer's context. * * @param {PIXI.BaseTexture|PIXI.Texture} texture - the texture to update - * @param {Number} location - the location the texture will be bound to. + * @param {number} location - the location the texture will be bound to. * @return {GLTexture} The gl texture. */ updateTexture(texture, location) diff --git a/src/core/renderers/webgl/WebGLRenderer.js b/src/core/renderers/webgl/WebGLRenderer.js index e4cd8a0..830b382 100644 --- a/src/core/renderers/webgl/WebGLRenderer.js +++ b/src/core/renderers/webgl/WebGLRenderer.js @@ -31,8 +31,8 @@ { /** * - * @param {number} [width=0] - the width of the canvas view - * @param {number} [height=0] - the height of the canvas view + * @param {number} [screenWidth=800] - the width of the screen + * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false @@ -53,9 +53,9 @@ * @param {boolean} [options.legacy=false] - If true Pixi will aim to ensure compatibility * with older / less advanced devices. If you experiance unexplained flickering try setting this to true. */ - constructor(width, height, options = {}) + constructor(screenWidth, screenHeight, options = {}) { - super('WebGL', width, height, options); + super('WebGL', screenWidth, screenHeight, options); this.legacy = !!options.legacy; @@ -245,7 +245,7 @@ glCore._testingContext = gl; // setup the width/height properties and gl viewport - this.resize(this.width, this.height); + this.resize(this.screen.width, this.screen.height); } /** @@ -343,16 +343,16 @@ /** * Resizes the webGL view to the specified width and height. * - * @param {number} width - the new width of the webGL view - * @param {number} height - the new height of the webGL view + * @param {number} screenWidth - the new width of the screen + * @param {number} screenHeight - the new height of the screen */ - resize(width, height) + resize(screenWidth, screenHeight) { // if(width * this.resolution === this.width && height * this.resolution === this.height)return; - SystemRenderer.prototype.resize.call(this, width, height); + SystemRenderer.prototype.resize.call(this, screenWidth, screenHeight); - this.rootRenderTarget.resize(width, height); + this.rootRenderTarget.resize(screenWidth, screenHeight); if (this._activeRenderTarget === this.rootRenderTarget) { @@ -396,6 +396,26 @@ } /** + * Erases the render texture and fills the drawing area with a colour + * + * @param {PIXI.RenderTexture} renderTexture - The render texture to clear + * @param {number} [clearColor] - The colour + * @return {PIXI.WebGLRenderer} Returns itself. + */ + clearRenderTexture(renderTexture, clearColor) + { + const baseTexture = renderTexture.baseTexture; + const renderTarget = baseTexture._glRenderTargets[this.CONTEXT_UID]; + + if (renderTarget) + { + renderTarget.clear(clearColor); + } + + return this; + } + + /** * Binds a render texture for rendering * * @param {PIXI.RenderTexture} renderTexture - The render texture to render @@ -474,9 +494,10 @@ * Changes the current GLShader to the one given in parameter * * @param {PIXI.glCore.Shader} shader - the new glShader + * @param {boolean} [autoProject=true] - Whether automatically set the projection matrix * @return {PIXI.WebGLRenderer} Returns itself. */ - _bindGLShader(shader) + _bindGLShader(shader, autoProject) { // TODO cache if (this._activeShader !== shader) @@ -484,8 +505,14 @@ this._activeShader = shader; shader.bind(); - // automatically set the projection matrix - shader.uniforms.projectionMatrix = this._activeRenderTarget.projectionMatrix.toArray(true); + // `autoProject` normally would be a default parameter set to true + // but because of how Babel transpiles default parameters + // it hinders the performance of this method. + if (autoProject !== false) + { + // automatically set the projection matrix + shader.uniforms.projectionMatrix = this._activeRenderTarget.projectionMatrix.toArray(true); + } } return this; diff --git a/src/core/renderers/webgl/WebGLState.js b/src/core/renderers/webgl/WebGLState.js index ba6ecc5..6c846d5 100755 --- a/src/core/renderers/webgl/WebGLState.js +++ b/src/core/renderers/webgl/WebGLState.js @@ -82,18 +82,20 @@ push() { // next state.. - let state = this.stack[++this.stackIndex]; + let state = this.stack[this.stackIndex]; if (!state) { state = this.stack[this.stackIndex] = new Uint8Array(16); } + ++this.stackIndex; + // copy state.. // set active state so we can force overrides of gl state for (let i = 0; i < this.activeState.length; i++) { - this.activeState[i] = state[i]; + state[i] = this.activeState[i]; } } diff --git a/src/core/renderers/webgl/filters/Filter.js b/src/core/renderers/webgl/filters/Filter.js index 2c3eaad..4eb6128 100644 --- a/src/core/renderers/webgl/filters/Filter.js +++ b/src/core/renderers/webgl/filters/Filter.js @@ -9,7 +9,7 @@ * @memberof PIXI * @extends PIXI.Shader */ -class Filter extends Shader +export default class Filter extends Shader { /** * @param {string} [vertexSrc] - The source of the vertex shader. @@ -56,15 +56,16 @@ * @param {PIXI.RenderTarget} input - The input render target. * @param {PIXI.RenderTarget} output - The target to output to. * @param {boolean} clear - Should the output be cleared before rendering to it + * @param {object} [currentState] - It's current state of filter. + * There are some useful properties in the currentState : + * target, filters, sourceFrame, destinationFrame, renderTarget, resolution */ - apply(filterManager, input, output, clear) + apply(filterManager, input, output, clear, currentState) // eslint-disable-line no-unused-vars { // do as you please! - filterManager.applyFilter(this, input, output, clear); + filterManager.applyFilter(this, input, output, clear, currentState); // or just do a regular render.. } } - -export default Filter; diff --git a/src/core/renderers/webgl/filters/extractUniformsFromSrc.js b/src/core/renderers/webgl/filters/extractUniformsFromSrc.js index d78a0b1..4893214 100644 --- a/src/core/renderers/webgl/filters/extractUniformsFromSrc.js +++ b/src/core/renderers/webgl/filters/extractUniformsFromSrc.js @@ -12,7 +12,7 @@ function extractUniformsFromString(string) { - const maskRegex = new RegExp('^(projectionMatrix|uSampler|filterArea)$'); + const maskRegex = new RegExp('^(projectionMatrix|uSampler|filterArea|filterClamp)$'); const uniforms = {}; let nameSplit; diff --git a/src/core/renderers/webgl/managers/FilterManager.js b/src/core/renderers/webgl/managers/FilterManager.js index 43fc869..6c7422d 100644 --- a/src/core/renderers/webgl/managers/FilterManager.js +++ b/src/core/renderers/webgl/managers/FilterManager.js @@ -2,6 +2,7 @@ import RenderTarget from '../utils/RenderTarget'; import Quad from '../utils/Quad'; import { Rectangle } from '../../../math'; +import Shader from '../../../Shader'; import * as filterTransforms from '../filters/filterTransforms'; import bitTwiddle from 'bit-twiddle'; @@ -22,8 +23,6 @@ this.filters = []; this.target = null; this.resolution = 1; - - this.firstRun = true; } } @@ -155,7 +154,7 @@ if (filters.length === 1) { - filters[0].apply(this, currentState.renderTarget, lastState.renderTarget, false); + filters[0].apply(this, currentState.renderTarget, lastState.renderTarget, false, currentState); this.freePotRenderTarget(currentState.renderTarget); } else @@ -177,7 +176,7 @@ for (i = 0; i < filters.length - 1; ++i) { - filters[i].apply(this, flip, flop, true); + filters[i].apply(this, flip, flop, true, currentState); const t = flip; @@ -185,7 +184,7 @@ flop = t; } - filters[i].apply(this, flip, lastState.renderTarget, false); + filters[i].apply(this, flip, lastState.renderTarget, false, currentState); this.freePotRenderTarget(flip); this.freePotRenderTarget(flop); @@ -212,16 +211,28 @@ const renderer = this.renderer; const gl = renderer.gl; - renderer.bindShader(filter, true); + let shader = filter.glShaders[renderer.CONTEXT_UID]; - // TODO theres a better way! - // quad can be a mesh and we then should be able to pull this off without accessing the shader directly - const shader = renderer.shaderManager.getGLShader(); - - // TODO change quad to mesh.. - if (!this.firstRun) + // cacheing.. + if (!shader) { - this.firstRun = true; + if (filter.glShaderKey) + { + shader = this.shaderCache[filter.glShaderKey]; + + if (!shader) + { + shader = new Shader(this.gl, filter.vertexSrc, filter.fragmentSrc); + + filter.glShaders[renderer.CONTEXT_UID] = this.shaderCache[filter.glShaderKey] = shader; + } + } + else + { + shader = filter.glShaders[renderer.CONTEXT_UID] = new Shader(this.gl, filter.vertexSrc, filter.fragmentSrc); + } + + // TODO - this only needs to be done once? renderer.bindVao(null); this.quad.initVao(shader); @@ -244,58 +255,146 @@ renderer.maskManager.pushScissorMask(null, renderer.maskManager.scissorData); } - // renderer._bindGLShader(shader); + renderer.bindShader(shader); - let currentState; - const uniforms = filter.uniforms; + // free unit 0 for us, doesn't matter what was there + // don't try to restore it, because syncUniforms can upload it to another slot + // and it'll be a problem + const tex = this.renderer.emptyTextures[0]; - if (uniforms.filterArea) - { - currentState = this.filterData.stack[this.filterData.index]; - - const filterArea = uniforms.filterArea; - - filterArea[0] = currentState.renderTarget.size.width; - filterArea[1] = currentState.renderTarget.size.height; - filterArea[2] = currentState.sourceFrame.x; - filterArea[3] = currentState.sourceFrame.y; - - filter.uniforms.filterArea = filterArea; - } - - // use this to clamp displaced texture coords so they belong to filterArea - // see displacementFilter fragment shader for an example - if (uniforms.filterClamp) - { - currentState = this.filterData.stack[this.filterData.index]; - - const filterClamp = uniforms.filterClamp; - - filterClamp[0] = 0; - filterClamp[1] = 0; - filterClamp[2] = (currentState.sourceFrame.width - 1) / currentState.renderTarget.size.width; - filterClamp[3] = (currentState.sourceFrame.height - 1) / currentState.renderTarget.size.height; - - filter.uniforms.filterClamp = filterClamp; - } - - renderer.shaderManager.setUniforms(filter.uniforms); + this.renderer.boundTextures[0] = tex; + // this syncs the pixi filters uniforms with glsl uniforms + this.syncUniforms(shader, filter); renderer.state.setBlendMode(filter.blendMode); - // temporary bypass cache.. - const tex = this.renderer.boundTextures[0]; - gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, input.texture.texture); this.quad.vao.draw(this.renderer.gl.TRIANGLES, 6, 0); - // restore cache. gl.bindTexture(gl.TEXTURE_2D, tex._glTextures[this.renderer.CONTEXT_UID].texture); } /** + * Uploads the uniforms of the filter. + * + * @param {GLShader} shader - The underlying gl shader. + * @param {PIXI.Filter} filter - The filter we are synchronizing. + */ + syncUniforms(shader, filter) + { + const uniformData = filter.uniformData; + const uniforms = filter.uniforms; + + // 0 is reserved for the pixi texture so we start at 1! + let textureCount = 1; + let currentState; + + // filterArea and filterClamp that are handled by FilterManager directly + // they must not appear in uniformData + + if (shader.uniforms.filterArea) + { + currentState = this.filterData.stack[this.filterData.index]; + + const filterArea = shader.uniforms.filterArea; + + filterArea[0] = currentState.renderTarget.size.width; + filterArea[1] = currentState.renderTarget.size.height; + filterArea[2] = currentState.sourceFrame.x; + filterArea[3] = currentState.sourceFrame.y; + + shader.uniforms.filterArea = filterArea; + } + + // use this to clamp displaced texture coords so they belong to filterArea + // see displacementFilter fragment shader for an example + if (shader.uniforms.filterClamp) + { + currentState = currentState || this.filterData.stack[this.filterData.index]; + + const filterClamp = shader.uniforms.filterClamp; + + filterClamp[0] = 0; + filterClamp[1] = 0; + filterClamp[2] = (currentState.sourceFrame.width - 1) / currentState.renderTarget.size.width; + filterClamp[3] = (currentState.sourceFrame.height - 1) / currentState.renderTarget.size.height; + + shader.uniforms.filterClamp = filterClamp; + } + + // TODO Cacheing layer.. + for (const i in uniformData) + { + if (uniformData[i].type === 'sampler2D' && uniforms[i] !== 0) + { + if (uniforms[i].baseTexture) + { + shader.uniforms[i] = this.renderer.bindTexture(uniforms[i].baseTexture, textureCount); + } + else + { + shader.uniforms[i] = textureCount; + + // TODO + // this is helpful as renderTargets can also be set. + // Although thinking about it, we could probably + // make the filter texture cache return a RenderTexture + // rather than a renderTarget + const gl = this.renderer.gl; + + this.renderer.boundTextures[textureCount] = this.renderer.emptyTextures[textureCount]; + gl.activeTexture(gl.TEXTURE0 + textureCount); + + uniforms[i].texture.bind(); + } + + textureCount++; + } + else if (uniformData[i].type === 'mat3') + { + // check if its pixi matrix.. + if (uniforms[i].a !== undefined) + { + shader.uniforms[i] = uniforms[i].toArray(true); + } + else + { + shader.uniforms[i] = uniforms[i]; + } + } + else if (uniformData[i].type === 'vec2') + { + // check if its a point.. + if (uniforms[i].x !== undefined) + { + const val = shader.uniforms[i] || new Float32Array(2); + + val[0] = uniforms[i].x; + val[1] = uniforms[i].y; + shader.uniforms[i] = val; + } + else + { + shader.uniforms[i] = uniforms[i]; + } + } + else if (uniformData[i].type === 'float') + { + if (shader.uniforms.data[i].value !== uniformData[i]) + { + shader.uniforms[i] = uniforms[i]; + } + } + else + { + shader.uniforms[i] = uniforms[i]; + } + } + } + + /** * Gets a render target from the pool, or creates a new one. * * @param {boolean} clear - Should we clear the render texture when we get it? @@ -442,7 +541,6 @@ renderTarget.resolution = resolution; renderTarget.defaultFrame.width = renderTarget.size.width = minWidth / resolution; renderTarget.defaultFrame.height = renderTarget.size.height = minHeight / resolution; - renderTarget.attachStencilBuffer(); return renderTarget; } diff --git a/src/core/settings.js b/src/core/settings.js index bb08cb0..f4dd087 100644 --- a/src/core/settings.js +++ b/src/core/settings.js @@ -77,11 +77,11 @@ * * @static * @memberof PIXI.settings - * @type {RegExp|string} + * @type {RegExp} * @example `@2x` - * @default /@(.+)x/ + * @default /@([0-9\.]+)x/ */ - RETINA_PREFIX: /@(.+)x/, + RETINA_PREFIX: /@([0-9\.]+)x/, /** * The default render options if none are supplied to {@link PIXI.WebGLRenderer} @@ -175,14 +175,24 @@ SCALE_MODE: 0, /** - * Default specify float precision in shaders. + * Default specify float precision in vertex shader. + * + * @static + * @memberof PIXI.settings + * @type {PIXI.PRECISION} + * @default PIXI.PRECISION.HIGH + */ + PRECISION_VERTEX: 'highp', + + /** + * Default specify float precision in fragment shader. * * @static * @memberof PIXI.settings * @type {PIXI.PRECISION} * @default PIXI.PRECISION.MEDIUM */ - PRECISION: 'mediump', + PRECISION_FRAGMENT: 'mediump', /** * Can we upload the same buffer in a single frame? diff --git a/src/core/sprites/Sprite.js b/src/core/sprites/Sprite.js index 28b0b18..121bf58 100644 --- a/src/core/sprites/Sprite.js +++ b/src/core/sprites/Sprite.js @@ -212,11 +212,11 @@ } else { - w0 = orig.width * (1 - anchor._x); - w1 = orig.width * -anchor._x; + w1 = -anchor._x * orig.width; + w0 = w1 + orig.width; - h0 = orig.height * (1 - anchor._y); - h1 = orig.height * -anchor._y; + h1 = -anchor._y * orig.height; + h0 = h1 + orig.height; } // xy @@ -269,11 +269,11 @@ const tx = wt.tx; const ty = wt.ty; - const w0 = (orig.width) * (1 - anchor._x); - const w1 = (orig.width) * -anchor._x; + const w1 = -anchor._x * orig.width; + const w0 = w1 + orig.width; - const h0 = orig.height * (1 - anchor._y); - const h1 = orig.height * -anchor._y; + const h1 = -anchor._y * orig.height; + const h0 = h1 + orig.height; // xy vertexData[0] = (a * w1) + (c * h1) + tx; @@ -440,7 +440,7 @@ * * @static * @param {number|string|PIXI.BaseTexture|HTMLCanvasElement|HTMLVideoElement} source Source to create texture from - * @return {PIXI.Sprite} The newly created texture + * @return {PIXI.Sprite} The newly created sprite */ static from(source) { @@ -538,8 +538,8 @@ } /** - * The tint applied to the sprite. This is a hex value. A value of - * 0xFFFFFF will remove any tint effect. + * The tint applied to the sprite. This is a hex value. + * A value of 0xFFFFFF will remove any tint effect. * * @member {number} * @default 0xFFFFFF diff --git a/src/core/sprites/canvas/CanvasTinter.js b/src/core/sprites/canvas/CanvasTinter.js index 1cf316b..27e5e0c 100644 --- a/src/core/sprites/canvas/CanvasTinter.js +++ b/src/core/sprites/canvas/CanvasTinter.js @@ -75,8 +75,8 @@ crop.width *= resolution; crop.height *= resolution; - canvas.width = crop.width; - canvas.height = crop.height; + canvas.width = Math.ceil(crop.width); + canvas.height = Math.ceil(crop.height); context.fillStyle = `#${(`00000${(color | 0).toString(16)}`).substr(-6)}`; @@ -130,8 +130,8 @@ crop.width *= resolution; crop.height *= resolution; - canvas.width = crop.width; - canvas.height = crop.height; + canvas.width = Math.ceil(crop.width); + canvas.height = Math.ceil(crop.height); context.globalCompositeOperation = 'copy'; context.fillStyle = `#${(`00000${(color | 0).toString(16)}`).substr(-6)}`; @@ -172,8 +172,8 @@ crop.width *= resolution; crop.height *= resolution; - canvas.width = crop.width; - canvas.height = crop.height; + canvas.width = Math.ceil(crop.width); + canvas.height = Math.ceil(crop.height); context.globalCompositeOperation = 'copy'; context.drawImage( diff --git a/src/core/sprites/webgl/SpriteRenderer.js b/src/core/sprites/webgl/SpriteRenderer.js index 2affcd5..7c352e2 100644 --- a/src/core/sprites/webgl/SpriteRenderer.js +++ b/src/core/sprites/webgl/SpriteRenderer.js @@ -30,7 +30,7 @@ /** * Number of values sent in the vertex buffer. - * positionX, positionY, colorR, colorG, colorB = 5 + * aVertexPosition(2), aTextureCoord(1), aColor(1), aTextureId(1) = 5 * * @member {number} */ @@ -75,7 +75,6 @@ this.shader = null; this.currentIndex = 0; - TICK = 0; this.groups = []; for (let k = 0; k < this.size; k++) diff --git a/src/core/textures/BaseTexture.js b/src/core/textures/BaseTexture.js index 853de7e..aeccf39 100644 --- a/src/core/textures/BaseTexture.js +++ b/src/core/textures/BaseTexture.js @@ -232,16 +232,24 @@ this.realWidth = this.source.naturalWidth || this.source.videoWidth || this.source.width; this.realHeight = this.source.naturalHeight || this.source.videoHeight || this.source.height; - this.width = this.realWidth / this.resolution; - this.height = this.realHeight / this.resolution; - - this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); + this._updateDimensions(); } this.emit('update', this); } /** + * Update dimensions from real values + */ + _updateDimensions() + { + this.width = this.realWidth / this.resolution; + this.height = this.realHeight / this.resolution; + + this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); + } + + /** * Load a source. * * If the source is not-immediately-available, such as an image that needs to be @@ -525,11 +533,7 @@ this.realWidth = Math.round(svgWidth * this.sourceScale); this.realHeight = Math.round(svgHeight * this.sourceScale); - this.width = this.realWidth / this.resolution; - this.height = this.realHeight / this.resolution; - - // Check pow2 after scale - this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); + this._updateDimensions(); // Create a canvas element const canvas = document.createElement('canvas'); @@ -690,4 +694,52 @@ return baseTexture; } + + /** + * Helper function that creates a base texture based on the source you provide. + * The source can be - image url, image element, canvas element. + * + * @static + * @param {string|HTMLImageElement|HTMLCanvasElement} source - The source to create base texture from. + * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values + * @param {number} [sourceScale=(auto)] - Scale for the original image, used with Svg images. + * @return {PIXI.BaseTexture} The new base texture. + */ + static from(source, scaleMode, sourceScale) + { + if (typeof source === 'string') + { + return BaseTexture.fromImage(source, undefined, scaleMode, sourceScale); + } + else if (source instanceof HTMLImageElement) + { + const imageUrl = source.src; + let baseTexture = BaseTextureCache[imageUrl]; + + if (!baseTexture) + { + baseTexture = new BaseTexture(source, scaleMode); + baseTexture.imageUrl = imageUrl; + + if (sourceScale) + { + baseTexture.sourceScale = sourceScale; + } + + // if there is an @2x at the end of the url we are going to assume its a highres image + baseTexture.resolution = getResolutionOfUrl(imageUrl); + + BaseTextureCache[imageUrl] = baseTexture; + } + + return baseTexture; + } + else if (source instanceof HTMLCanvasElement) + { + return BaseTexture.fromCanvas(source, scaleMode); + } + + // lets assume its a base texture! + return source; + } } diff --git a/src/core/textures/Spritesheet.js b/src/core/textures/Spritesheet.js new file mode 100644 index 0000000..d469c62 --- /dev/null +++ b/src/core/textures/Spritesheet.js @@ -0,0 +1,274 @@ +import { Rectangle, Texture } from '../'; +import { getResolutionOfUrl, TextureCache } from '../utils'; + +/** + * Utility class for maintaining reference to a collection + * of Textures on a single Spritesheet. + * + * @class + * @memberof PIXI + */ +export default class Spritesheet +{ + /** + * The maximum number of Textures to build per process. + * + * @type {number} + * @default 1000 + */ + static get BATCH_SIZE() + { + return 1000; + } + + /** + * @param {PIXI.BaseTexture} baseTexture Reference to the source BaseTexture object. + * @param {Object} data - Spritesheet image data. + * @param {string} [resolutionFilename] - The filename to consider when determining + * the resolution of the spritesheet. If not provided, the imageUrl will + * be used on the BaseTexture. + */ + constructor(baseTexture, data, resolutionFilename = null) + { + /** + * Reference to ths source texture + * @type {PIXI.BaseTexture} + */ + this.baseTexture = baseTexture; + + /** + * Map of spritesheet textures. + * @type {Object} + */ + this.textures = {}; + + /** + * Reference to the original JSON data. + * @type {Object} + */ + this.data = data; + + /** + * The resolution of the spritesheet. + * @type {number} + */ + this.resolution = this._updateResolution( + resolutionFilename || this.baseTexture.imageUrl + ); + + /** + * Map of spritesheet frames. + * @type {Object} + * @private + */ + this._frames = this.data.frames; + + /** + * Collection of frame names. + * @type {string[]} + * @private + */ + this._frameKeys = Object.keys(this._frames); + + /** + * Current batch index being processed. + * @type {number} + * @private + */ + this._batchIndex = 0; + + /** + * Callback when parse is completed. + * @type {Function} + * @private + */ + this._callback = null; + } + + /** + * Generate the resolution from the filename or fallback + * to the meta.scale field of the JSON data. + * + * @private + * @param {string} resolutionFilename - The filename to use for resolving + * the default resolution. + * @return {number} Resolution to use for spritesheet. + */ + _updateResolution(resolutionFilename) + { + const scale = this.data.meta.scale; + + // Use a defaultValue of `null` to check if a url-based resolution is set + let resolution = getResolutionOfUrl(resolutionFilename, null); + + // No resolution found via URL + if (resolution === null) + { + // Use the scale value or default to 1 + resolution = scale !== undefined ? parseFloat(scale) : 1; + } + + // For non-1 resolutions, update baseTexture + if (resolution !== 1) + { + this.baseTexture.resolution = resolution; + this.baseTexture.update(); + } + + return resolution; + } + + /** + * Parser spritesheet from loaded data. This is done asynchronously + * to prevent creating too many Texture within a single process. + * + * @param {Function} callback - Callback when complete returns + * a map of the Textures for this spritesheet. + */ + parse(callback) + { + this._batchIndex = 0; + this._callback = callback; + + if (this._frameKeys.length <= Spritesheet.BATCH_SIZE) + { + this._processFrames(0); + this._parseComplete(); + } + else + { + this._nextBatch(); + } + } + + /** + * Process a batch of frames + * + * @private + * @param {number} initialFrameIndex - The index of frame to start. + */ + _processFrames(initialFrameIndex) + { + let frameIndex = initialFrameIndex; + const maxFrames = Spritesheet.BATCH_SIZE; + + while (frameIndex - initialFrameIndex < maxFrames && frameIndex < this._frameKeys.length) + { + const i = this._frameKeys[frameIndex]; + const rect = this._frames[i].frame; + + if (rect) + { + let frame = null; + let trim = null; + const orig = new Rectangle( + 0, + 0, + this._frames[i].sourceSize.w / this.resolution, + this._frames[i].sourceSize.h / this.resolution + ); + + if (this._frames[i].rotated) + { + frame = new Rectangle( + rect.x / this.resolution, + rect.y / this.resolution, + rect.h / this.resolution, + rect.w / this.resolution + ); + } + else + { + frame = new Rectangle( + rect.x / this.resolution, + rect.y / this.resolution, + rect.w / this.resolution, + rect.h / this.resolution + ); + } + + // Check to see if the sprite is trimmed + if (this._frames[i].trimmed) + { + trim = new Rectangle( + this._frames[i].spriteSourceSize.x / this.resolution, + this._frames[i].spriteSourceSize.y / this.resolution, + rect.w / this.resolution, + rect.h / this.resolution + ); + } + + this.textures[i] = new Texture( + this.baseTexture, + frame, + orig, + trim, + this._frames[i].rotated ? 2 : 0 + ); + + // lets also add the frame to pixi's global cache for fromFrame and fromImage functions + TextureCache[i] = this.textures[i]; + } + + frameIndex++; + } + } + + /** + * The parse has completed. + * + * @private + */ + _parseComplete() + { + const callback = this._callback; + + this._callback = null; + this._batchIndex = 0; + callback.call(this, this.textures); + } + + /** + * Begin the next batch of textures. + * + * @private + */ + _nextBatch() + { + this._processFrames(this._batchIndex * Spritesheet.BATCH_SIZE); + this._batchIndex++; + setTimeout(() => + { + if (this._batchIndex * Spritesheet.BATCH_SIZE < this._frameKeys.length) + { + this._nextBatch(); + } + else + { + this._parseComplete(); + } + }, 0); + } + + /** + * Destroy Spritesheet and don't use after this. + * + * @param {boolean} [destroyBase=false] Whether to destroy the base texture as well + */ + destroy(destroyBase = false) + { + for (const i in this.textures) + { + this.textures[i].destroy(); + } + this._frames = null; + this._frameKeys = null; + this.data = null; + this.textures = null; + if (destroyBase) + { + this.baseTexture.destroy(); + } + this.baseTexture = null; + } +} diff --git a/src/core/textures/Texture.js b/src/core/textures/Texture.js index 6403d4f..afe4254 100644 --- a/src/core/textures/Texture.js +++ b/src/core/textures/Texture.js @@ -3,7 +3,7 @@ import TextureUvs from './TextureUvs'; import EventEmitter from 'eventemitter3'; import { Rectangle } from '../math'; -import { TextureCache, BaseTextureCache } from '../utils'; +import { TextureCache, BaseTextureCache, getResolutionOfUrl } from '../utils'; /** * A texture stores the information that represents an image or part of an image. It cannot be added @@ -365,7 +365,8 @@ * The source can be - frame id, image url, video url, canvas element, video element, base texture * * @static - * @param {number|string|PIXI.BaseTexture|HTMLCanvasElement|HTMLVideoElement} source - Source to create texture from + * @param {number|string|HTMLImageElement|HTMLCanvasElement|HTMLVideoElement|PIXI.BaseTexture} + * source - Source to create texture from * @return {PIXI.Texture} The newly created texture */ static from(source) @@ -393,7 +394,7 @@ } else if (source instanceof HTMLImageElement) { - return new Texture(new BaseTexture(source)); + return new Texture(BaseTexture.from(source)); } else if (source instanceof HTMLCanvasElement) { @@ -413,6 +414,43 @@ } /** + * Create a texture from a source and add to the cache. + * + * @static + * @param {HTMLImageElement|HTMLCanvasElement} source - The input source. + * @param {String} imageUrl - File name of texture, for cache and resolving resolution. + * @param {String} [name] - Human readible name for the texture cache. If no name is + * specified, only `imageUrl` will be used as the cache ID. + * @return {PIXI.Texture} Output texture + */ + static fromLoader(source, imageUrl, name) + { + const baseTexture = new BaseTexture(source, null, getResolutionOfUrl(imageUrl)); + const texture = new Texture(baseTexture); + + baseTexture.imageUrl = imageUrl; + + // No name, use imageUrl instead + if (!name) + { + name = imageUrl; + } + + // lets also add the frame to pixi's global cache for fromFrame and fromImage fucntions + BaseTextureCache[name] = baseTexture; + TextureCache[name] = texture; + + // also add references by url if they are different. + if (name !== imageUrl) + { + BaseTextureCache[imageUrl] = baseTexture; + TextureCache[imageUrl] = texture; + } + + return texture; + } + + /** * Adds a texture to the global TextureCache. This cache is shared across the whole PIXI object. * * @static @@ -522,6 +560,29 @@ } } +function createWhiteTexture() +{ + const canvas = document.createElement('canvas'); + + canvas.width = 10; + canvas.height = 10; + + const context = canvas.getContext('2d'); + + context.fillStyle = 'white'; + context.fillRect(0, 0, 10, 10); + + return new Texture(new BaseTexture(canvas)); +} + +function removeAllHandlers(tex) +{ + tex.destroy = function _emptyDestroy() { /* empty */ }; + tex.on = function _emptyOn() { /* empty */ }; + tex.once = function _emptyOnce() { /* empty */ }; + tex.emit = function _emptyEmit() { /* empty */ }; +} + /** * An empty texture, used often to not have to create multiple empty textures. * Can not be destroyed. @@ -530,7 +591,14 @@ * @constant */ Texture.EMPTY = new Texture(new BaseTexture()); -Texture.EMPTY.destroy = function _emptyDestroy() { /* empty */ }; -Texture.EMPTY.on = function _emptyOn() { /* empty */ }; -Texture.EMPTY.once = function _emptyOnce() { /* empty */ }; -Texture.EMPTY.emit = function _emptyEmit() { /* empty */ }; +removeAllHandlers(Texture.EMPTY); + +/** + * A white texture of 10x10 size, used for graphics and other things + * Can not be destroyed. + * + * @static + * @constant + */ +Texture.WHITE = createWhiteTexture(); +removeAllHandlers(Texture.WHITE); diff --git a/src/core/ticker/Ticker.js b/src/core/ticker/Ticker.js index d98a1f0..17a1517 100644 --- a/src/core/ticker/Ticker.js +++ b/src/core/ticker/Ticker.js @@ -67,11 +67,12 @@ * is based, this value is neither capped nor scaled. * If the platform supports DOMHighResTimeStamp, * this value will have a precision of 1 µs. + * Defaults to target frame time * * @member {number} - * @default 1 / TARGET_FPMS + * @default 16.66 */ - this.elapsedMS = 1 / settings.TARGET_FPMS; // default to target frame time + this.elapsedMS = 1 / settings.TARGET_FPMS; /** * The last time {@link PIXI.ticker.Ticker#update} was invoked. diff --git a/src/core/utils/index.js b/src/core/utils/index.js index 6ff609f..be44750 100644 --- a/src/core/utils/index.js +++ b/src/core/utils/index.js @@ -2,6 +2,7 @@ import settings from '../settings'; import EventEmitter from 'eventemitter3'; import pluginTarget from './pluginTarget'; +import * as mixins from './mixin'; import * as isMobile from 'ismobilejs'; let nextUid = 0; @@ -33,6 +34,7 @@ * @type {mixin} */ pluginTarget, + mixins, }; /** @@ -93,7 +95,7 @@ */ export function rgb2hex(rgb) { - return (((rgb[0] * 255) << 16) + ((rgb[1] * 255) << 8) + (rgb[2] * 255)); + return (((rgb[0] * 255) << 16) + ((rgb[1] * 255) << 8) + (rgb[2] * 255 | 0)); } /** @@ -368,3 +370,43 @@ * @private */ export const ProgramCache = {}; + +/** + * Destroys all texture in the cache + * + * @memberof PIXI.utils + * @function destroyTextureCache + */ +export function destroyTextureCache() +{ + let key; + + for (key in TextureCache) + { + TextureCache[key].destroy(); + } + for (key in BaseTextureCache) + { + BaseTextureCache[key].destroy(); + } +} + +/** + * Removes all textures from cache, but does not destroy them + * + * @memberof PIXI.utils + * @function clearTextureCache + */ +export function clearTextureCache() +{ + let key; + + for (key in TextureCache) + { + delete TextureCache[key]; + } + for (key in BaseTextureCache) + { + delete BaseTextureCache[key]; + } +} diff --git a/src/core/utils/mixin.js b/src/core/utils/mixin.js new file mode 100644 index 0000000..114de2a --- /dev/null +++ b/src/core/utils/mixin.js @@ -0,0 +1,59 @@ +/** + * Mixes all enumerable properties and methods from a source object to a target object. + * + * @memberof PIXI.utils.mixins + * @function mixin + * @param {object} target The prototype or instance that properties and methods should be added to. + * @param {object} source The source of properties and methods to mix in. + */ +export function mixin(target, source) +{ + if (!target || !source) return; + // in ES8/ES2017, this would be really easy: + // Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); + + // get all the enumerable property keys + const keys = Object.keys(source); + + // loop through properties + for (let i = 0; i < keys.length; ++i) + { + const propertyName = keys[i]; + + // Set the property using the property descriptor - this works for accessors and normal value properties + Object.defineProperty(target, propertyName, Object.getOwnPropertyDescriptor(source, propertyName)); + } +} + +const mixins = []; + +/** + * Queues a mixin to be handled towards the end of the initialization of PIXI, so that deprecation + * can take effect. + * + * @memberof PIXI.utils.mixins + * @function delayMixin + * @private + * @param {object} target The prototype or instance that properties and methods should be added to. + * @param {object} source The source of properties and methods to mix in. + */ +export function delayMixin(target, source) +{ + mixins.push(target, source); +} + +/** + * Handles all mixins queued via delayMixin(). + * + * @memberof PIXI.utils.mixins + * @function performMixins + * @private + */ +export function performMixins() +{ + for (let i = 0; i < mixins.length; i += 2) + { + mixin(mixins[i], mixins[i + 1]); + } + mixins.length = 0; +} diff --git a/src/deprecation.js b/src/deprecation.js index 045a19f..b9c73b3 100644 --- a/src/deprecation.js +++ b/src/deprecation.js @@ -5,6 +5,7 @@ import * as filters from './filters'; import * as prepare from './prepare'; import * as loaders from './loaders'; +import * as interaction from './interaction'; // provide method to give a stack track for warnings // useful for tracking-down where deprecated methods/properties/classes @@ -534,7 +535,7 @@ { parent: 'GC_MODES', target: 'GC_MODE' }, { parent: 'WRAP_MODES', target: 'WRAP_MODE' }, { parent: 'SCALE_MODES', target: 'SCALE_MODE' }, - { parent: 'PRECISION', target: 'PRECISION' }, + { parent: 'PRECISION', target: 'PRECISION_FRAGMENT' }, ]; for (let i = 0; i < defaults.length; i++) @@ -558,6 +559,32 @@ }); } +Object.defineProperties(core.settings, { + + /** + * @static + * @name PRECISION + * @memberof PIXI.settings + * @see PIXI.PRECISION + * @deprecated since version 4.4.0 + */ + PRECISION: { + enumerable: true, + get() + { + warn('PIXI.settings.PRECISION has been deprecated, please use PIXI.settings.PRECISION_FRAGMENT'); + + return core.settings.PRECISION_FRAGMENT; + }, + set(value) + { + warn('PIXI.settings.PRECISION has been deprecated, please use PIXI.settings.PRECISION_FRAGMENT'); + + core.settings.PRECISION_FRAGMENT = value; + }, + }, +}); + Object.defineProperties(extras, { /** @@ -694,7 +721,7 @@ warn('determineFontProperties is now deprecated, please use the static calculateFontProperties method, ' + 'e.g : Text.calculateFontProperties(fontStyle);'); - return Text.calculateFontProperties(fontStyle); + return core.Text.calculateFontProperties(fontStyle); }; Object.defineProperties(core.TextStyle.prototype, { @@ -1003,3 +1030,69 @@ }, }, }); + +/** + * @name PIXI.interaction.interactiveTarget#defaultCursor + * @static + * @type {number} + * @see PIXI.interaction.interactiveTarget#cursor + * @deprecated since 4.3.0 + */ +Object.defineProperty(interaction.interactiveTarget, 'defaultCursor', { + set(value) + { + warn('Property defaultCursor has been replaced with \'cursor\'. '); + this.cursor = value; + }, + get() + { + warn('Property defaultCursor has been replaced with \'cursor\'. '); + + return this.cursor; + }, + enumerable: true, +}); + +/** + * @name PIXI.interaction.InteractionManager#defaultCursorStyle + * @static + * @type {string} + * @see PIXI.interaction.InteractionManager#cursorStyles + * @deprecated since 4.3.0 + */ +Object.defineProperty(interaction.InteractionManager, 'defaultCursorStyle', { + set(value) + { + warn('Property defaultCursorStyle has been replaced with \'cursorStyles.default\'. '); + this.cursorStyles.default = value; + }, + get() + { + warn('Property defaultCursorStyle has been replaced with \'cursorStyles.default\'. '); + + return this.cursorStyles.default; + }, +}); + +/** + * @name PIXI.interaction.InteractionManager#currentCursorStyle + * @static + * @type {string} + * @see PIXI.interaction.InteractionManager#cursorStyles + * @deprecated since 4.3.0 + */ +Object.defineProperty(interaction.InteractionManager, 'currentCursorStyle', { + set(value) + { + warn('Property currentCursorStyle has been removed.' + + 'See the currentCursorMode property, which works differently.'); + this.currentCursorMode = value; + }, + get() + { + warn('Property currentCursorStyle has been removed.' + + 'See the currentCursorMode property, which works differently.'); + + return this.currentCursorMode; + }, +}); diff --git a/src/extras/BitmapText.js b/src/extras/BitmapText.js index fb6c343..0a0e150 100644 --- a/src/extras/BitmapText.js +++ b/src/extras/BitmapText.js @@ -455,6 +455,73 @@ return this._textHeight; } + + /** + * Register a bitmap font with data and a texture. + * + * @static + * @param {XMLDocument} xml - The XML document data. + * @param {PIXI.Texture} texture - Texture with all symbols. + * @return {Object} Result font object with font, size, lineHeight and char fields. + */ + static registerFont(xml, texture) + { + const data = {}; + const info = xml.getElementsByTagName('info')[0]; + const common = xml.getElementsByTagName('common')[0]; + + data.font = info.getAttribute('face'); + data.size = parseInt(info.getAttribute('size'), 10); + data.lineHeight = parseInt(common.getAttribute('lineHeight'), 10); + data.chars = {}; + + // parse letters + const letters = xml.getElementsByTagName('char'); + + for (let i = 0; i < letters.length; i++) + { + const letter = letters[i]; + const charCode = parseInt(letter.getAttribute('id'), 10); + + const textureRect = new core.Rectangle( + parseInt(letter.getAttribute('x'), 10) + texture.frame.x, + parseInt(letter.getAttribute('y'), 10) + texture.frame.y, + parseInt(letter.getAttribute('width'), 10), + parseInt(letter.getAttribute('height'), 10) + ); + + data.chars[charCode] = { + xOffset: parseInt(letter.getAttribute('xoffset'), 10), + yOffset: parseInt(letter.getAttribute('yoffset'), 10), + xAdvance: parseInt(letter.getAttribute('xadvance'), 10), + kerning: {}, + texture: new core.Texture(texture.baseTexture, textureRect), + + }; + } + + // parse kernings + const kernings = xml.getElementsByTagName('kerning'); + + for (let i = 0; i < kernings.length; i++) + { + const kerning = kernings[i]; + const first = parseInt(kerning.getAttribute('first'), 10); + const second = parseInt(kerning.getAttribute('second'), 10); + const amount = parseInt(kerning.getAttribute('amount'), 10); + + if (data.chars[second]) + { + data.chars[second].kerning[first] = amount; + } + } + + // I'm leaving this as a temporary fix so we can test the bitmap fonts in v3 + // but it's very likely to change + BitmapText.fonts[data.font] = data; + + return data; + } } BitmapText.fonts = {}; diff --git a/src/index.js b/src/index.js index 59356cf..c553427 100644 --- a/src/index.js +++ b/src/index.js @@ -16,6 +16,10 @@ import * as particles from './particles'; import * as prepare from './prepare'; +// handle mixins now, after all code has been added, including deprecation +import { utils } from './core'; +utils.mixins.performMixins(); + export { accessibility, extract, diff --git a/src/interaction/InteractionManager.js b/src/interaction/InteractionManager.js index 7d51134..8baf13a 100644 --- a/src/interaction/InteractionManager.js +++ b/src/interaction/InteractionManager.js @@ -1,18 +1,20 @@ 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'; -// Mix interactiveTarget into core.DisplayObject.prototype -Object.assign( +// Mix interactiveTarget into core.DisplayObject.prototype, after deprecation has been handled +core.utils.mixins.delayMixin( core.DisplayObject.prototype, 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 +70,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 +101,6 @@ this.eventData = new InteractionEvent(); /** - * Tiny little interactiveData pool ! - * - * @member {PIXI.interaction.InteractionData[]} - */ - this.interactiveDataPool = []; - - /** * The DOM element to bind to. * * @private @@ -153,66 +155,12 @@ */ this.supportsPointerEvents = !!window.PointerEvent; - /** - * Are touch events being 'normalized' and converted into pointer events if pointer events are not supported - * For example, on a touch screen mobile device, a touchstart would also be emitted as a pointerdown - * - * @private - * @readonly - * @member {boolean} - */ - this.normalizeTouchEvents = !this.supportsPointerEvents && this.supportsTouchEvents; - - /** - * Are mouse events being 'normalized' and converted into pointer events if pointer events are not supported - * For example, on a desktop pc, a mousedown would also be emitted as a pointerdown - * - * @private - * @readonly - * @member {boolean} - */ - this.normalizeMouseEvents = !this.supportsPointerEvents && !MobileDevice.any; - // this will make it so that you don't have to call bind all the time /** * @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 +168,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,41 +199,32 @@ this.onPointerOver = this.onPointerOver.bind(this); /** - * @private - * @member {Function} + * Dictionary of how different cursor modes are handled. Strings are handled as CSS cursor + * values, objects are handled as dictionaries of CSS values for interactionDOMElement, + * and functions are called instead of changing the CSS. + * Default CSS cursor values are provided for 'default' and 'pointer' modes. + * @member {Object.)>} */ - this.onTouchStart = this.onTouchStart.bind(this); - this.processTouchStart = this.processTouchStart.bind(this); + this.cursorStyles = { + default: 'inherit', + pointer: 'pointer', + }; /** - * @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. - * - * @member {string} - * @default 'inherit' - */ - this.defaultCursorStyle = 'inherit'; - - /** - * The css style of the cursor that is being used. + * The mode of the cursor that is being used. + * The value of this is a key from the cursorStyles dictionary. * * @member {string} */ - this.currentCursorStyle = 'inherit'; + this.currentCursorMode = null; + + /** + * Internal cached let. + * + * @private + * @member {string} + */ + this.cursor = null; /** * Internal cached let. @@ -303,6 +249,7 @@ * object. * * @event mousedown + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -311,6 +258,7 @@ * on the display object. * * @event rightdown + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -319,6 +267,7 @@ * object. * * @event mouseup + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -327,6 +276,7 @@ * over the display object. * * @event rightup + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -335,6 +285,7 @@ * the display object. * * @event click + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -343,6 +294,7 @@ * and released on the display object. * * @event rightclick + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -352,6 +304,7 @@ * [mousedown]{@link PIXI.interaction.InteractionManager#event:mousedown}. * * @event mouseupoutside + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -361,6 +314,7 @@ * [rightdown]{@link PIXI.interaction.InteractionManager#event:rightdown}. * * @event rightupoutside + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -368,6 +322,7 @@ * Fired when a pointer device (usually a mouse) is moved while over the display object * * @event mousemove + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -375,6 +330,7 @@ * Fired when a pointer device (usually a mouse) is moved onto the display object * * @event mouseover + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -382,6 +338,7 @@ * Fired when a pointer device (usually a mouse) is moved off the display object * * @event mouseout + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -389,6 +346,7 @@ * Fired when a pointer device button is pressed on the display object. * * @event pointerdown + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -396,6 +354,14 @@ * Fired when a pointer device button is released over the display object. * * @event pointerup + * @type {PIXI.interaction.InteractionData} + * @memberof PIXI.interaction.InteractionManager# + */ + + /** + * Fired when the operating system cancels a pointer event + * + * @event pointercancel * @memberof PIXI.interaction.InteractionManager# */ @@ -403,6 +369,7 @@ * Fired when a pointer device button is pressed and released on the display object. * * @event pointertap + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -411,6 +378,7 @@ * registered a [pointerdown]{@link PIXI.interaction.InteractionManager#event:pointerdown}. * * @event pointerupoutside + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -418,6 +386,7 @@ * Fired when a pointer device is moved while over the display object * * @event pointermove + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -425,6 +394,7 @@ * Fired when a pointer device is moved onto the display object * * @event pointerover + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -432,6 +402,7 @@ * Fired when a pointer device is moved off the display object * * @event pointerout + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -439,6 +410,7 @@ * Fired when a touch point is placed on the display object. * * @event touchstart + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -446,6 +418,14 @@ * Fired when a touch point is removed from the display object. * * @event touchend + * @type {PIXI.interaction.InteractionData} + * @memberof PIXI.interaction.InteractionManager# + */ + + /** + * Fired when the operating system cancels a touch + * + * @event touchcancel * @memberof PIXI.interaction.InteractionManager# */ @@ -453,6 +433,7 @@ * Fired when a touch point is placed and removed from the display object. * * @event tap + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -461,6 +442,7 @@ * registered a [touchstart]{@link PIXI.interaction.InteractionManager#event:touchstart}. * * @event touchendoutside + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ @@ -468,6 +450,7 @@ * Fired when a touch point is moved along the display object. * * @event touchmove + * @type {PIXI.interaction.InteractionData} * @memberof PIXI.interaction.InteractionManager# */ } @@ -524,45 +507,30 @@ { window.document.addEventListener('pointermove', this.onPointerMove, true); this.interactionDOMElement.addEventListener('pointerdown', this.onPointerDown, true); - this.interactionDOMElement.addEventListener('pointerout', this.onPointerOut, true); + // pointerout is fired in addition to pointerup (for touch events) and pointercancel + // we already handle those, so for the purposes of what we do in onPointerOut, we only + // care about the pointerleave event + this.interactionDOMElement.addEventListener('pointerleave', 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; @@ -596,45 +564,26 @@ { window.document.removeEventListener('pointermove', this.onPointerMove, true); this.interactionDOMElement.removeEventListener('pointerdown', this.onPointerDown, true); - this.interactionDOMElement.removeEventListener('pointerout', this.onPointerOut, true); + this.interactionDOMElement.removeEventListener('pointerleave', 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) + 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.onPointerDown, true); + this.interactionDOMElement.removeEventListener('touchcancel', this.onPointerCancel, 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); - - 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 = null; @@ -664,7 +613,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; @@ -672,25 +621,80 @@ return; } - this.cursor = this.defaultCursorStyle; + this.cursor = null; // 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(); - - this.processInteractive(this.mouse.global, this.renderer._lastObjectRendered, this.processMouseOverOut, true); - - if (this.currentCursorStyle !== this.cursor) + for (const k in this.activeInteractionData) { - this.currentCursorStyle = this.cursor; - this.interactionDOMElement.style.cursor = this.cursor; + // eslint-disable-next-line no-prototype-builtins + if (this.activeInteractionData.hasOwnProperty(k)) + { + const interactionData = this.activeInteractionData[k]; + + if (interactionData.originalEvent && interactionData.pointerType !== 'touch') + { + const interactionEvent = this.configureInteractionEventForDOMEvent( + this.eventData, + interactionData.originalEvent, + interactionData + ); + + this.processInteractive( + interactionEvent, + this.renderer._lastObjectRendered, + this.processPointerOverOut, + true + ); + } + } } + this.setCursorMode(this.cursor); + // TODO } /** + * Sets the current cursor mode, handling any callbacks or CSS style changes. + * + * @param {string} mode - cursor mode, a key from the cursorStyles dictionary + */ + setCursorMode(mode) + { + mode = mode || 'default'; + // if the mode didn't actually change, bail early + if (this.currentCursorMode === mode) + { + return; + } + this.currentCursorMode = mode; + const style = this.cursorStyles[mode]; + + // only do things if there is a cursor style for it + if (style) + { + switch (typeof style) + { + case 'string': + // string styles are handled as cursor CSS + this.interactionDOMElement.style.cursor = style; + break; + case 'function': + // functions are just called, and passed the cursor mode + style(mode); + break; + case 'object': + // if it is an object, assume that it is a dictionary of CSS styles, + // apply it to the interactionDOMElement + Object.assign(this.interactionDOMElement.style, style); + break; + } + } + } + + /** * Dispatches an event on the display object that was interacted with * * @param {PIXI.Container|PIXI.Sprite|PIXI.extras.TilingSprite} displayObject - the display object in question @@ -748,22 +752,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 @@ -787,9 +795,8 @@ { interactiveParent = false; } - - // it has a mask! Then lets hit test that before continuing.. - if (hitTest && displayObject._mask) + // it has a mask! Then lets hit test that before continuing + else if (hitTest && displayObject._mask) { if (!displayObject._mask.containsPoint(point)) { @@ -797,14 +804,7 @@ } } - // it has a filterArea! Same as mask but easier, its a rectangle - if (hitTest && displayObject.filterArea) - { - if (!displayObject.filterArea.contains(point.x, point.y)) - { - hitTest = false; - } - } + let keepHitTestingAfterChildren = hitTest; // ** FREE TIP **! If an object is not interactive or has no buttons in it // (such as a game scene!) set interactiveChildren to false for that displayObject. @@ -818,7 +818,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 @@ -838,15 +838,20 @@ // This means we no longer need to hit test anything else. We still need to run // through all objects, but we don't need to perform any hit tests. - // { - hitTest = false; - // } + keepHitTestingAfterChildren = false; + + if (child.interactive) + { + hitTest = false; + } // we can break now as we have hit an object. } } } + hitTest = keepHitTestingAfterChildren; + // no point running this if the item is not interactive or does not have an interactive parent. if (interactive) { @@ -868,14 +873,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 +886,175 @@ } /** - * 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].isNormalized) { - 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); + if (!displayObject.trackedPointers[id]) + { + displayObject.trackedPointers[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.trackedPointers[id].rightDown = true; + } + else + { + displayObject.trackedPointers[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.trackedPointers[id] !== undefined) + { + delete displayObject.trackedPointers[id]; + this.dispatchEvent(displayObject, 'pointercancel', interactionEvent); + + if (e.type === 'touchcancel' || e.pointerType === 'touch') + { + this.dispatchEvent(displayObject, 'touchcancel', interactionEvent); + } } } @@ -1159,42 +1066,94 @@ */ 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) { - if (hit) - { - this.dispatchEvent(displayObject, 'pointerup', this.eventData); + const e = interactionEvent.data.originalEvent; - if (displayObject._pointerDown) + const id = interactionEvent.data.identifier; + + const trackingData = displayObject.trackedPointers[id]; + + const isTouch = (e.type === 'touchend' || e.pointerType === 'touch'); + + const isMouse = (e.type.indexOf('mouse') === 0 || e.pointerType === 'mouse'); + + // 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) { - displayObject._pointerDown = false; - this.dispatchEvent(displayObject, 'pointertap', this.eventData); + 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); + } + // update the down state of the tracking data + if (trackingData) + { + if (isRightButton) + { + trackingData.rightDown = hit; + } + else + { + trackingData.leftDown = hit; + } } } - else if (displayObject._pointerDown) + + // Pointers and Touches, and Mouse + if (hit) { - displayObject._pointerDown = false; - this.dispatchEvent(displayObject, 'pointerupoutside', this.eventData); + this.dispatchEvent(displayObject, 'pointerup', interactionEvent); + if (isTouch) this.dispatchEvent(displayObject, 'touchend', interactionEvent); + + if (trackingData) + { + this.dispatchEvent(displayObject, 'pointertap', interactionEvent); + if (isTouch) + { + this.dispatchEvent(displayObject, 'tap', interactionEvent); + // touches are no longer over (if they ever were) when we get the touchend + // so we should ensure that we don't keep pretending that they are + trackingData.over = false; + } + } + } + else if (trackingData) + { + this.dispatchEvent(displayObject, 'pointerupoutside', interactionEvent); + if (isTouch) this.dispatchEvent(displayObject, 'touchendoutside', interactionEvent); + } + // Only remove the tracking data if there is no over/down state still associated with it + if (trackingData && trackingData.none) + { + delete displayObject.trackedPointers[id]; } } @@ -1202,39 +1161,78 @@ * 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 = null; + } - 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') + { + this.setCursorMode(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 (isMouse) { - 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 +1240,94 @@ * 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.setCursorMode(null); + } - 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'); + + let trackingData = displayObject.trackedPointers[id]; + + // if we just moused over the display object, then we need to track that state + if (hit && !trackingData) + { + trackingData = displayObject.trackedPointers[id] = new InteractionTrackingData(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); + } + } + + // only change the cursor if it has not already been changed (by something deeper in the + // display tree) + if (isMouse && this.cursor === null) + { + this.cursor = displayObject.cursor; } } - 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); + } + // if there is no mouse down information for the pointer, then it is safe to delete + if (trackingData.none) + { + delete displayObject.trackedPointers[id]; + } } } @@ -1287,253 +1335,154 @@ * 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 && pointerEvent.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; + + // mark the touch as normalized, just so that we know we did it + touch.isNormalized = true; + + normalizedEvents.push(touch); + } } - else if (this.normalizeMouseEvents) + // apparently PointerEvent subclasses MouseEvent, so yay + else if (event instanceof MouseEvent && (!this.supportsPointerEvents || !(event instanceof window.PointerEvent))) { if (typeof event.isPrimary === 'undefined') event.isPrimary = true; if (typeof event.width === 'undefined') event.width = 1; @@ -1541,10 +1490,21 @@ 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; + + // mark the mouse event as normalized, just so that we know we did it + event.isNormalized = true; + + normalizedEvents.push(event); } + else + { + normalizedEvents.push(event); + } + + return normalizedEvents; } /** @@ -1563,30 +1523,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 +1542,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..6a1f69e --- /dev/null +++ b/src/interaction/InteractionTrackingData.js @@ -0,0 +1,147 @@ +/** + * 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 inactive (not over or down)? + * + * @member {number} + * @memberof PIXI.interaction.InteractionTrackingData# + */ + get none() + { + return this._flags === this.constructor.FLAGS.NONE; + } + + /** + * 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..d252fba 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 * ); */ @@ -38,68 +38,56 @@ hitArea: null, /** - * If enabled, the mouse cursor will change when hovered over the displayObject if it is interactive + * If enabled, the mouse cursor use the pointer behavior when hovered over the displayObject if it is interactive + * Setting this changes the 'cursor' property to `'pointer'`. * - * @inner {boolean} + * @member {boolean} + * @memberof PIXI.interaction.interactiveTarget# */ - buttonMode: false, + get buttonMode() + { + return this.cursor === 'pointer'; + }, + set buttonMode(value) + { + if (value) + { + this.cursor = 'pointer'; + } + else if (this.cursor === 'pointer') + { + this.cursor = null; + } + }, /** - * If buttonMode is enabled, this defines what CSS cursor property is used when the mouse cursor - * is hovered over the displayObject + * This defines what cursor mode is used when the mouse cursor + * is hovered over the displayObject. * * @see https://developer.mozilla.org/en/docs/Web/CSS/cursor * * @inner {string} */ - defaultCursor: 'pointer', - - // some internal checks.. - /** - * Internal check to detect if the mouse cursor is hovered over the displayObject - * - * @inner {boolean} - * @private - */ - _over: false, + cursor: null, /** - * Internal check to detect if the left mouse button is pressed on the displayObject + * Internal set of all active pointers, by identifier * - * @inner {boolean} + * @member {Map} + * @memberof PIXI.interaction.interactiveTarget# * @private */ - _isLeftDown: false, + get trackedPointers() + { + if (this._trackedPointers === undefined) this._trackedPointers = {}; + + return this._trackedPointers; + }, /** - * Internal check to detect if the right mouse button is pressed on the displayObject + * Map of all tracked pointers, by identifier. Use trackedPointers to access. * - * @inner {boolean} - * @private + * @private {Map} */ - _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, + _trackedPointers: undefined, }; diff --git a/src/loaders/bitmapFontParser.js b/src/loaders/bitmapFontParser.js index cc2618c..f4cf996 100644 --- a/src/loaders/bitmapFontParser.js +++ b/src/loaders/bitmapFontParser.js @@ -1,63 +1,19 @@ import * as path from 'path'; -import { Rectangle, Texture, utils } from '../core'; +import { utils } from '../core'; import { Resource } from 'resource-loader'; import { BitmapText } from '../extras'; +/** + * Register a BitmapText font from loader resource. + * + * @function parseBitmapFontData + * @memberof PIXI.loaders + * @param {PIXI.loaders.Resource} resource - Loader resource. + * @param {PIXI.Texture} texture - Reference to texture. + */ export function parse(resource, texture) { - const data = {}; - const info = resource.data.getElementsByTagName('info')[0]; - const common = resource.data.getElementsByTagName('common')[0]; - - data.font = info.getAttribute('face'); - data.size = parseInt(info.getAttribute('size'), 10); - data.lineHeight = parseInt(common.getAttribute('lineHeight'), 10); - data.chars = {}; - - // parse letters - const letters = resource.data.getElementsByTagName('char'); - - for (let i = 0; i < letters.length; i++) - { - const charCode = parseInt(letters[i].getAttribute('id'), 10); - - const textureRect = new Rectangle( - parseInt(letters[i].getAttribute('x'), 10) + texture.frame.x, - parseInt(letters[i].getAttribute('y'), 10) + texture.frame.y, - parseInt(letters[i].getAttribute('width'), 10), - parseInt(letters[i].getAttribute('height'), 10) - ); - - data.chars[charCode] = { - xOffset: parseInt(letters[i].getAttribute('xoffset'), 10), - yOffset: parseInt(letters[i].getAttribute('yoffset'), 10), - xAdvance: parseInt(letters[i].getAttribute('xadvance'), 10), - kerning: {}, - texture: new Texture(texture.baseTexture, textureRect), - - }; - } - - // parse kernings - const kernings = resource.data.getElementsByTagName('kerning'); - - for (let i = 0; i < kernings.length; i++) - { - const first = parseInt(kernings[i].getAttribute('first'), 10); - const second = parseInt(kernings[i].getAttribute('second'), 10); - const amount = parseInt(kernings[i].getAttribute('amount'), 10); - - if (data.chars[second]) - { - data.chars[second].kerning[first] = amount; - } - } - - resource.bitmapFont = data; - - // I'm leaving this as a temporary fix so we can test the bitmap fonts in v3 - // but it's very likely to change - BitmapText.fonts[data.font] = data; + resource.bitmapFont = BitmapText.registerFont(resource.data, texture); } export default function () diff --git a/src/loaders/index.js b/src/loaders/index.js index 8942436..5189793 100644 --- a/src/loaders/index.js +++ b/src/loaders/index.js @@ -5,4 +5,11 @@ export { default as bitmapFontParser, parse as parseBitmapFontData } from './bitmapFontParser'; export { default as spritesheetParser } from './spritesheetParser'; export { default as textureParser } from './textureParser'; + +/** + * Reference to **resource-loader**'s Resource class. + * See https://github.com/englercj/resource-loader + * @class Resource + * @memberof PIXI.loaders + */ export { Resource } from 'resource-loader'; diff --git a/src/loaders/loader.js b/src/loaders/loader.js index 0a670cb..767a411 100644 --- a/src/loaders/loader.js +++ b/src/loaders/loader.js @@ -7,20 +7,47 @@ /** * - * The new loader, extends Resource Loader by Chad Engler : https://github.com/englercj/resource-loader + * The new loader, extends Resource Loader by Chad Engler: https://github.com/englercj/resource-loader * * ```js - * let loader = PIXI.loader; // pixi exposes a premade instance for you to use. + * const loader = PIXI.loader; // pixi exposes a premade instance for you to use. * //or - * let loader = new PIXI.loaders.Loader(); // you can also create your own if you want + * const loader = new PIXI.loaders.Loader(); // you can also create your own if you want * - * loader.add('bunny', 'data/bunny.png'); - * loader.add('spaceship', 'assets/spritesheet.json'); + * const sprites = {}; + * + * // Chainable `add` to enqueue a resource + * loader.add('bunny', 'data/bunny.png') + * .add('spaceship', 'assets/spritesheet.json'); * loader.add('scoreFont', 'assets/score.fnt'); * - * loader.once('complete',onAssetsLoaded); + * // Chainable `pre` to add a middleware that runs for each resource, *before* loading that resource. + * // This is useful to implement custom caching modules (using filesystem, indexeddb, memory, etc). + * loader.pre(cachingMiddleware); * - * loader.load(); + * // Chainable `use` to add a middleware that runs for each resource, *after* loading that resource. + * // This is useful to implement custom parsing modules (like spritesheet parsers, spine parser, etc). + * loader.use(parsingMiddleware); + * + * // The `load` method loads the queue of resources, and calls the passed in callback called once all + * // resources have loaded. + * loader.load((loader, resources) => { + * // resources is an object where the key is the name of the resource loaded and the value is the resource object. + * // They have a couple default properties: + * // - `url`: The URL that the resource was loaded from + * // - `error`: The error that happened when trying to load (if any) + * // - `data`: The raw data that was loaded + * // also may contain other properties based on the middleware that runs. + * sprites.bunny = new PIXI.TilingSprite(resources.bunny.texture); + * sprites.spaceship = new PIXI.TilingSprite(resources.spaceship.texture); + * sprites.scoreFont = new PIXI.TilingSprite(resources.scoreFont.texture); + * }); + * + * // throughout the process multiple signals can be dispatched. + * loader.onProgress.add(() => {}); // called once per loaded/errored file + * loader.onError.add(() => {}); // called once per errored file + * loader.onLoad.add(() => {}); // called once per loaded file + * loader.onComplete.add(() => {}); // called once when the queued resources all load. * ``` * * @see https://github.com/englercj/resource-loader diff --git a/src/loaders/spritesheetParser.js b/src/loaders/spritesheetParser.js index 7570332..e57dfb2 100644 --- a/src/loaders/spritesheetParser.js +++ b/src/loaders/spritesheetParser.js @@ -1,8 +1,6 @@ import { Resource } from 'resource-loader'; import path from 'path'; -import * as core from '../core'; - -const BATCH_SIZE = 1000; +import { Spritesheet } from '../core'; export default function () { @@ -43,134 +41,18 @@ // load the image for this sheet this.add(imageResourceName, resourcePath, loadOptions, function onImageLoad(res) { - resource.textures = {}; + const spritesheet = new Spritesheet( + res.texture.baseTexture, + resource.data, + resource.url + ); - const frames = resource.data.frames; - const frameKeys = Object.keys(frames); - const baseTexture = res.texture.baseTexture; - const scale = resource.data.meta.scale; - - // Use a defaultValue of `null` to check if a url-based resolution is set - let resolution = core.utils.getResolutionOfUrl(resource.url, null); - - // No resolution found via URL - if (resolution === null) + spritesheet.parse(() => { - // Use the scale value or default to 1 - resolution = scale !== undefined ? scale : 1; - } - - // For non-1 resolutions, update baseTexture - if (resolution !== 1) - { - baseTexture.resolution = resolution; - baseTexture.update(); - } - - let batchIndex = 0; - - function processFrames(initialFrameIndex, maxFrames) - { - let frameIndex = initialFrameIndex; - - while (frameIndex - initialFrameIndex < maxFrames && frameIndex < frameKeys.length) - { - const i = frameKeys[frameIndex]; - const rect = frames[i].frame; - - if (rect) - { - let frame = null; - let trim = null; - const orig = new core.Rectangle( - 0, - 0, - frames[i].sourceSize.w / resolution, - frames[i].sourceSize.h / resolution - ); - - if (frames[i].rotated) - { - frame = new core.Rectangle( - rect.x / resolution, - rect.y / resolution, - rect.h / resolution, - rect.w / resolution - ); - } - else - { - frame = new core.Rectangle( - rect.x / resolution, - rect.y / resolution, - rect.w / resolution, - rect.h / resolution - ); - } - - // Check to see if the sprite is trimmed - if (frames[i].trimmed) - { - trim = new core.Rectangle( - frames[i].spriteSourceSize.x / resolution, - frames[i].spriteSourceSize.y / resolution, - rect.w / resolution, - rect.h / resolution - ); - } - - resource.textures[i] = new core.Texture( - baseTexture, - frame, - orig, - trim, - frames[i].rotated ? 2 : 0 - ); - - // lets also add the frame to pixi's global cache for fromFrame and fromImage functions - core.utils.TextureCache[i] = resource.textures[i]; - } - - frameIndex++; - } - } - - function shouldProcessNextBatch() - { - return batchIndex * BATCH_SIZE < frameKeys.length; - } - - function processNextBatch(done) - { - processFrames(batchIndex * BATCH_SIZE, BATCH_SIZE); - batchIndex++; - setTimeout(done, 0); - } - - function iteration() - { - processNextBatch(() => - { - if (shouldProcessNextBatch()) - { - iteration(); - } - else - { - next(); - } - }); - } - - if (frameKeys.length <= BATCH_SIZE) - { - processFrames(0, BATCH_SIZE); + resource.spritesheet = spritesheet; + resource.textures = spritesheet.textures; next(); - } - else - { - iteration(); - } + }); }); }; } diff --git a/src/loaders/textureParser.js b/src/loaders/textureParser.js index 5398a7f..480edc1 100644 --- a/src/loaders/textureParser.js +++ b/src/loaders/textureParser.js @@ -1,5 +1,5 @@ -import * as core from '../core'; import { Resource } from 'resource-loader'; +import Texture from '../core/textures/Texture'; export default function () { @@ -8,23 +8,12 @@ // create a new texture if the data is an Image object if (resource.data && resource.type === Resource.TYPE.IMAGE) { - const baseTexture = new core.BaseTexture(resource.data, null, core.utils.getResolutionOfUrl(resource.url)); - - baseTexture.imageUrl = resource.url; - resource.texture = new core.Texture(baseTexture); - - // lets also add the frame to pixi's global cache for fromFrame and fromImage fucntions - core.utils.BaseTextureCache[resource.name] = baseTexture; - core.utils.TextureCache[resource.name] = resource.texture; - - // also add references by url if they are different. - if (resource.name !== resource.url) - { - core.utils.BaseTextureCache[resource.url] = baseTexture; - core.utils.TextureCache[resource.url] = resource.texture; - } + resource.texture = Texture.fromLoader( + resource.data, + resource.url, + resource.name + ); } - next(); }; } diff --git a/test/core/Container.js b/test/core/Container.js index 8a10b40..d11be33 100644 --- a/test/core/Container.js +++ b/test/core/Container.js @@ -1,5 +1,33 @@ 'use strict'; +function testAddChild(fn) +{ + fn(function (container, obj) + { + container.addChild(obj); + }); + fn(function (container, obj) + { + container.addChildAt(obj); + }); +} + +function testRemoveChild(fn) +{ + fn(function (container, obj) + { + container.removeChild(obj); + }); + fn(function (container, obj) + { + container.removeChildAt(container.children.indexOf(obj)); + }); + fn(function (container, obj) + { + container.removeChildren(container.children.indexOf(obj), container.children.indexOf(obj) + 1); + }); +} + describe('PIXI.Container', function () { describe('parent', function () @@ -70,6 +98,49 @@ expect(spy).to.have.been.called; expect(spy).to.have.been.calledWith(0); }); + + it('should flag child transform and container bounds for recalculation', testAddChild(function (mockAddChild) + { + const container = new PIXI.Container(); + const child = new PIXI.Container(); + + container.getBounds(); + child.getBounds(); + + const boundsID = container._boundsID; + const childParentID = child.transform._parentID; + + mockAddChild(container, child); + + expect(boundsID).to.not.be.equals(container._boundsID); + expect(childParentID).to.not.be.equals(child.transform._parentID); + })); + + it('should recalculate added child correctly', testAddChild(function (mockAddChild) + { + const parent = new PIXI.Container(); + const container = new PIXI.Container(); + const graphics = new PIXI.Graphics(); + + parent.addChild(container); + + graphics.drawRect(0, 0, 10, 10); + container.position.set(100, 200); + container.updateTransform(); + + graphics.getBounds(); + // Oops, that can happen sometimes! + graphics.transform._parentID = container.transform._worldID + 1; + + mockAddChild(container, graphics); + + const bounds = graphics.getBounds(); + + expect(bounds.x).to.be.equal(100); + expect(bounds.y).to.be.equal(200); + expect(bounds.width).to.be.equal(10); + expect(bounds.height).to.be.equal(10); + })); }); describe('removeChildAt', function () @@ -202,6 +273,44 @@ expect(spy).to.have.been.called; expect(spy).to.have.been.calledWith(0); }); + + it('should flag transform for recalculation', testRemoveChild(function (mockRemoveChild) + { + const container = new PIXI.Container(); + const child = new PIXI.Container(); + + container.addChild(child); + container.getBounds(); + + const childParentID = child.transform._parentID; + const boundsID = container._boundsID; + + mockRemoveChild(container, child); + + expect(childParentID).to.not.be.equals(child.transform._parentID); + expect(boundsID).to.not.be.equals(container._boundsID); + })); + + it('should recalculate removed child correctly', testRemoveChild(function (mockRemoveChild) + { + const parent = new PIXI.Container(); + const container = new PIXI.Container(); + const graphics = new PIXI.Graphics(); + + parent.addChild(container); + + graphics.drawRect(0, 0, 10, 10); + container.position.set(100, 200); + container.addChild(graphics); + graphics.getBounds(); + + mockRemoveChild(container, graphics); + + const bounds = graphics.getBounds(); + + expect(bounds.x).to.be.equal(0); + expect(bounds.y).to.be.equal(0); + })); }); describe('getChildIndex', function () diff --git a/test/core/Graphics.js b/test/core/Graphics.js index 98c7584..11e47a0 100644 --- a/test/core/Graphics.js +++ b/test/core/Graphics.js @@ -1,5 +1,8 @@ 'use strict'; +const MockPointer = require('../interaction/MockPointer'); +const withGL = require('../withGL'); + describe('PIXI.Graphics', function () { describe('constructor', function () @@ -218,4 +221,153 @@ expect(spy).to.have.been.calledOnce; }); }); + + describe('mask', function () + { + it('should trigger interaction callback when no mask present', function () + { + const stage = new PIXI.Container(); + const pointer = new MockPointer(stage); + const graphics = new PIXI.Graphics(); + const mask = new PIXI.Graphics(); + const spy = sinon.spy(); + + graphics.interactive = true; + graphics.beginFill(0xFF0000); + graphics.drawRect(0, 0, 50, 50); + graphics.on('click', spy); + stage.addChild(graphics); + mask.beginFill(); + mask.drawRect(0, 0, 50, 50); + graphics.mask = mask; + + pointer.click(10, 10); + + expect(spy).to.have.been.calledOnce; + }); + it('should trigger interaction callback when mask uses beginFill', function () + { + const stage = new PIXI.Container(); + const pointer = new MockPointer(stage); + const graphics = new PIXI.Graphics(); + const mask = new PIXI.Graphics(); + const spy = sinon.spy(); + + graphics.interactive = true; + graphics.beginFill(0xFF0000); + graphics.drawRect(0, 0, 50, 50); + graphics.on('click', spy); + stage.addChild(graphics); + mask.beginFill(); + mask.drawRect(0, 0, 50, 50); + graphics.mask = mask; + + pointer.click(10, 10); + + expect(spy).to.have.been.calledOnce; + }); + + it('should not trigger interaction callback when mask doesn\'t use beginFill', function () + { + const stage = new PIXI.Container(); + const pointer = new MockPointer(stage); + const graphics = new PIXI.Graphics(); + const mask = new PIXI.Graphics(); + const spy = sinon.spy(); + + graphics.interactive = true; + graphics.beginFill(0xFF0000); + graphics.drawRect(0, 0, 50, 50); + graphics.on('click', spy); + stage.addChild(graphics); + mask.drawRect(0, 0, 50, 50); + graphics.mask = mask; + + pointer.click(10, 10); + + expect(spy).to.have.not.been.called; + }); + + it('should trigger interaction callback when mask doesn\'t use beginFill but hitArea is defined', function () + { + const stage = new PIXI.Container(); + const pointer = new MockPointer(stage); + const graphics = new PIXI.Graphics(); + const mask = new PIXI.Graphics(); + const spy = sinon.spy(); + + graphics.interactive = true; + graphics.beginFill(0xFF0000); + graphics.hitArea = new PIXI.Rectangle(0, 0, 50, 50); + graphics.drawRect(0, 0, 50, 50); + graphics.on('click', spy); + stage.addChild(graphics); + mask.drawRect(0, 0, 50, 50); + graphics.mask = mask; + + pointer.click(10, 10); + + expect(spy).to.have.been.calledOnce; + }); + + it('should trigger interaction callback when mask is a sprite', function () + { + const stage = new PIXI.Container(); + const pointer = new MockPointer(stage); + const graphics = new PIXI.Graphics(); + const mask = new PIXI.Graphics(); + const spy = sinon.spy(); + + graphics.interactive = true; + graphics.beginFill(0xFF0000); + graphics.drawRect(0, 0, 50, 50); + graphics.on('click', spy); + stage.addChild(graphics); + mask.drawRect(0, 0, 50, 50); + graphics.mask = new PIXI.Sprite(mask.generateCanvasTexture()); + + pointer.click(10, 10); + + expect(spy).to.have.been.calledOnce; + }); + + it('should calculate tint, alpha and blendMode of fastRect correctly', withGL(function () + { + const renderer = new PIXI.WebGLRenderer(200, 200, {}); + + try + { + const graphics = new PIXI.Graphics(); + + graphics.beginFill(0x102030, 0.6); + graphics.drawRect(2, 3, 100, 100); + graphics.endFill(); + graphics.tint = 0x101010; + graphics.blendMode = 2; + graphics.alpha = 0.3; + + renderer.render(graphics); + + expect(graphics.isFastRect()).to.be.true; + + const sprite = graphics._spriteRect; + + expect(sprite).to.not.be.equals(null); + expect(sprite.worldAlpha).to.equals(0.18); + expect(sprite.blendMode).to.equals(2); + expect(sprite.tint).to.equals(0x010203); + + const bounds = sprite.getBounds(); + + expect(bounds.x).to.equals(2); + expect(bounds.y).to.equals(3); + expect(bounds.width).to.equals(100); + expect(bounds.height).to.equals(100); + } + finally + { + renderer.destroy(); + } + })); + }); }); diff --git a/test/core/Spritesheet.js b/test/core/Spritesheet.js new file mode 100644 index 0000000..283adf5 --- /dev/null +++ b/test/core/Spritesheet.js @@ -0,0 +1,89 @@ +'use strict'; + +const path = require('path'); + +describe('PIXI.Spritesheet', function () +{ + before(function () + { + this.resources = path.join(__dirname, 'resources'); + this.validate = function (spritesheet, done) + { + spritesheet.parse(function (textures) + { + const id = 'goldmine_10_5.png'; + + expect(Object.keys(textures).length).to.equal(1); + expect(Object.keys(spritesheet.textures).length).to.equal(1); + expect(textures[id]).to.be.an.instanceof(PIXI.Texture); + expect(textures[id].width).to.equal(spritesheet.data.frames[id].frame.w / spritesheet.resolution); + expect(textures[id].height).to.equal(spritesheet.data.frames[id].frame.h / spritesheet.resolution); + spritesheet.destroy(true); + expect(spritesheet.textures).to.be.null; + expect(spritesheet.baseTexture).to.be.null; + done(); + }); + }; + }); + + it('should exist on PIXI', function () + { + expect(PIXI.Spritesheet).to.be.a.function; + expect(PIXI.Spritesheet.BATCH_SIZE).to.be.a.number; + }); + + it('should create an instance', function () + { + const baseTexture = new PIXI.BaseTexture(); + const data = { + frames: {}, + meta: {}, + }; + const spritesheet = new PIXI.Spritesheet(baseTexture, data); + + expect(spritesheet.data).to.equal(data); + expect(spritesheet.baseTexture).to.equal(baseTexture); + expect(spritesheet.resolution).to.equal(1); + + spritesheet.destroy(true); + }); + + it('should create instance with scale resolution', function (done) + { + const data = require(path.resolve(this.resources, 'building1.json')); // eslint-disable-line global-require + const image = new Image(); + + image.src = path.join(this.resources, data.meta.image); + image.onload = () => + { + const baseTexture = new PIXI.BaseTexture(image, null, 1); + const spritesheet = new PIXI.Spritesheet(baseTexture, data); + + expect(data).to.be.an.object; + expect(data.meta.image).to.equal('building1.png'); + expect(spritesheet.resolution).to.equal(0.5); + + this.validate(spritesheet, done); + }; + }); + + it('should create instance with filename resolution', function (done) + { + const uri = path.resolve(this.resources, 'building1@2x.json'); + const data = require(uri); // eslint-disable-line global-require + const image = new Image(); + + image.src = path.join(this.resources, data.meta.image); + image.onload = () => + { + const baseTexture = new PIXI.BaseTexture(image, null, 1); + const spritesheet = new PIXI.Spritesheet(baseTexture, data, uri); + + expect(data).to.be.an.object; + expect(data.meta.image).to.equal('building1@2x.png'); + expect(spritesheet.resolution).to.equal(2); + + this.validate(spritesheet, done); + }; + }); +}); diff --git a/test/core/Texture.js b/test/core/Texture.js new file mode 100644 index 0000000..8c4ef98 --- /dev/null +++ b/test/core/Texture.js @@ -0,0 +1,19 @@ +'use strict'; + +describe('PIXI.Texture', function () +{ + it('should register Texture from Loader', function () + { + const URL = 'foo.png'; + const NAME = 'bar'; + const image = new Image(); + + const texture = PIXI.Texture.fromLoader(image, URL, NAME); + + expect(texture.baseTexture.imageUrl).to.equal('foo.png'); + expect(PIXI.utils.TextureCache[NAME]).to.equal(texture); + expect(PIXI.utils.BaseTextureCache[NAME]).to.equal(texture.baseTexture); + expect(PIXI.utils.TextureCache[URL]).to.equal(texture); + expect(PIXI.utils.BaseTextureCache[URL]).to.equal(texture.baseTexture); + }); +}); diff --git a/test/core/WebGLRenderer.js b/test/core/WebGLRenderer.js index 9bc3bc2..3d6cf98 100644 --- a/test/core/WebGLRenderer.js +++ b/test/core/WebGLRenderer.js @@ -1,21 +1,21 @@ 'use strict'; -function isWebGLSupported(fn) -{ - return PIXI.utils.isWebGLSupported() ? fn : function () {}; // eslint-disable-line no-empty-function -} +const withGL = require('../withGL'); -describe('PIXI.WebGLRenderer', isWebGLSupported(function () +describe('PIXI.WebGLRenderer', function () { - it('setting option legacy should disable VAOs and set minimum SPRITE_MAX_TEXTURES to 1', function () + it('setting option legacy should disable VAOs and SPRITE_MAX_TEXTURES', withGL(function () { const renderer = new PIXI.WebGLRenderer(1, 1, { legacy: true }); - - expect(PIXI.glCore.VertexArrayObject.FORCE_NATIVE).to.equal(true); - expect(renderer.plugins.sprite.MAX_TEXTURES).to.equal(1); - - renderer.destroy(); - }); -})); - + try + { + expect(PIXI.glCore.VertexArrayObject.FORCE_NATIVE).to.equal(true); + expect(renderer.plugins.sprite.MAX_TEXTURES).to.equal(1); + } + finally + { + renderer.destroy(); + } + })); +}); diff --git a/test/core/filters.js b/test/core/filters.js new file mode 100644 index 0000000..df459f6 --- /dev/null +++ b/test/core/filters.js @@ -0,0 +1,21 @@ +'use strict'; + +describe('PIXI.filters', function () +{ + it('should correctly form uniformData', function () + { + const sprite = new PIXI.Sprite(PIXI.Texture.EMPTY); + const displ = new PIXI.filters.DisplacementFilter(sprite); + + expect(!!displ.uniformData.scale).to.be.true; + expect(!!displ.uniformData.filterMatrix).to.be.true; + expect(!!displ.uniformData.mapSampler).to.be.true; + // it does have filterClamp, but it is handled by FilterManager + expect(!!displ.uniformData.filterClamp).to.be.false; + + const fxaa = new PIXI.filters.FXAAFilter(); + + // it does have filterArea, but it is handled by FilterManager + expect(!!fxaa.uniformData.filterArea).to.be.false; + }); +}); diff --git a/test/core/index.js b/test/core/index.js index 63a8f21..8530131 100755 --- a/test/core/index.js +++ b/test/core/index.js @@ -7,6 +7,7 @@ require('./DisplayObject'); require('./getLocalBounds'); require('./Sprite'); +require('./Spritesheet'); require('./TilingSprite'); require('./TextStyle'); require('./Text'); @@ -25,3 +26,5 @@ require('./SpriteRenderer'); require('./WebGLRenderer'); require('./Ellipse'); +require('./Texture'); +require('./filters'); diff --git a/test/core/resources/building1.json b/test/core/resources/building1.json new file mode 100755 index 0000000..03fa5c1 --- /dev/null +++ b/test/core/resources/building1.json @@ -0,0 +1,21 @@ +{"frames": { + +"goldmine_10_5.png": +{ + "frame": {"x":1,"y":1,"w":95,"h":115}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":95,"h":115}, + "sourceSize": {"w":95,"h":115}, + "pivot": {"x":0.5,"y":0.5} +}}, +"meta": { + "app": "http://www.codeandweb.com/texturepacker", + "version": "1.0", + "image": "building1.png", + "format": "RGBA8888", + "size": {"w":128,"h":128}, + "scale": "0.5", + "smartupdate": "$TexturePacker:SmartUpdate:d7a5e54c8f8a3fecd508baf190c44807:99a0e3a4dc0f441e4aad77614191ab38:6046b8eb706ddefaa771c33ceb7cb6d5$" +} +} diff --git a/test/core/resources/building1.png b/test/core/resources/building1.png new file mode 100755 index 0000000..7e1e114 --- /dev/null +++ b/test/core/resources/building1.png Binary files differ diff --git a/test/core/resources/building1@2x.json b/test/core/resources/building1@2x.json new file mode 100755 index 0000000..24e25ff --- /dev/null +++ b/test/core/resources/building1@2x.json @@ -0,0 +1,21 @@ +{"frames": { + +"goldmine_10_5.png": +{ + "frame": {"x":1,"y":1,"w":190,"h":229}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":190,"h":229}, + "sourceSize": {"w":190,"h":229}, + "pivot": {"x":0.5,"y":0.5} +}}, +"meta": { + "app": "http://www.codeandweb.com/texturepacker", + "version": "1.0", + "image": "building1@2x.png", + "format": "RGBA8888", + "size": {"w":256,"h":256}, + "scale": "1", + "smartupdate": "$TexturePacker:SmartUpdate:d7a5e54c8f8a3fecd508baf190c44807:99a0e3a4dc0f441e4aad77614191ab38:6046b8eb706ddefaa771c33ceb7cb6d5$" +} +} diff --git a/test/core/resources/building1@2x.png b/test/core/resources/building1@2x.png new file mode 100755 index 0000000..d5ecd04 --- /dev/null +++ b/test/core/resources/building1@2x.png Binary files differ diff --git a/test/core/util.js b/test/core/util.js index dce6003..239aaeb 100755 --- a/test/core/util.js +++ b/test/core/util.js @@ -47,6 +47,11 @@ .to.be.a('function'); }); + it('should calculate correctly', function () + { + expect(PIXI.utils.rgb2hex([0.3, 0.2, 0.1])).to.equals(0x4c3319); + }); + // it('should properly convert rgb array to hex color string'); }); diff --git a/test/interaction/InteractionManager.js b/test/interaction/InteractionManager.js index 2cf6d9d..0e7e707 100644 --- a/test/interaction/InteractionManager.js +++ b/test/interaction/InteractionManager.js @@ -4,6 +4,281 @@ describe('PIXI.interaction.InteractionManager', function () { + describe('event basics', function () + { + it('should call mousedown handler', function () + { + const stage = new PIXI.Container(); + const graphics = new PIXI.Graphics(); + const eventSpy = sinon.spy(); + const pointer = new MockPointer(stage); + + stage.addChild(graphics); + graphics.beginFill(0xFFFFFF); + graphics.drawRect(0, 0, 50, 50); + graphics.interactive = true; + graphics.on('mousedown', eventSpy); + + pointer.mousedown(10, 10); + + expect(eventSpy).to.have.been.calledOnce; + }); + + it('should call mouseup handler', function () + { + const stage = new PIXI.Container(); + const graphics = new PIXI.Graphics(); + const eventSpy = sinon.spy(); + const pointer = new MockPointer(stage); + + stage.addChild(graphics); + graphics.beginFill(0xFFFFFF); + graphics.drawRect(0, 0, 50, 50); + graphics.interactive = true; + graphics.on('mouseup', eventSpy); + + pointer.click(10, 10); + + expect(eventSpy).to.have.been.called; + }); + + it('should call mouseupoutside handler', function () + { + const stage = new PIXI.Container(); + const graphics = new PIXI.Graphics(); + const eventSpy = sinon.spy(); + const pointer = new MockPointer(stage); + + stage.addChild(graphics); + graphics.beginFill(0xFFFFFF); + graphics.drawRect(0, 0, 50, 50); + graphics.interactive = true; + graphics.on('mouseupoutside', eventSpy); + + pointer.mousedown(10, 10); + pointer.mouseup(60, 60); + + expect(eventSpy).to.have.been.called; + }); + + it('should call mouseover handler', function () + { + const stage = new PIXI.Container(); + const graphics = new PIXI.Graphics(); + const eventSpy = sinon.spy(); + const pointer = new MockPointer(stage); + + stage.addChild(graphics); + graphics.beginFill(0xFFFFFF); + graphics.drawRect(0, 0, 50, 50); + graphics.interactive = true; + graphics.on('mouseover', eventSpy); + + pointer.mousemove(10, 10); + + expect(eventSpy).to.have.been.called; + }); + + it('should call mouseout handler', function () + { + const stage = new PIXI.Container(); + const graphics = new PIXI.Graphics(); + const eventSpy = sinon.spy(); + const pointer = new MockPointer(stage); + + stage.addChild(graphics); + graphics.beginFill(0xFFFFFF); + graphics.drawRect(0, 0, 50, 50); + graphics.interactive = true; + graphics.on('mouseout', eventSpy); + + pointer.mousemove(10, 10); + pointer.mousemove(60, 60); + + expect(eventSpy).to.have.been.called; + }); + }); + + describe('add/remove events', function () + { + let stub; + + before(function () + { + stub = sinon.stub(PIXI.interaction.InteractionManager.prototype, 'setTargetElement'); + }); + + after(function () + { + stub.restore(); + }); + + it('should add and remove pointer events to document', function () + { + const manager = new PIXI.interaction.InteractionManager(sinon.stub()); + const addSpy = sinon.spy(window.document, 'addEventListener'); + const removeSpy = sinon.spy(window.document, 'removeEventListener'); + + manager.interactionDOMElement = { style: {}, addEventListener: sinon.stub(), removeEventListener: sinon.stub() }; + manager.supportsPointerEvents = true; + + manager.addEvents(); + + expect(addSpy).to.have.been.calledOnce; + expect(addSpy).to.have.been.calledWith('pointermove'); + + manager.removeEvents(); + + expect(removeSpy).to.have.been.calledOnce; + expect(removeSpy).to.have.been.calledWith('pointermove'); + + addSpy.restore(); + removeSpy.restore(); + }); + + it('should add and remove pointer events to window', function () + { + const manager = new PIXI.interaction.InteractionManager(sinon.stub()); + const addSpy = sinon.spy(window, 'addEventListener'); + const removeSpy = sinon.spy(window, 'removeEventListener'); + + manager.interactionDOMElement = { style: {}, addEventListener: sinon.stub(), removeEventListener: sinon.stub() }; + manager.supportsPointerEvents = true; + + manager.addEvents(); + + expect(addSpy).to.have.been.calledTwice; + expect(addSpy).to.have.been.calledWith('pointercancel'); + expect(addSpy).to.have.been.calledWith('pointerup'); + + manager.removeEvents(); + + expect(removeSpy).to.have.been.calledTwice; + expect(removeSpy).to.have.been.calledWith('pointercancel'); + expect(removeSpy).to.have.been.calledWith('pointerup'); + + addSpy.restore(); + removeSpy.restore(); + }); + + it('should add and remove pointer events to element', function () + { + const manager = new PIXI.interaction.InteractionManager(sinon.stub()); + const element = { style: {}, addEventListener: sinon.stub(), removeEventListener: sinon.stub() }; + + manager.interactionDOMElement = element; + manager.supportsPointerEvents = true; + + manager.addEvents(); + + expect(element.addEventListener).to.have.been.calledThrice; + expect(element.addEventListener).to.have.been.calledWith('pointerdown'); + expect(element.addEventListener).to.have.been.calledWith('pointerleave'); + expect(element.addEventListener).to.have.been.calledWith('pointerover'); + + manager.removeEvents(); + + expect(element.removeEventListener).to.have.been.calledThrice; + expect(element.removeEventListener).to.have.been.calledWith('pointerdown'); + expect(element.removeEventListener).to.have.been.calledWith('pointerleave'); + expect(element.removeEventListener).to.have.been.calledWith('pointerover'); + }); + + it('should add and remove mouse events to document', function () + { + const manager = new PIXI.interaction.InteractionManager(sinon.stub()); + const addSpy = sinon.spy(window.document, 'addEventListener'); + const removeSpy = sinon.spy(window.document, 'removeEventListener'); + + manager.interactionDOMElement = { style: {}, addEventListener: sinon.stub(), removeEventListener: sinon.stub() }; + manager.supportsPointerEvents = false; + + manager.addEvents(); + + expect(addSpy).to.have.been.calledOnce; + expect(addSpy).to.have.been.calledWith('mousemove'); + + manager.removeEvents(); + + expect(removeSpy).to.have.been.calledOnce; + expect(removeSpy).to.have.been.calledWith('mousemove'); + + addSpy.restore(); + removeSpy.restore(); + }); + + it('should add and remove mouse events to window', function () + { + const manager = new PIXI.interaction.InteractionManager(sinon.stub()); + const addSpy = sinon.spy(window, 'addEventListener'); + const removeSpy = sinon.spy(window, 'removeEventListener'); + + manager.interactionDOMElement = { style: {}, addEventListener: sinon.stub(), removeEventListener: sinon.stub() }; + manager.supportsPointerEvents = false; + + manager.addEvents(); + + expect(addSpy).to.have.been.calledOnce; + expect(addSpy).to.have.been.calledWith('mouseup'); + + manager.removeEvents(); + + expect(removeSpy).to.have.been.calledOnce; + expect(removeSpy).to.have.been.calledWith('mouseup'); + + addSpy.restore(); + removeSpy.restore(); + }); + + it('should add and remove mouse events to element', function () + { + const manager = new PIXI.interaction.InteractionManager(sinon.stub()); + const element = { style: {}, addEventListener: sinon.stub(), removeEventListener: sinon.stub() }; + + manager.interactionDOMElement = element; + manager.supportsPointerEvents = false; + manager.supportsTouchEvents = false; + + manager.addEvents(); + + expect(element.addEventListener).to.have.been.calledThrice; + expect(element.addEventListener).to.have.been.calledWith('mousedown'); + expect(element.addEventListener).to.have.been.calledWith('mouseout'); + expect(element.addEventListener).to.have.been.calledWith('mouseover'); + + manager.removeEvents(); + + expect(element.removeEventListener).to.have.been.calledThrice; + expect(element.removeEventListener).to.have.been.calledWith('mousedown'); + expect(element.removeEventListener).to.have.been.calledWith('mouseout'); + expect(element.removeEventListener).to.have.been.calledWith('mouseover'); + }); + + it('should add and remove touch events to element', function () + { + const manager = new PIXI.interaction.InteractionManager(sinon.stub()); + const element = { style: {}, addEventListener: sinon.stub(), removeEventListener: sinon.stub() }; + + manager.interactionDOMElement = element; + manager.supportsPointerEvents = false; + manager.supportsTouchEvents = true; + + manager.addEvents(); + + expect(element.addEventListener).to.have.been.calledWith('touchstart'); + expect(element.addEventListener).to.have.been.calledWith('touchcancel'); + expect(element.addEventListener).to.have.been.calledWith('touchend'); + expect(element.addEventListener).to.have.been.calledWith('touchmove'); + + manager.removeEvents(); + + expect(element.removeEventListener).to.have.been.calledWith('touchstart'); + expect(element.removeEventListener).to.have.been.calledWith('touchcancel'); + expect(element.removeEventListener).to.have.been.calledWith('touchend'); + expect(element.removeEventListener).to.have.been.calledWith('touchmove'); + }); + }); + describe('onClick', function () { it('should call handler when inside', function () @@ -364,7 +639,6 @@ expect(scene.parentCallback).to.have.been.calledOnce; }); - /* TODO: Fix #3596 it('should callback parent and behind child when clicking overlap', function () { const stage = new PIXI.Container(); @@ -382,7 +656,6 @@ expect(scene.frontChildCallback).to.not.have.been.called; expect(scene.parentCallback).to.have.been.calledOnce; }); - */ it('should callback parent and behind child when clicking behind child', function () { @@ -461,4 +734,241 @@ }); }); }); + + describe('cursor changes', function () + { + it('cursor should be the cursor of interactive item', function () + { + const stage = new PIXI.Container(); + const graphics = new PIXI.Graphics(); + const pointer = new MockPointer(stage); + + stage.addChild(graphics); + graphics.beginFill(0xFFFFFF); + graphics.drawRect(0, 0, 50, 50); + graphics.interactive = true; + graphics.cursor = 'help'; + pointer.interaction.cursorStyles.help = 'help'; + + pointer.mousemove(10, 10); + + expect(pointer.renderer.view.style.cursor).to.equal('help'); + }); + + it('should return cursor to default on mouseout', function () + { + const stage = new PIXI.Container(); + const graphics = new PIXI.Graphics(); + const pointer = new MockPointer(stage); + + stage.addChild(graphics); + graphics.beginFill(0xFFFFFF); + graphics.drawRect(0, 0, 50, 50); + graphics.interactive = true; + graphics.cursor = 'help'; + pointer.interaction.cursorStyles.help = 'help'; + + pointer.mousemove(10, 10); + pointer.mousemove(60, 60); + + expect(pointer.renderer.view.style.cursor).to.equal(pointer.interaction.cursorStyles.default); + }); + + it('should still be the over cursor after a click', function () + { + const stage = new PIXI.Container(); + const graphics = new PIXI.Graphics(); + const pointer = new MockPointer(stage); + + stage.addChild(graphics); + graphics.beginFill(0xFFFFFF); + graphics.drawRect(0, 0, 50, 50); + graphics.interactive = true; + graphics.cursor = 'help'; + pointer.interaction.cursorStyles.help = 'help'; + + pointer.mousemove(10, 10); + pointer.click(10, 10); + + expect(pointer.renderer.view.style.cursor).to.equal('help'); + }); + + it('should return cursor to default when mouse leaves renderer', function () + { + const stage = new PIXI.Container(); + const graphics = new PIXI.Graphics(); + const pointer = new MockPointer(stage); + + stage.addChild(graphics); + graphics.beginFill(0xFFFFFF); + graphics.drawRect(0, 0, 50, 50); + graphics.interactive = true; + graphics.cursor = 'help'; + pointer.interaction.cursorStyles.help = 'help'; + + pointer.mousemove(10, 10); + pointer.mousemove(-10, 60); + + expect(pointer.renderer.view.style.cursor).to.equal(pointer.interaction.cursorStyles.default); + }); + + it('cursor callback should be called', function () + { + const stage = new PIXI.Container(); + const graphics = new PIXI.Graphics(); + const overSpy = sinon.spy(); + const defaultSpy = sinon.spy(); + const pointer = new MockPointer(stage); + + stage.addChild(graphics); + graphics.beginFill(0xFFFFFF); + graphics.drawRect(0, 0, 50, 50); + graphics.interactive = true; + graphics.cursor = 'help'; + pointer.interaction.cursorStyles.help = overSpy; + pointer.interaction.cursorStyles.default = defaultSpy; + + pointer.mousemove(10, 10); + pointer.mousemove(60, 60); + + expect(overSpy).to.have.been.called; + expect(defaultSpy).to.have.been.called; + }); + + it('cursor callback should only be called if the cursor actually changed', function () + { + const stage = new PIXI.Container(); + const graphics = new PIXI.Graphics(); + const defaultSpy = sinon.spy(); + const pointer = new MockPointer(stage); + + stage.addChild(graphics); + graphics.beginFill(0xFFFFFF); + graphics.drawRect(0, 0, 50, 50); + graphics.interactive = true; + graphics.cursor = null; + pointer.interaction.cursorStyles.default = defaultSpy; + + pointer.mousemove(10, 10); + pointer.mousemove(20, 20); + + expect(defaultSpy).to.have.been.calledOnce; + }); + + it('cursor style object should be fully applied', function () + { + const stage = new PIXI.Container(); + const graphics = new PIXI.Graphics(); + const pointer = new MockPointer(stage); + + stage.addChild(graphics); + graphics.beginFill(0xFFFFFF); + graphics.drawRect(0, 0, 50, 50); + graphics.interactive = true; + graphics.cursor = 'help'; + pointer.interaction.cursorStyles.help = { + cursor: 'none', + display: 'none', + }; + + pointer.mousemove(10, 10); + + expect(pointer.renderer.view.style.cursor).to.equal('none'); + expect(pointer.renderer.view.style.display).to.equal('none'); + }); + + it('should not change cursor style if no cursor style provided', function () + { + const stage = new PIXI.Container(); + const graphics = new PIXI.Graphics(); + const pointer = new MockPointer(stage); + + stage.addChild(graphics); + graphics.beginFill(0xFFFFFF); + graphics.drawRect(0, 0, 50, 50); + graphics.interactive = true; + graphics.cursor = 'pointer'; + pointer.interaction.cursorStyles.pointer = null; + pointer.interaction.cursorStyles.default = null; + + pointer.mousemove(10, 10); + expect(pointer.renderer.view.style.cursor).to.equal(''); + + pointer.mousemove(60, 60); + expect(pointer.renderer.view.style.cursor).to.equal(''); + }); + }); + + describe('recursive hitTesting', function () + { + function getScene() + { + const stage = new PIXI.Container(); + const behindChild = new PIXI.Graphics(); + const middleChild = new PIXI.Graphics(); + const frontChild = new PIXI.Graphics(); + + behindChild.beginFill(0xFF); + behindChild.drawRect(0, 0, 50, 50); + + middleChild.beginFill(0xFF00); + middleChild.drawRect(0, 0, 50, 50); + + frontChild.beginFill(0xFF0000); + frontChild.drawRect(0, 0, 50, 50); + + stage.addChild(behindChild, middleChild, frontChild); + + return { + behindChild, + middleChild, + frontChild, + stage, + }; + } + + describe('when frontChild is interactive', function () + { + it('should stop hitTesting after first hit', function () + { + const scene = getScene(); + const pointer = new MockPointer(scene.stage); + const frontHitTest = sinon.spy(scene.frontChild, 'containsPoint'); + const middleHitTest = sinon.spy(scene.middleChild, 'containsPoint'); + const behindHitTest = sinon.spy(scene.behindChild, 'containsPoint'); + + scene.frontChild.interactive = true; + scene.middleChild.interactive = true; + scene.behindChild.interactive = true; + + pointer.mousedown(25, 25); + + expect(frontHitTest).to.have.been.calledOnce; + expect(middleHitTest).to.not.have.been.called; + expect(behindHitTest).to.not.have.been.called; + }); + }); + + describe('when frontChild is not interactive', function () + { + it('should stop hitTesting after first hit', function () + { + const scene = getScene(); + const pointer = new MockPointer(scene.stage); + const frontHitTest = sinon.spy(scene.frontChild, 'containsPoint'); + const middleHitTest = sinon.spy(scene.middleChild, 'containsPoint'); + const behindHitTest = sinon.spy(scene.behindChild, 'containsPoint'); + + scene.frontChild.interactive = false; + scene.middleChild.interactive = true; + scene.behindChild.interactive = true; + + pointer.mousedown(25, 25); + + expect(frontHitTest).to.not.have.been.called; + expect(middleHitTest).to.have.been.calledOnce; + expect(behindHitTest).to.not.have.been.called; + }); + }); + }); }); diff --git a/test/interaction/MockPointer.js b/test/interaction/MockPointer.js index 61fb0d8..67878af 100644 --- a/test/interaction/MockPointer.js +++ b/test/interaction/MockPointer.js @@ -46,6 +46,45 @@ * @param {number} x - pointer x position * @param {number} y - pointer y position */ + mousemove(x, y) + { + const mouseEvent = new MouseEvent('mousemove', { + clientX: x, + clientY: y, + preventDefault: sinon.stub(), + }); + + this.setPosition(x, y); + this.render(); + // mouseOverRenderer state should be correct, so mouse position to view rect + const rect = new PIXI.Rectangle(0, 0, this.renderer.width, this.renderer.height); + + if (rect.contains(x, y)) + { + if (!this.interaction.mouseOverRenderer) + { + this.interaction.onPointerOver(new MouseEvent('mouseover', { + clientX: x, + clientY: y, + preventDefault: sinon.stub(), + })); + } + this.interaction.onPointerMove(mouseEvent); + } + else + { + this.interaction.onPointerOut(new MouseEvent('mouseout', { + clientX: x, + clientY: y, + preventDefault: sinon.stub(), + })); + } + } + + /** + * @param {number} x - pointer x position + * @param {number} y - pointer y position + */ click(x, y) { this.mousedown(x, y); diff --git a/test/withGL.js b/test/withGL.js new file mode 100644 index 0000000..bd47b2d --- /dev/null +++ b/test/withGL.js @@ -0,0 +1,8 @@ +'use strict'; + +function withGL(fn) +{ + return PIXI.utils.isWebGLSupported() ? fn : undefined; +} + +module.exports = withGL;