diff --git a/package.json b/package.json index 1271fef..71ade15 100644 --- a/package.json +++ b/package.json @@ -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 1271fef..71ade15 100644 --- a/package.json +++ b/package.json @@ -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/scripts/release.js b/scripts/release.js index c4c8e36..156620c 100644 --- a/scripts/release.js +++ b/scripts/release.js @@ -12,7 +12,8 @@ 'dist/**/*', 'lib/**/*', 'src/**/*', - 'scripts/**/*', + 'scripts/*', + 'scripts/renders/*', 'test/**/*', '*.json', '*.md', diff --git a/package.json b/package.json index 1271fef..71ade15 100644 --- a/package.json +++ b/package.json @@ -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/scripts/release.js b/scripts/release.js index c4c8e36..156620c 100644 --- a/scripts/release.js +++ b/scripts/release.js @@ -12,7 +12,8 @@ 'dist/**/*', 'lib/**/*', 'src/**/*', - 'scripts/**/*', + 'scripts/*', + 'scripts/renders/*', 'test/**/*', '*.json', '*.md', diff --git a/scripts/renders/client.js b/scripts/renders/client.js index 16c9565..6faee34 100755 --- a/scripts/renders/client.js +++ b/scripts/renders/client.js @@ -1,6 +1,6 @@ 'use strict'; -require('../../bin/pixi'); +require('../../dist/pixi'); PIXI.utils.skipHello(); const fs = require('fs'); diff --git a/package.json b/package.json index 1271fef..71ade15 100644 --- a/package.json +++ b/package.json @@ -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/scripts/release.js b/scripts/release.js index c4c8e36..156620c 100644 --- a/scripts/release.js +++ b/scripts/release.js @@ -12,7 +12,8 @@ 'dist/**/*', 'lib/**/*', 'src/**/*', - 'scripts/**/*', + 'scripts/*', + 'scripts/renders/*', 'test/**/*', '*.json', '*.md', diff --git a/scripts/renders/client.js b/scripts/renders/client.js index 16c9565..6faee34 100755 --- a/scripts/renders/client.js +++ b/scripts/renders/client.js @@ -1,6 +1,6 @@ 'use strict'; -require('../../bin/pixi'); +require('../../dist/pixi'); PIXI.utils.skipHello(); const fs = require('fs'); 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 1271fef..71ade15 100644 --- a/package.json +++ b/package.json @@ -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/scripts/release.js b/scripts/release.js index c4c8e36..156620c 100644 --- a/scripts/release.js +++ b/scripts/release.js @@ -12,7 +12,8 @@ 'dist/**/*', 'lib/**/*', 'src/**/*', - 'scripts/**/*', + 'scripts/*', + 'scripts/renders/*', 'test/**/*', '*.json', '*.md', diff --git a/scripts/renders/client.js b/scripts/renders/client.js index 16c9565..6faee34 100755 --- a/scripts/renders/client.js +++ b/scripts/renders/client.js @@ -1,6 +1,6 @@ 'use strict'; -require('../../bin/pixi'); +require('../../dist/pixi'); PIXI.utils.skipHello(); const fs = require('fs'); 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 bc730db..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. @@ -33,8 +33,11 @@ * need to call toDataUrl on the webgl context * @param {number} [options.resolution=1] - The resolution / device pixel ratio of the renderer, retina would be 2 * @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 @@ -43,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. */ @@ -73,7 +99,7 @@ */ stop() { - this.ticker.stop(); + this._ticker.stop(); } /** @@ -81,7 +107,7 @@ */ start() { - this.ticker.start(); + this._ticker.start(); } /** @@ -95,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 1271fef..71ade15 100644 --- a/package.json +++ b/package.json @@ -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/scripts/release.js b/scripts/release.js index c4c8e36..156620c 100644 --- a/scripts/release.js +++ b/scripts/release.js @@ -12,7 +12,8 @@ 'dist/**/*', 'lib/**/*', 'src/**/*', - 'scripts/**/*', + 'scripts/*', + 'scripts/renders/*', 'test/**/*', '*.json', '*.md', diff --git a/scripts/renders/client.js b/scripts/renders/client.js index 16c9565..6faee34 100755 --- a/scripts/renders/client.js +++ b/scripts/renders/client.js @@ -1,6 +1,6 @@ 'use strict'; -require('../../bin/pixi'); +require('../../dist/pixi'); PIXI.utils.skipHello(); const fs = require('fs'); 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 bc730db..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. @@ -33,8 +33,11 @@ * need to call toDataUrl on the webgl context * @param {number} [options.resolution=1] - The resolution / device pixel ratio of the renderer, retina would be 2 * @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 @@ -43,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. */ @@ -73,7 +99,7 @@ */ stop() { - this.ticker.stop(); + this._ticker.stop(); } /** @@ -81,7 +107,7 @@ */ start() { - this.ticker.start(); + this._ticker.start(); } /** @@ -95,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 index 81e076b..78b17e6 100644 --- a/src/core/Shader.js +++ b/src/core/Shader.js @@ -1,9 +1,7 @@ import { GLShader } from 'pixi-gl-core'; import settings from './settings'; -const { PRECISION } = settings; - -function checkPrecision(src) +function checkPrecision(src, def) { if (src instanceof Array) { @@ -11,14 +9,14 @@ { const copy = src.slice(0); - copy.unshift(`precision ${PRECISION} float;`); + copy.unshift(`precision ${def} float;`); return copy; } } else if (src.substring(0, 9) !== 'precision') { - return `precision ${PRECISION} float;\n${src}`; + return `precision ${def} float;\n${src}`; } return src; @@ -42,6 +40,7 @@ */ constructor(gl, vertexSrc, fragmentSrc) { - super(gl, checkPrecision(vertexSrc), checkPrecision(fragmentSrc)); + super(gl, checkPrecision(vertexSrc, settings.PRECISION_VERTEX), + checkPrecision(fragmentSrc, settings.PRECISION_FRAGMENT)); } } diff --git a/package.json b/package.json index 1271fef..71ade15 100644 --- a/package.json +++ b/package.json @@ -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/scripts/release.js b/scripts/release.js index c4c8e36..156620c 100644 --- a/scripts/release.js +++ b/scripts/release.js @@ -12,7 +12,8 @@ 'dist/**/*', 'lib/**/*', 'src/**/*', - 'scripts/**/*', + 'scripts/*', + 'scripts/renders/*', 'test/**/*', '*.json', '*.md', diff --git a/scripts/renders/client.js b/scripts/renders/client.js index 16c9565..6faee34 100755 --- a/scripts/renders/client.js +++ b/scripts/renders/client.js @@ -1,6 +1,6 @@ 'use strict'; -require('../../bin/pixi'); +require('../../dist/pixi'); PIXI.utils.skipHello(); const fs = require('fs'); 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 bc730db..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. @@ -33,8 +33,11 @@ * need to call toDataUrl on the webgl context * @param {number} [options.resolution=1] - The resolution / device pixel ratio of the renderer, retina would be 2 * @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 @@ -43,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. */ @@ -73,7 +99,7 @@ */ stop() { - this.ticker.stop(); + this._ticker.stop(); } /** @@ -81,7 +107,7 @@ */ start() { - this.ticker.start(); + this._ticker.start(); } /** @@ -95,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 index 81e076b..78b17e6 100644 --- a/src/core/Shader.js +++ b/src/core/Shader.js @@ -1,9 +1,7 @@ import { GLShader } from 'pixi-gl-core'; import settings from './settings'; -const { PRECISION } = settings; - -function checkPrecision(src) +function checkPrecision(src, def) { if (src instanceof Array) { @@ -11,14 +9,14 @@ { const copy = src.slice(0); - copy.unshift(`precision ${PRECISION} float;`); + copy.unshift(`precision ${def} float;`); return copy; } } else if (src.substring(0, 9) !== 'precision') { - return `precision ${PRECISION} float;\n${src}`; + return `precision ${def} float;\n${src}`; } return src; @@ -42,6 +40,7 @@ */ constructor(gl, vertexSrc, fragmentSrc) { - super(gl, checkPrecision(vertexSrc), checkPrecision(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 3425b41..f9235e4 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -181,7 +181,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 1271fef..71ade15 100644 --- a/package.json +++ b/package.json @@ -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/scripts/release.js b/scripts/release.js index c4c8e36..156620c 100644 --- a/scripts/release.js +++ b/scripts/release.js @@ -12,7 +12,8 @@ 'dist/**/*', 'lib/**/*', 'src/**/*', - 'scripts/**/*', + 'scripts/*', + 'scripts/renders/*', 'test/**/*', '*.json', '*.md', diff --git a/scripts/renders/client.js b/scripts/renders/client.js index 16c9565..6faee34 100755 --- a/scripts/renders/client.js +++ b/scripts/renders/client.js @@ -1,6 +1,6 @@ 'use strict'; -require('../../bin/pixi'); +require('../../dist/pixi'); PIXI.utils.skipHello(); const fs = require('fs'); 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 bc730db..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. @@ -33,8 +33,11 @@ * need to call toDataUrl on the webgl context * @param {number} [options.resolution=1] - The resolution / device pixel ratio of the renderer, retina would be 2 * @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 @@ -43,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. */ @@ -73,7 +99,7 @@ */ stop() { - this.ticker.stop(); + this._ticker.stop(); } /** @@ -81,7 +107,7 @@ */ start() { - this.ticker.start(); + this._ticker.start(); } /** @@ -95,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 index 81e076b..78b17e6 100644 --- a/src/core/Shader.js +++ b/src/core/Shader.js @@ -1,9 +1,7 @@ import { GLShader } from 'pixi-gl-core'; import settings from './settings'; -const { PRECISION } = settings; - -function checkPrecision(src) +function checkPrecision(src, def) { if (src instanceof Array) { @@ -11,14 +9,14 @@ { const copy = src.slice(0); - copy.unshift(`precision ${PRECISION} float;`); + copy.unshift(`precision ${def} float;`); return copy; } } else if (src.substring(0, 9) !== 'precision') { - return `precision ${PRECISION} float;\n${src}`; + return `precision ${def} float;\n${src}`; } return src; @@ -42,6 +40,7 @@ */ constructor(gl, vertexSrc, fragmentSrc) { - super(gl, checkPrecision(vertexSrc), checkPrecision(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 3425b41..f9235e4 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -181,7 +181,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 b3792ef..17fde8d 100644 --- a/src/core/display/Container.js +++ b/src/core/display/Container.js @@ -73,13 +73,14 @@ } child.parent = this; - - // ensure a transform will be recalculated.. - this.transform._parentID = -1; - this._boundsID++; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.push(child); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(this.children.length - 1); child.emit('added', this); @@ -108,9 +109,14 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.splice(index, 0, child); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(index); child.emit('added', this); @@ -174,6 +180,7 @@ removeItems(this.children, currentIndex, 1); // remove from old position this.children.splice(index, 0, child); // add at new position + this.onChildrenChange(index); } @@ -220,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! @@ -244,9 +252,14 @@ { const child = this.getChildAt(index); + // ensure child transform will be recalculated.. child.parent = null; + child.transform._parentID = -1; removeItems(this.children, index, 1); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(index); child.emit('removed', this); @@ -275,8 +288,14 @@ for (let i = 0; i < removed.length; ++i) { removed[i].parent = null; + if (removed[i].transform) + { + removed[i].transform._parentID = -1; + } } + this._boundsID++; + this.onChildrenChange(beginIndex); for (let i = 0; i < removed.length; ++i) diff --git a/package.json b/package.json index 1271fef..71ade15 100644 --- a/package.json +++ b/package.json @@ -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/scripts/release.js b/scripts/release.js index c4c8e36..156620c 100644 --- a/scripts/release.js +++ b/scripts/release.js @@ -12,7 +12,8 @@ 'dist/**/*', 'lib/**/*', 'src/**/*', - 'scripts/**/*', + 'scripts/*', + 'scripts/renders/*', 'test/**/*', '*.json', '*.md', diff --git a/scripts/renders/client.js b/scripts/renders/client.js index 16c9565..6faee34 100755 --- a/scripts/renders/client.js +++ b/scripts/renders/client.js @@ -1,6 +1,6 @@ 'use strict'; -require('../../bin/pixi'); +require('../../dist/pixi'); PIXI.utils.skipHello(); const fs = require('fs'); 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 bc730db..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. @@ -33,8 +33,11 @@ * need to call toDataUrl on the webgl context * @param {number} [options.resolution=1] - The resolution / device pixel ratio of the renderer, retina would be 2 * @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 @@ -43,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. */ @@ -73,7 +99,7 @@ */ stop() { - this.ticker.stop(); + this._ticker.stop(); } /** @@ -81,7 +107,7 @@ */ start() { - this.ticker.start(); + this._ticker.start(); } /** @@ -95,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 index 81e076b..78b17e6 100644 --- a/src/core/Shader.js +++ b/src/core/Shader.js @@ -1,9 +1,7 @@ import { GLShader } from 'pixi-gl-core'; import settings from './settings'; -const { PRECISION } = settings; - -function checkPrecision(src) +function checkPrecision(src, def) { if (src instanceof Array) { @@ -11,14 +9,14 @@ { const copy = src.slice(0); - copy.unshift(`precision ${PRECISION} float;`); + copy.unshift(`precision ${def} float;`); return copy; } } else if (src.substring(0, 9) !== 'precision') { - return `precision ${PRECISION} float;\n${src}`; + return `precision ${def} float;\n${src}`; } return src; @@ -42,6 +40,7 @@ */ constructor(gl, vertexSrc, fragmentSrc) { - super(gl, checkPrecision(vertexSrc), checkPrecision(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 3425b41..f9235e4 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -181,7 +181,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 b3792ef..17fde8d 100644 --- a/src/core/display/Container.js +++ b/src/core/display/Container.js @@ -73,13 +73,14 @@ } child.parent = this; - - // ensure a transform will be recalculated.. - this.transform._parentID = -1; - this._boundsID++; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.push(child); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(this.children.length - 1); child.emit('added', this); @@ -108,9 +109,14 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.splice(index, 0, child); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(index); child.emit('added', this); @@ -174,6 +180,7 @@ removeItems(this.children, currentIndex, 1); // remove from old position this.children.splice(index, 0, child); // add at new position + this.onChildrenChange(index); } @@ -220,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! @@ -244,9 +252,14 @@ { const child = this.getChildAt(index); + // ensure child transform will be recalculated.. child.parent = null; + child.transform._parentID = -1; removeItems(this.children, index, 1); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(index); child.emit('removed', this); @@ -275,8 +288,14 @@ for (let i = 0; i < removed.length; ++i) { removed[i].parent = null; + if (removed[i].transform) + { + removed[i].transform._parentID = -1; + } } + this._boundsID++; + this.onChildrenChange(beginIndex); for (let i = 0; i < removed.length; ++i) diff --git a/src/core/display/DisplayObject.js b/src/core/display/DisplayObject.js index 3affe36..4e6c77d 100644 --- a/src/core/display/DisplayObject.js +++ b/src/core/display/DisplayObject.js @@ -109,7 +109,7 @@ /** * The original, cached mask of the object * - * @member {PIXI.Rectangle} + * @member {PIXI.Graphics|PIXI.Sprite} * @private */ this._mask = null; diff --git a/package.json b/package.json index 1271fef..71ade15 100644 --- a/package.json +++ b/package.json @@ -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/scripts/release.js b/scripts/release.js index c4c8e36..156620c 100644 --- a/scripts/release.js +++ b/scripts/release.js @@ -12,7 +12,8 @@ 'dist/**/*', 'lib/**/*', 'src/**/*', - 'scripts/**/*', + 'scripts/*', + 'scripts/renders/*', 'test/**/*', '*.json', '*.md', diff --git a/scripts/renders/client.js b/scripts/renders/client.js index 16c9565..6faee34 100755 --- a/scripts/renders/client.js +++ b/scripts/renders/client.js @@ -1,6 +1,6 @@ 'use strict'; -require('../../bin/pixi'); +require('../../dist/pixi'); PIXI.utils.skipHello(); const fs = require('fs'); 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 bc730db..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. @@ -33,8 +33,11 @@ * need to call toDataUrl on the webgl context * @param {number} [options.resolution=1] - The resolution / device pixel ratio of the renderer, retina would be 2 * @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 @@ -43,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. */ @@ -73,7 +99,7 @@ */ stop() { - this.ticker.stop(); + this._ticker.stop(); } /** @@ -81,7 +107,7 @@ */ start() { - this.ticker.start(); + this._ticker.start(); } /** @@ -95,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 index 81e076b..78b17e6 100644 --- a/src/core/Shader.js +++ b/src/core/Shader.js @@ -1,9 +1,7 @@ import { GLShader } from 'pixi-gl-core'; import settings from './settings'; -const { PRECISION } = settings; - -function checkPrecision(src) +function checkPrecision(src, def) { if (src instanceof Array) { @@ -11,14 +9,14 @@ { const copy = src.slice(0); - copy.unshift(`precision ${PRECISION} float;`); + copy.unshift(`precision ${def} float;`); return copy; } } else if (src.substring(0, 9) !== 'precision') { - return `precision ${PRECISION} float;\n${src}`; + return `precision ${def} float;\n${src}`; } return src; @@ -42,6 +40,7 @@ */ constructor(gl, vertexSrc, fragmentSrc) { - super(gl, checkPrecision(vertexSrc), checkPrecision(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 3425b41..f9235e4 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -181,7 +181,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 b3792ef..17fde8d 100644 --- a/src/core/display/Container.js +++ b/src/core/display/Container.js @@ -73,13 +73,14 @@ } child.parent = this; - - // ensure a transform will be recalculated.. - this.transform._parentID = -1; - this._boundsID++; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.push(child); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(this.children.length - 1); child.emit('added', this); @@ -108,9 +109,14 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.splice(index, 0, child); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(index); child.emit('added', this); @@ -174,6 +180,7 @@ removeItems(this.children, currentIndex, 1); // remove from old position this.children.splice(index, 0, child); // add at new position + this.onChildrenChange(index); } @@ -220,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! @@ -244,9 +252,14 @@ { const child = this.getChildAt(index); + // ensure child transform will be recalculated.. child.parent = null; + child.transform._parentID = -1; removeItems(this.children, index, 1); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(index); child.emit('removed', this); @@ -275,8 +288,14 @@ for (let i = 0; i < removed.length; ++i) { removed[i].parent = null; + if (removed[i].transform) + { + removed[i].transform._parentID = -1; + } } + this._boundsID++; + this.onChildrenChange(beginIndex); for (let i = 0; i < removed.length; ++i) diff --git a/src/core/display/DisplayObject.js b/src/core/display/DisplayObject.js index 3affe36..4e6c77d 100644 --- a/src/core/display/DisplayObject.js +++ b/src/core/display/DisplayObject.js @@ -109,7 +109,7 @@ /** * The original, cached mask of the object * - * @member {PIXI.Rectangle} + * @member {PIXI.Graphics|PIXI.Sprite} * @private */ this._mask = null; diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index 95f2af4..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 ); @@ -1065,10 +1061,15 @@ canvasRenderer = new CanvasRenderer(); } - tempMatrix.tx = -bounds.x; - tempMatrix.ty = -bounds.y; + this.transform.updateLocalTransform(); + this.transform.localTransform.copy(tempMatrix); - canvasRenderer.render(this, canvasBuffer, false, tempMatrix); + tempMatrix.invert(); + + tempMatrix.tx -= bounds.x; + tempMatrix.ty -= bounds.y; + + canvasRenderer.render(this, canvasBuffer, true, tempMatrix); const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode); diff --git a/package.json b/package.json index 1271fef..71ade15 100644 --- a/package.json +++ b/package.json @@ -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/scripts/release.js b/scripts/release.js index c4c8e36..156620c 100644 --- a/scripts/release.js +++ b/scripts/release.js @@ -12,7 +12,8 @@ 'dist/**/*', 'lib/**/*', 'src/**/*', - 'scripts/**/*', + 'scripts/*', + 'scripts/renders/*', 'test/**/*', '*.json', '*.md', diff --git a/scripts/renders/client.js b/scripts/renders/client.js index 16c9565..6faee34 100755 --- a/scripts/renders/client.js +++ b/scripts/renders/client.js @@ -1,6 +1,6 @@ 'use strict'; -require('../../bin/pixi'); +require('../../dist/pixi'); PIXI.utils.skipHello(); const fs = require('fs'); 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 bc730db..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. @@ -33,8 +33,11 @@ * need to call toDataUrl on the webgl context * @param {number} [options.resolution=1] - The resolution / device pixel ratio of the renderer, retina would be 2 * @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 @@ -43,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. */ @@ -73,7 +99,7 @@ */ stop() { - this.ticker.stop(); + this._ticker.stop(); } /** @@ -81,7 +107,7 @@ */ start() { - this.ticker.start(); + this._ticker.start(); } /** @@ -95,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 index 81e076b..78b17e6 100644 --- a/src/core/Shader.js +++ b/src/core/Shader.js @@ -1,9 +1,7 @@ import { GLShader } from 'pixi-gl-core'; import settings from './settings'; -const { PRECISION } = settings; - -function checkPrecision(src) +function checkPrecision(src, def) { if (src instanceof Array) { @@ -11,14 +9,14 @@ { const copy = src.slice(0); - copy.unshift(`precision ${PRECISION} float;`); + copy.unshift(`precision ${def} float;`); return copy; } } else if (src.substring(0, 9) !== 'precision') { - return `precision ${PRECISION} float;\n${src}`; + return `precision ${def} float;\n${src}`; } return src; @@ -42,6 +40,7 @@ */ constructor(gl, vertexSrc, fragmentSrc) { - super(gl, checkPrecision(vertexSrc), checkPrecision(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 3425b41..f9235e4 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -181,7 +181,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 b3792ef..17fde8d 100644 --- a/src/core/display/Container.js +++ b/src/core/display/Container.js @@ -73,13 +73,14 @@ } child.parent = this; - - // ensure a transform will be recalculated.. - this.transform._parentID = -1; - this._boundsID++; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.push(child); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(this.children.length - 1); child.emit('added', this); @@ -108,9 +109,14 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.splice(index, 0, child); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(index); child.emit('added', this); @@ -174,6 +180,7 @@ removeItems(this.children, currentIndex, 1); // remove from old position this.children.splice(index, 0, child); // add at new position + this.onChildrenChange(index); } @@ -220,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! @@ -244,9 +252,14 @@ { const child = this.getChildAt(index); + // ensure child transform will be recalculated.. child.parent = null; + child.transform._parentID = -1; removeItems(this.children, index, 1); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(index); child.emit('removed', this); @@ -275,8 +288,14 @@ for (let i = 0; i < removed.length; ++i) { removed[i].parent = null; + if (removed[i].transform) + { + removed[i].transform._parentID = -1; + } } + this._boundsID++; + this.onChildrenChange(beginIndex); for (let i = 0; i < removed.length; ++i) diff --git a/src/core/display/DisplayObject.js b/src/core/display/DisplayObject.js index 3affe36..4e6c77d 100644 --- a/src/core/display/DisplayObject.js +++ b/src/core/display/DisplayObject.js @@ -109,7 +109,7 @@ /** * The original, cached mask of the object * - * @member {PIXI.Rectangle} + * @member {PIXI.Graphics|PIXI.Sprite} * @private */ this._mask = null; diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index 95f2af4..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 ); @@ -1065,10 +1061,15 @@ canvasRenderer = new CanvasRenderer(); } - tempMatrix.tx = -bounds.x; - tempMatrix.ty = -bounds.y; + this.transform.updateLocalTransform(); + this.transform.localTransform.copy(tempMatrix); - canvasRenderer.render(this, canvasBuffer, false, tempMatrix); + tempMatrix.invert(); + + tempMatrix.tx -= bounds.x; + tempMatrix.ty -= bounds.y; + + canvasRenderer.render(this, canvasBuffer, true, tempMatrix); const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode); 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 1271fef..71ade15 100644 --- a/package.json +++ b/package.json @@ -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/scripts/release.js b/scripts/release.js index c4c8e36..156620c 100644 --- a/scripts/release.js +++ b/scripts/release.js @@ -12,7 +12,8 @@ 'dist/**/*', 'lib/**/*', 'src/**/*', - 'scripts/**/*', + 'scripts/*', + 'scripts/renders/*', 'test/**/*', '*.json', '*.md', diff --git a/scripts/renders/client.js b/scripts/renders/client.js index 16c9565..6faee34 100755 --- a/scripts/renders/client.js +++ b/scripts/renders/client.js @@ -1,6 +1,6 @@ 'use strict'; -require('../../bin/pixi'); +require('../../dist/pixi'); PIXI.utils.skipHello(); const fs = require('fs'); 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 bc730db..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. @@ -33,8 +33,11 @@ * need to call toDataUrl on the webgl context * @param {number} [options.resolution=1] - The resolution / device pixel ratio of the renderer, retina would be 2 * @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 @@ -43,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. */ @@ -73,7 +99,7 @@ */ stop() { - this.ticker.stop(); + this._ticker.stop(); } /** @@ -81,7 +107,7 @@ */ start() { - this.ticker.start(); + this._ticker.start(); } /** @@ -95,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 index 81e076b..78b17e6 100644 --- a/src/core/Shader.js +++ b/src/core/Shader.js @@ -1,9 +1,7 @@ import { GLShader } from 'pixi-gl-core'; import settings from './settings'; -const { PRECISION } = settings; - -function checkPrecision(src) +function checkPrecision(src, def) { if (src instanceof Array) { @@ -11,14 +9,14 @@ { const copy = src.slice(0); - copy.unshift(`precision ${PRECISION} float;`); + copy.unshift(`precision ${def} float;`); return copy; } } else if (src.substring(0, 9) !== 'precision') { - return `precision ${PRECISION} float;\n${src}`; + return `precision ${def} float;\n${src}`; } return src; @@ -42,6 +40,7 @@ */ constructor(gl, vertexSrc, fragmentSrc) { - super(gl, checkPrecision(vertexSrc), checkPrecision(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 3425b41..f9235e4 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -181,7 +181,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 b3792ef..17fde8d 100644 --- a/src/core/display/Container.js +++ b/src/core/display/Container.js @@ -73,13 +73,14 @@ } child.parent = this; - - // ensure a transform will be recalculated.. - this.transform._parentID = -1; - this._boundsID++; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.push(child); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(this.children.length - 1); child.emit('added', this); @@ -108,9 +109,14 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.splice(index, 0, child); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(index); child.emit('added', this); @@ -174,6 +180,7 @@ removeItems(this.children, currentIndex, 1); // remove from old position this.children.splice(index, 0, child); // add at new position + this.onChildrenChange(index); } @@ -220,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! @@ -244,9 +252,14 @@ { const child = this.getChildAt(index); + // ensure child transform will be recalculated.. child.parent = null; + child.transform._parentID = -1; removeItems(this.children, index, 1); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(index); child.emit('removed', this); @@ -275,8 +288,14 @@ for (let i = 0; i < removed.length; ++i) { removed[i].parent = null; + if (removed[i].transform) + { + removed[i].transform._parentID = -1; + } } + this._boundsID++; + this.onChildrenChange(beginIndex); for (let i = 0; i < removed.length; ++i) diff --git a/src/core/display/DisplayObject.js b/src/core/display/DisplayObject.js index 3affe36..4e6c77d 100644 --- a/src/core/display/DisplayObject.js +++ b/src/core/display/DisplayObject.js @@ -109,7 +109,7 @@ /** * The original, cached mask of the object * - * @member {PIXI.Rectangle} + * @member {PIXI.Graphics|PIXI.Sprite} * @private */ this._mask = null; diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index 95f2af4..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 ); @@ -1065,10 +1061,15 @@ canvasRenderer = new CanvasRenderer(); } - tempMatrix.tx = -bounds.x; - tempMatrix.ty = -bounds.y; + this.transform.updateLocalTransform(); + this.transform.localTransform.copy(tempMatrix); - canvasRenderer.render(this, canvasBuffer, false, tempMatrix); + tempMatrix.invert(); + + tempMatrix.tx -= bounds.x; + tempMatrix.ty -= bounds.y; + + canvasRenderer.render(this, canvasBuffer, true, tempMatrix); const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode); 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 e67680f..867dde2 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 1271fef..71ade15 100644 --- a/package.json +++ b/package.json @@ -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/scripts/release.js b/scripts/release.js index c4c8e36..156620c 100644 --- a/scripts/release.js +++ b/scripts/release.js @@ -12,7 +12,8 @@ 'dist/**/*', 'lib/**/*', 'src/**/*', - 'scripts/**/*', + 'scripts/*', + 'scripts/renders/*', 'test/**/*', '*.json', '*.md', diff --git a/scripts/renders/client.js b/scripts/renders/client.js index 16c9565..6faee34 100755 --- a/scripts/renders/client.js +++ b/scripts/renders/client.js @@ -1,6 +1,6 @@ 'use strict'; -require('../../bin/pixi'); +require('../../dist/pixi'); PIXI.utils.skipHello(); const fs = require('fs'); 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 bc730db..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. @@ -33,8 +33,11 @@ * need to call toDataUrl on the webgl context * @param {number} [options.resolution=1] - The resolution / device pixel ratio of the renderer, retina would be 2 * @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 @@ -43,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. */ @@ -73,7 +99,7 @@ */ stop() { - this.ticker.stop(); + this._ticker.stop(); } /** @@ -81,7 +107,7 @@ */ start() { - this.ticker.start(); + this._ticker.start(); } /** @@ -95,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 index 81e076b..78b17e6 100644 --- a/src/core/Shader.js +++ b/src/core/Shader.js @@ -1,9 +1,7 @@ import { GLShader } from 'pixi-gl-core'; import settings from './settings'; -const { PRECISION } = settings; - -function checkPrecision(src) +function checkPrecision(src, def) { if (src instanceof Array) { @@ -11,14 +9,14 @@ { const copy = src.slice(0); - copy.unshift(`precision ${PRECISION} float;`); + copy.unshift(`precision ${def} float;`); return copy; } } else if (src.substring(0, 9) !== 'precision') { - return `precision ${PRECISION} float;\n${src}`; + return `precision ${def} float;\n${src}`; } return src; @@ -42,6 +40,7 @@ */ constructor(gl, vertexSrc, fragmentSrc) { - super(gl, checkPrecision(vertexSrc), checkPrecision(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 3425b41..f9235e4 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -181,7 +181,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 b3792ef..17fde8d 100644 --- a/src/core/display/Container.js +++ b/src/core/display/Container.js @@ -73,13 +73,14 @@ } child.parent = this; - - // ensure a transform will be recalculated.. - this.transform._parentID = -1; - this._boundsID++; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.push(child); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(this.children.length - 1); child.emit('added', this); @@ -108,9 +109,14 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.splice(index, 0, child); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(index); child.emit('added', this); @@ -174,6 +180,7 @@ removeItems(this.children, currentIndex, 1); // remove from old position this.children.splice(index, 0, child); // add at new position + this.onChildrenChange(index); } @@ -220,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! @@ -244,9 +252,14 @@ { const child = this.getChildAt(index); + // ensure child transform will be recalculated.. child.parent = null; + child.transform._parentID = -1; removeItems(this.children, index, 1); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(index); child.emit('removed', this); @@ -275,8 +288,14 @@ for (let i = 0; i < removed.length; ++i) { removed[i].parent = null; + if (removed[i].transform) + { + removed[i].transform._parentID = -1; + } } + this._boundsID++; + this.onChildrenChange(beginIndex); for (let i = 0; i < removed.length; ++i) diff --git a/src/core/display/DisplayObject.js b/src/core/display/DisplayObject.js index 3affe36..4e6c77d 100644 --- a/src/core/display/DisplayObject.js +++ b/src/core/display/DisplayObject.js @@ -109,7 +109,7 @@ /** * The original, cached mask of the object * - * @member {PIXI.Rectangle} + * @member {PIXI.Graphics|PIXI.Sprite} * @private */ this._mask = null; diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index 95f2af4..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 ); @@ -1065,10 +1061,15 @@ canvasRenderer = new CanvasRenderer(); } - tempMatrix.tx = -bounds.x; - tempMatrix.ty = -bounds.y; + this.transform.updateLocalTransform(); + this.transform.localTransform.copy(tempMatrix); - canvasRenderer.render(this, canvasBuffer, false, tempMatrix); + tempMatrix.invert(); + + tempMatrix.tx -= bounds.x; + tempMatrix.ty -= bounds.y; + + canvasRenderer.render(this, canvasBuffer, true, tempMatrix); const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode); 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 e67680f..867dde2 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/buildCircle.js b/src/core/graphics/webgl/utils/buildCircle.js index 0400d13..abf5d02 100644 --- a/src/core/graphics/webgl/utils/buildCircle.js +++ b/src/core/graphics/webgl/utils/buildCircle.js @@ -33,6 +33,11 @@ height = circleData.height; } + if (width === 0 || height === 0) + { + return; + } + const totalSegs = Math.floor(30 * Math.sqrt(circleData.radius)) || Math.floor(15 * Math.sqrt(circleData.width + circleData.height)); diff --git a/package.json b/package.json index 1271fef..71ade15 100644 --- a/package.json +++ b/package.json @@ -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/scripts/release.js b/scripts/release.js index c4c8e36..156620c 100644 --- a/scripts/release.js +++ b/scripts/release.js @@ -12,7 +12,8 @@ 'dist/**/*', 'lib/**/*', 'src/**/*', - 'scripts/**/*', + 'scripts/*', + 'scripts/renders/*', 'test/**/*', '*.json', '*.md', diff --git a/scripts/renders/client.js b/scripts/renders/client.js index 16c9565..6faee34 100755 --- a/scripts/renders/client.js +++ b/scripts/renders/client.js @@ -1,6 +1,6 @@ 'use strict'; -require('../../bin/pixi'); +require('../../dist/pixi'); PIXI.utils.skipHello(); const fs = require('fs'); 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 bc730db..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. @@ -33,8 +33,11 @@ * need to call toDataUrl on the webgl context * @param {number} [options.resolution=1] - The resolution / device pixel ratio of the renderer, retina would be 2 * @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 @@ -43,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. */ @@ -73,7 +99,7 @@ */ stop() { - this.ticker.stop(); + this._ticker.stop(); } /** @@ -81,7 +107,7 @@ */ start() { - this.ticker.start(); + this._ticker.start(); } /** @@ -95,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 index 81e076b..78b17e6 100644 --- a/src/core/Shader.js +++ b/src/core/Shader.js @@ -1,9 +1,7 @@ import { GLShader } from 'pixi-gl-core'; import settings from './settings'; -const { PRECISION } = settings; - -function checkPrecision(src) +function checkPrecision(src, def) { if (src instanceof Array) { @@ -11,14 +9,14 @@ { const copy = src.slice(0); - copy.unshift(`precision ${PRECISION} float;`); + copy.unshift(`precision ${def} float;`); return copy; } } else if (src.substring(0, 9) !== 'precision') { - return `precision ${PRECISION} float;\n${src}`; + return `precision ${def} float;\n${src}`; } return src; @@ -42,6 +40,7 @@ */ constructor(gl, vertexSrc, fragmentSrc) { - super(gl, checkPrecision(vertexSrc), checkPrecision(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 3425b41..f9235e4 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -181,7 +181,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 b3792ef..17fde8d 100644 --- a/src/core/display/Container.js +++ b/src/core/display/Container.js @@ -73,13 +73,14 @@ } child.parent = this; - - // ensure a transform will be recalculated.. - this.transform._parentID = -1; - this._boundsID++; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.push(child); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(this.children.length - 1); child.emit('added', this); @@ -108,9 +109,14 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.splice(index, 0, child); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(index); child.emit('added', this); @@ -174,6 +180,7 @@ removeItems(this.children, currentIndex, 1); // remove from old position this.children.splice(index, 0, child); // add at new position + this.onChildrenChange(index); } @@ -220,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! @@ -244,9 +252,14 @@ { const child = this.getChildAt(index); + // ensure child transform will be recalculated.. child.parent = null; + child.transform._parentID = -1; removeItems(this.children, index, 1); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(index); child.emit('removed', this); @@ -275,8 +288,14 @@ for (let i = 0; i < removed.length; ++i) { removed[i].parent = null; + if (removed[i].transform) + { + removed[i].transform._parentID = -1; + } } + this._boundsID++; + this.onChildrenChange(beginIndex); for (let i = 0; i < removed.length; ++i) diff --git a/src/core/display/DisplayObject.js b/src/core/display/DisplayObject.js index 3affe36..4e6c77d 100644 --- a/src/core/display/DisplayObject.js +++ b/src/core/display/DisplayObject.js @@ -109,7 +109,7 @@ /** * The original, cached mask of the object * - * @member {PIXI.Rectangle} + * @member {PIXI.Graphics|PIXI.Sprite} * @private */ this._mask = null; diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index 95f2af4..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 ); @@ -1065,10 +1061,15 @@ canvasRenderer = new CanvasRenderer(); } - tempMatrix.tx = -bounds.x; - tempMatrix.ty = -bounds.y; + this.transform.updateLocalTransform(); + this.transform.localTransform.copy(tempMatrix); - canvasRenderer.render(this, canvasBuffer, false, tempMatrix); + tempMatrix.invert(); + + tempMatrix.tx -= bounds.x; + tempMatrix.ty -= bounds.y; + + canvasRenderer.render(this, canvasBuffer, true, tempMatrix); const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode); 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 e67680f..867dde2 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/buildCircle.js b/src/core/graphics/webgl/utils/buildCircle.js index 0400d13..abf5d02 100644 --- a/src/core/graphics/webgl/utils/buildCircle.js +++ b/src/core/graphics/webgl/utils/buildCircle.js @@ -33,6 +33,11 @@ height = circleData.height; } + if (width === 0 || height === 0) + { + return; + } + const totalSegs = Math.floor(30 * Math.sqrt(circleData.radius)) || Math.floor(15 * Math.sqrt(circleData.width + circleData.height)); diff --git a/src/core/graphics/webgl/utils/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 1271fef..71ade15 100644 --- a/package.json +++ b/package.json @@ -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/scripts/release.js b/scripts/release.js index c4c8e36..156620c 100644 --- a/scripts/release.js +++ b/scripts/release.js @@ -12,7 +12,8 @@ 'dist/**/*', 'lib/**/*', 'src/**/*', - 'scripts/**/*', + 'scripts/*', + 'scripts/renders/*', 'test/**/*', '*.json', '*.md', diff --git a/scripts/renders/client.js b/scripts/renders/client.js index 16c9565..6faee34 100755 --- a/scripts/renders/client.js +++ b/scripts/renders/client.js @@ -1,6 +1,6 @@ 'use strict'; -require('../../bin/pixi'); +require('../../dist/pixi'); PIXI.utils.skipHello(); const fs = require('fs'); 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 bc730db..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. @@ -33,8 +33,11 @@ * need to call toDataUrl on the webgl context * @param {number} [options.resolution=1] - The resolution / device pixel ratio of the renderer, retina would be 2 * @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 @@ -43,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. */ @@ -73,7 +99,7 @@ */ stop() { - this.ticker.stop(); + this._ticker.stop(); } /** @@ -81,7 +107,7 @@ */ start() { - this.ticker.start(); + this._ticker.start(); } /** @@ -95,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 index 81e076b..78b17e6 100644 --- a/src/core/Shader.js +++ b/src/core/Shader.js @@ -1,9 +1,7 @@ import { GLShader } from 'pixi-gl-core'; import settings from './settings'; -const { PRECISION } = settings; - -function checkPrecision(src) +function checkPrecision(src, def) { if (src instanceof Array) { @@ -11,14 +9,14 @@ { const copy = src.slice(0); - copy.unshift(`precision ${PRECISION} float;`); + copy.unshift(`precision ${def} float;`); return copy; } } else if (src.substring(0, 9) !== 'precision') { - return `precision ${PRECISION} float;\n${src}`; + return `precision ${def} float;\n${src}`; } return src; @@ -42,6 +40,7 @@ */ constructor(gl, vertexSrc, fragmentSrc) { - super(gl, checkPrecision(vertexSrc), checkPrecision(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 3425b41..f9235e4 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -181,7 +181,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 b3792ef..17fde8d 100644 --- a/src/core/display/Container.js +++ b/src/core/display/Container.js @@ -73,13 +73,14 @@ } child.parent = this; - - // ensure a transform will be recalculated.. - this.transform._parentID = -1; - this._boundsID++; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.push(child); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(this.children.length - 1); child.emit('added', this); @@ -108,9 +109,14 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.splice(index, 0, child); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(index); child.emit('added', this); @@ -174,6 +180,7 @@ removeItems(this.children, currentIndex, 1); // remove from old position this.children.splice(index, 0, child); // add at new position + this.onChildrenChange(index); } @@ -220,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! @@ -244,9 +252,14 @@ { const child = this.getChildAt(index); + // ensure child transform will be recalculated.. child.parent = null; + child.transform._parentID = -1; removeItems(this.children, index, 1); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(index); child.emit('removed', this); @@ -275,8 +288,14 @@ for (let i = 0; i < removed.length; ++i) { removed[i].parent = null; + if (removed[i].transform) + { + removed[i].transform._parentID = -1; + } } + this._boundsID++; + this.onChildrenChange(beginIndex); for (let i = 0; i < removed.length; ++i) diff --git a/src/core/display/DisplayObject.js b/src/core/display/DisplayObject.js index 3affe36..4e6c77d 100644 --- a/src/core/display/DisplayObject.js +++ b/src/core/display/DisplayObject.js @@ -109,7 +109,7 @@ /** * The original, cached mask of the object * - * @member {PIXI.Rectangle} + * @member {PIXI.Graphics|PIXI.Sprite} * @private */ this._mask = null; diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index 95f2af4..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 ); @@ -1065,10 +1061,15 @@ canvasRenderer = new CanvasRenderer(); } - tempMatrix.tx = -bounds.x; - tempMatrix.ty = -bounds.y; + this.transform.updateLocalTransform(); + this.transform.localTransform.copy(tempMatrix); - canvasRenderer.render(this, canvasBuffer, false, tempMatrix); + tempMatrix.invert(); + + tempMatrix.tx -= bounds.x; + tempMatrix.ty -= bounds.y; + + canvasRenderer.render(this, canvasBuffer, true, tempMatrix); const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode); 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 e67680f..867dde2 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/buildCircle.js b/src/core/graphics/webgl/utils/buildCircle.js index 0400d13..abf5d02 100644 --- a/src/core/graphics/webgl/utils/buildCircle.js +++ b/src/core/graphics/webgl/utils/buildCircle.js @@ -33,6 +33,11 @@ height = circleData.height; } + if (width === 0 || height === 0) + { + return; + } + const totalSegs = Math.floor(30 * Math.sqrt(circleData.radius)) || Math.floor(15 * Math.sqrt(circleData.width + circleData.height)); diff --git a/src/core/graphics/webgl/utils/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 1271fef..71ade15 100644 --- a/package.json +++ b/package.json @@ -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/scripts/release.js b/scripts/release.js index c4c8e36..156620c 100644 --- a/scripts/release.js +++ b/scripts/release.js @@ -12,7 +12,8 @@ 'dist/**/*', 'lib/**/*', 'src/**/*', - 'scripts/**/*', + 'scripts/*', + 'scripts/renders/*', 'test/**/*', '*.json', '*.md', diff --git a/scripts/renders/client.js b/scripts/renders/client.js index 16c9565..6faee34 100755 --- a/scripts/renders/client.js +++ b/scripts/renders/client.js @@ -1,6 +1,6 @@ 'use strict'; -require('../../bin/pixi'); +require('../../dist/pixi'); PIXI.utils.skipHello(); const fs = require('fs'); 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 bc730db..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. @@ -33,8 +33,11 @@ * need to call toDataUrl on the webgl context * @param {number} [options.resolution=1] - The resolution / device pixel ratio of the renderer, retina would be 2 * @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 @@ -43,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. */ @@ -73,7 +99,7 @@ */ stop() { - this.ticker.stop(); + this._ticker.stop(); } /** @@ -81,7 +107,7 @@ */ start() { - this.ticker.start(); + this._ticker.start(); } /** @@ -95,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 index 81e076b..78b17e6 100644 --- a/src/core/Shader.js +++ b/src/core/Shader.js @@ -1,9 +1,7 @@ import { GLShader } from 'pixi-gl-core'; import settings from './settings'; -const { PRECISION } = settings; - -function checkPrecision(src) +function checkPrecision(src, def) { if (src instanceof Array) { @@ -11,14 +9,14 @@ { const copy = src.slice(0); - copy.unshift(`precision ${PRECISION} float;`); + copy.unshift(`precision ${def} float;`); return copy; } } else if (src.substring(0, 9) !== 'precision') { - return `precision ${PRECISION} float;\n${src}`; + return `precision ${def} float;\n${src}`; } return src; @@ -42,6 +40,7 @@ */ constructor(gl, vertexSrc, fragmentSrc) { - super(gl, checkPrecision(vertexSrc), checkPrecision(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 3425b41..f9235e4 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -181,7 +181,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 b3792ef..17fde8d 100644 --- a/src/core/display/Container.js +++ b/src/core/display/Container.js @@ -73,13 +73,14 @@ } child.parent = this; - - // ensure a transform will be recalculated.. - this.transform._parentID = -1; - this._boundsID++; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.push(child); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(this.children.length - 1); child.emit('added', this); @@ -108,9 +109,14 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.splice(index, 0, child); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(index); child.emit('added', this); @@ -174,6 +180,7 @@ removeItems(this.children, currentIndex, 1); // remove from old position this.children.splice(index, 0, child); // add at new position + this.onChildrenChange(index); } @@ -220,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! @@ -244,9 +252,14 @@ { const child = this.getChildAt(index); + // ensure child transform will be recalculated.. child.parent = null; + child.transform._parentID = -1; removeItems(this.children, index, 1); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(index); child.emit('removed', this); @@ -275,8 +288,14 @@ for (let i = 0; i < removed.length; ++i) { removed[i].parent = null; + if (removed[i].transform) + { + removed[i].transform._parentID = -1; + } } + this._boundsID++; + this.onChildrenChange(beginIndex); for (let i = 0; i < removed.length; ++i) diff --git a/src/core/display/DisplayObject.js b/src/core/display/DisplayObject.js index 3affe36..4e6c77d 100644 --- a/src/core/display/DisplayObject.js +++ b/src/core/display/DisplayObject.js @@ -109,7 +109,7 @@ /** * The original, cached mask of the object * - * @member {PIXI.Rectangle} + * @member {PIXI.Graphics|PIXI.Sprite} * @private */ this._mask = null; diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index 95f2af4..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 ); @@ -1065,10 +1061,15 @@ canvasRenderer = new CanvasRenderer(); } - tempMatrix.tx = -bounds.x; - tempMatrix.ty = -bounds.y; + this.transform.updateLocalTransform(); + this.transform.localTransform.copy(tempMatrix); - canvasRenderer.render(this, canvasBuffer, false, tempMatrix); + tempMatrix.invert(); + + tempMatrix.tx -= bounds.x; + tempMatrix.ty -= bounds.y; + + canvasRenderer.render(this, canvasBuffer, true, tempMatrix); const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode); 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 e67680f..867dde2 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/buildCircle.js b/src/core/graphics/webgl/utils/buildCircle.js index 0400d13..abf5d02 100644 --- a/src/core/graphics/webgl/utils/buildCircle.js +++ b/src/core/graphics/webgl/utils/buildCircle.js @@ -33,6 +33,11 @@ height = circleData.height; } + if (width === 0 || height === 0) + { + return; + } + const totalSegs = Math.floor(30 * Math.sqrt(circleData.radius)) || Math.floor(15 * Math.sqrt(circleData.width + circleData.height)); diff --git a/src/core/graphics/webgl/utils/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 90155f9..f03fe15 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -30,6 +30,7 @@ export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; export { default as CanvasGraphicsRenderer } from './graphics/canvas/CanvasGraphicsRenderer'; +export { default as Spritesheet } from './textures/Spritesheet'; export { default as Texture } from './textures/Texture'; export { default as BaseTexture } from './textures/BaseTexture'; export { default as RenderTexture } from './textures/RenderTexture'; diff --git a/package.json b/package.json index 1271fef..71ade15 100644 --- a/package.json +++ b/package.json @@ -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/scripts/release.js b/scripts/release.js index c4c8e36..156620c 100644 --- a/scripts/release.js +++ b/scripts/release.js @@ -12,7 +12,8 @@ 'dist/**/*', 'lib/**/*', 'src/**/*', - 'scripts/**/*', + 'scripts/*', + 'scripts/renders/*', 'test/**/*', '*.json', '*.md', diff --git a/scripts/renders/client.js b/scripts/renders/client.js index 16c9565..6faee34 100755 --- a/scripts/renders/client.js +++ b/scripts/renders/client.js @@ -1,6 +1,6 @@ 'use strict'; -require('../../bin/pixi'); +require('../../dist/pixi'); PIXI.utils.skipHello(); const fs = require('fs'); 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 bc730db..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. @@ -33,8 +33,11 @@ * need to call toDataUrl on the webgl context * @param {number} [options.resolution=1] - The resolution / device pixel ratio of the renderer, retina would be 2 * @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 @@ -43,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. */ @@ -73,7 +99,7 @@ */ stop() { - this.ticker.stop(); + this._ticker.stop(); } /** @@ -81,7 +107,7 @@ */ start() { - this.ticker.start(); + this._ticker.start(); } /** @@ -95,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 index 81e076b..78b17e6 100644 --- a/src/core/Shader.js +++ b/src/core/Shader.js @@ -1,9 +1,7 @@ import { GLShader } from 'pixi-gl-core'; import settings from './settings'; -const { PRECISION } = settings; - -function checkPrecision(src) +function checkPrecision(src, def) { if (src instanceof Array) { @@ -11,14 +9,14 @@ { const copy = src.slice(0); - copy.unshift(`precision ${PRECISION} float;`); + copy.unshift(`precision ${def} float;`); return copy; } } else if (src.substring(0, 9) !== 'precision') { - return `precision ${PRECISION} float;\n${src}`; + return `precision ${def} float;\n${src}`; } return src; @@ -42,6 +40,7 @@ */ constructor(gl, vertexSrc, fragmentSrc) { - super(gl, checkPrecision(vertexSrc), checkPrecision(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 3425b41..f9235e4 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -181,7 +181,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 b3792ef..17fde8d 100644 --- a/src/core/display/Container.js +++ b/src/core/display/Container.js @@ -73,13 +73,14 @@ } child.parent = this; - - // ensure a transform will be recalculated.. - this.transform._parentID = -1; - this._boundsID++; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.push(child); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(this.children.length - 1); child.emit('added', this); @@ -108,9 +109,14 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.splice(index, 0, child); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(index); child.emit('added', this); @@ -174,6 +180,7 @@ removeItems(this.children, currentIndex, 1); // remove from old position this.children.splice(index, 0, child); // add at new position + this.onChildrenChange(index); } @@ -220,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! @@ -244,9 +252,14 @@ { const child = this.getChildAt(index); + // ensure child transform will be recalculated.. child.parent = null; + child.transform._parentID = -1; removeItems(this.children, index, 1); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(index); child.emit('removed', this); @@ -275,8 +288,14 @@ for (let i = 0; i < removed.length; ++i) { removed[i].parent = null; + if (removed[i].transform) + { + removed[i].transform._parentID = -1; + } } + this._boundsID++; + this.onChildrenChange(beginIndex); for (let i = 0; i < removed.length; ++i) diff --git a/src/core/display/DisplayObject.js b/src/core/display/DisplayObject.js index 3affe36..4e6c77d 100644 --- a/src/core/display/DisplayObject.js +++ b/src/core/display/DisplayObject.js @@ -109,7 +109,7 @@ /** * The original, cached mask of the object * - * @member {PIXI.Rectangle} + * @member {PIXI.Graphics|PIXI.Sprite} * @private */ this._mask = null; diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index 95f2af4..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 ); @@ -1065,10 +1061,15 @@ canvasRenderer = new CanvasRenderer(); } - tempMatrix.tx = -bounds.x; - tempMatrix.ty = -bounds.y; + this.transform.updateLocalTransform(); + this.transform.localTransform.copy(tempMatrix); - canvasRenderer.render(this, canvasBuffer, false, tempMatrix); + tempMatrix.invert(); + + tempMatrix.tx -= bounds.x; + tempMatrix.ty -= bounds.y; + + canvasRenderer.render(this, canvasBuffer, true, tempMatrix); const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode); 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 e67680f..867dde2 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/buildCircle.js b/src/core/graphics/webgl/utils/buildCircle.js index 0400d13..abf5d02 100644 --- a/src/core/graphics/webgl/utils/buildCircle.js +++ b/src/core/graphics/webgl/utils/buildCircle.js @@ -33,6 +33,11 @@ height = circleData.height; } + if (width === 0 || height === 0) + { + return; + } + const totalSegs = Math.floor(30 * Math.sqrt(circleData.radius)) || Math.floor(15 * Math.sqrt(circleData.width + circleData.height)); diff --git a/src/core/graphics/webgl/utils/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 90155f9..f03fe15 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -30,6 +30,7 @@ export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; export { default as CanvasGraphicsRenderer } from './graphics/canvas/CanvasGraphicsRenderer'; +export { default as Spritesheet } from './textures/Spritesheet'; export { default as Texture } from './textures/Texture'; export { default as BaseTexture } from './textures/BaseTexture'; export { default as RenderTexture } from './textures/RenderTexture'; diff --git a/src/core/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 1271fef..71ade15 100644 --- a/package.json +++ b/package.json @@ -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/scripts/release.js b/scripts/release.js index c4c8e36..156620c 100644 --- a/scripts/release.js +++ b/scripts/release.js @@ -12,7 +12,8 @@ 'dist/**/*', 'lib/**/*', 'src/**/*', - 'scripts/**/*', + 'scripts/*', + 'scripts/renders/*', 'test/**/*', '*.json', '*.md', diff --git a/scripts/renders/client.js b/scripts/renders/client.js index 16c9565..6faee34 100755 --- a/scripts/renders/client.js +++ b/scripts/renders/client.js @@ -1,6 +1,6 @@ 'use strict'; -require('../../bin/pixi'); +require('../../dist/pixi'); PIXI.utils.skipHello(); const fs = require('fs'); 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 bc730db..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. @@ -33,8 +33,11 @@ * need to call toDataUrl on the webgl context * @param {number} [options.resolution=1] - The resolution / device pixel ratio of the renderer, retina would be 2 * @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 @@ -43,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. */ @@ -73,7 +99,7 @@ */ stop() { - this.ticker.stop(); + this._ticker.stop(); } /** @@ -81,7 +107,7 @@ */ start() { - this.ticker.start(); + this._ticker.start(); } /** @@ -95,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 index 81e076b..78b17e6 100644 --- a/src/core/Shader.js +++ b/src/core/Shader.js @@ -1,9 +1,7 @@ import { GLShader } from 'pixi-gl-core'; import settings from './settings'; -const { PRECISION } = settings; - -function checkPrecision(src) +function checkPrecision(src, def) { if (src instanceof Array) { @@ -11,14 +9,14 @@ { const copy = src.slice(0); - copy.unshift(`precision ${PRECISION} float;`); + copy.unshift(`precision ${def} float;`); return copy; } } else if (src.substring(0, 9) !== 'precision') { - return `precision ${PRECISION} float;\n${src}`; + return `precision ${def} float;\n${src}`; } return src; @@ -42,6 +40,7 @@ */ constructor(gl, vertexSrc, fragmentSrc) { - super(gl, checkPrecision(vertexSrc), checkPrecision(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 3425b41..f9235e4 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -181,7 +181,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 b3792ef..17fde8d 100644 --- a/src/core/display/Container.js +++ b/src/core/display/Container.js @@ -73,13 +73,14 @@ } child.parent = this; - - // ensure a transform will be recalculated.. - this.transform._parentID = -1; - this._boundsID++; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.push(child); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(this.children.length - 1); child.emit('added', this); @@ -108,9 +109,14 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.splice(index, 0, child); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(index); child.emit('added', this); @@ -174,6 +180,7 @@ removeItems(this.children, currentIndex, 1); // remove from old position this.children.splice(index, 0, child); // add at new position + this.onChildrenChange(index); } @@ -220,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! @@ -244,9 +252,14 @@ { const child = this.getChildAt(index); + // ensure child transform will be recalculated.. child.parent = null; + child.transform._parentID = -1; removeItems(this.children, index, 1); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(index); child.emit('removed', this); @@ -275,8 +288,14 @@ for (let i = 0; i < removed.length; ++i) { removed[i].parent = null; + if (removed[i].transform) + { + removed[i].transform._parentID = -1; + } } + this._boundsID++; + this.onChildrenChange(beginIndex); for (let i = 0; i < removed.length; ++i) diff --git a/src/core/display/DisplayObject.js b/src/core/display/DisplayObject.js index 3affe36..4e6c77d 100644 --- a/src/core/display/DisplayObject.js +++ b/src/core/display/DisplayObject.js @@ -109,7 +109,7 @@ /** * The original, cached mask of the object * - * @member {PIXI.Rectangle} + * @member {PIXI.Graphics|PIXI.Sprite} * @private */ this._mask = null; diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index 95f2af4..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 ); @@ -1065,10 +1061,15 @@ canvasRenderer = new CanvasRenderer(); } - tempMatrix.tx = -bounds.x; - tempMatrix.ty = -bounds.y; + this.transform.updateLocalTransform(); + this.transform.localTransform.copy(tempMatrix); - canvasRenderer.render(this, canvasBuffer, false, tempMatrix); + tempMatrix.invert(); + + tempMatrix.tx -= bounds.x; + tempMatrix.ty -= bounds.y; + + canvasRenderer.render(this, canvasBuffer, true, tempMatrix); const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode); 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 e67680f..867dde2 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/buildCircle.js b/src/core/graphics/webgl/utils/buildCircle.js index 0400d13..abf5d02 100644 --- a/src/core/graphics/webgl/utils/buildCircle.js +++ b/src/core/graphics/webgl/utils/buildCircle.js @@ -33,6 +33,11 @@ height = circleData.height; } + if (width === 0 || height === 0) + { + return; + } + const totalSegs = Math.floor(30 * Math.sqrt(circleData.radius)) || Math.floor(15 * Math.sqrt(circleData.width + circleData.height)); diff --git a/src/core/graphics/webgl/utils/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 90155f9..f03fe15 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -30,6 +30,7 @@ export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; export { default as CanvasGraphicsRenderer } from './graphics/canvas/CanvasGraphicsRenderer'; +export { default as Spritesheet } from './textures/Spritesheet'; export { default as Texture } from './textures/Texture'; export { default as BaseTexture } from './textures/BaseTexture'; export { default as RenderTexture } from './textures/RenderTexture'; diff --git a/src/core/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 1271fef..71ade15 100644 --- a/package.json +++ b/package.json @@ -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/scripts/release.js b/scripts/release.js index c4c8e36..156620c 100644 --- a/scripts/release.js +++ b/scripts/release.js @@ -12,7 +12,8 @@ 'dist/**/*', 'lib/**/*', 'src/**/*', - 'scripts/**/*', + 'scripts/*', + 'scripts/renders/*', 'test/**/*', '*.json', '*.md', diff --git a/scripts/renders/client.js b/scripts/renders/client.js index 16c9565..6faee34 100755 --- a/scripts/renders/client.js +++ b/scripts/renders/client.js @@ -1,6 +1,6 @@ 'use strict'; -require('../../bin/pixi'); +require('../../dist/pixi'); PIXI.utils.skipHello(); const fs = require('fs'); 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 bc730db..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. @@ -33,8 +33,11 @@ * need to call toDataUrl on the webgl context * @param {number} [options.resolution=1] - The resolution / device pixel ratio of the renderer, retina would be 2 * @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 @@ -43,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. */ @@ -73,7 +99,7 @@ */ stop() { - this.ticker.stop(); + this._ticker.stop(); } /** @@ -81,7 +107,7 @@ */ start() { - this.ticker.start(); + this._ticker.start(); } /** @@ -95,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 index 81e076b..78b17e6 100644 --- a/src/core/Shader.js +++ b/src/core/Shader.js @@ -1,9 +1,7 @@ import { GLShader } from 'pixi-gl-core'; import settings from './settings'; -const { PRECISION } = settings; - -function checkPrecision(src) +function checkPrecision(src, def) { if (src instanceof Array) { @@ -11,14 +9,14 @@ { const copy = src.slice(0); - copy.unshift(`precision ${PRECISION} float;`); + copy.unshift(`precision ${def} float;`); return copy; } } else if (src.substring(0, 9) !== 'precision') { - return `precision ${PRECISION} float;\n${src}`; + return `precision ${def} float;\n${src}`; } return src; @@ -42,6 +40,7 @@ */ constructor(gl, vertexSrc, fragmentSrc) { - super(gl, checkPrecision(vertexSrc), checkPrecision(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 3425b41..f9235e4 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -181,7 +181,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 b3792ef..17fde8d 100644 --- a/src/core/display/Container.js +++ b/src/core/display/Container.js @@ -73,13 +73,14 @@ } child.parent = this; - - // ensure a transform will be recalculated.. - this.transform._parentID = -1; - this._boundsID++; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.push(child); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(this.children.length - 1); child.emit('added', this); @@ -108,9 +109,14 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.splice(index, 0, child); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(index); child.emit('added', this); @@ -174,6 +180,7 @@ removeItems(this.children, currentIndex, 1); // remove from old position this.children.splice(index, 0, child); // add at new position + this.onChildrenChange(index); } @@ -220,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! @@ -244,9 +252,14 @@ { const child = this.getChildAt(index); + // ensure child transform will be recalculated.. child.parent = null; + child.transform._parentID = -1; removeItems(this.children, index, 1); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(index); child.emit('removed', this); @@ -275,8 +288,14 @@ for (let i = 0; i < removed.length; ++i) { removed[i].parent = null; + if (removed[i].transform) + { + removed[i].transform._parentID = -1; + } } + this._boundsID++; + this.onChildrenChange(beginIndex); for (let i = 0; i < removed.length; ++i) diff --git a/src/core/display/DisplayObject.js b/src/core/display/DisplayObject.js index 3affe36..4e6c77d 100644 --- a/src/core/display/DisplayObject.js +++ b/src/core/display/DisplayObject.js @@ -109,7 +109,7 @@ /** * The original, cached mask of the object * - * @member {PIXI.Rectangle} + * @member {PIXI.Graphics|PIXI.Sprite} * @private */ this._mask = null; diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index 95f2af4..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 ); @@ -1065,10 +1061,15 @@ canvasRenderer = new CanvasRenderer(); } - tempMatrix.tx = -bounds.x; - tempMatrix.ty = -bounds.y; + this.transform.updateLocalTransform(); + this.transform.localTransform.copy(tempMatrix); - canvasRenderer.render(this, canvasBuffer, false, tempMatrix); + tempMatrix.invert(); + + tempMatrix.tx -= bounds.x; + tempMatrix.ty -= bounds.y; + + canvasRenderer.render(this, canvasBuffer, true, tempMatrix); const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode); 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 e67680f..867dde2 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/buildCircle.js b/src/core/graphics/webgl/utils/buildCircle.js index 0400d13..abf5d02 100644 --- a/src/core/graphics/webgl/utils/buildCircle.js +++ b/src/core/graphics/webgl/utils/buildCircle.js @@ -33,6 +33,11 @@ height = circleData.height; } + if (width === 0 || height === 0) + { + return; + } + const totalSegs = Math.floor(30 * Math.sqrt(circleData.radius)) || Math.floor(15 * Math.sqrt(circleData.width + circleData.height)); diff --git a/src/core/graphics/webgl/utils/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 90155f9..f03fe15 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -30,6 +30,7 @@ export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; export { default as CanvasGraphicsRenderer } from './graphics/canvas/CanvasGraphicsRenderer'; +export { default as Spritesheet } from './textures/Spritesheet'; export { default as Texture } from './textures/Texture'; export { default as BaseTexture } from './textures/BaseTexture'; export { default as RenderTexture } from './textures/RenderTexture'; diff --git a/src/core/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 ccce680..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); } /** @@ -162,6 +162,9 @@ if (transform) { transform.copy(tempWt); + + // lets not forget to flag the parent transform as dirty... + this._tempDisplayObjectParent.transform._worldID = -1; } else { @@ -169,6 +172,7 @@ } displayObject.parent = this._tempDisplayObjectParent; + displayObject.updateTransform(); displayObject.parent = cacheParent; // displayObject.hitArea = //TODO add a temp hit area @@ -279,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 1271fef..71ade15 100644 --- a/package.json +++ b/package.json @@ -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/scripts/release.js b/scripts/release.js index c4c8e36..156620c 100644 --- a/scripts/release.js +++ b/scripts/release.js @@ -12,7 +12,8 @@ 'dist/**/*', 'lib/**/*', 'src/**/*', - 'scripts/**/*', + 'scripts/*', + 'scripts/renders/*', 'test/**/*', '*.json', '*.md', diff --git a/scripts/renders/client.js b/scripts/renders/client.js index 16c9565..6faee34 100755 --- a/scripts/renders/client.js +++ b/scripts/renders/client.js @@ -1,6 +1,6 @@ 'use strict'; -require('../../bin/pixi'); +require('../../dist/pixi'); PIXI.utils.skipHello(); const fs = require('fs'); 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 bc730db..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. @@ -33,8 +33,11 @@ * need to call toDataUrl on the webgl context * @param {number} [options.resolution=1] - The resolution / device pixel ratio of the renderer, retina would be 2 * @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 @@ -43,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. */ @@ -73,7 +99,7 @@ */ stop() { - this.ticker.stop(); + this._ticker.stop(); } /** @@ -81,7 +107,7 @@ */ start() { - this.ticker.start(); + this._ticker.start(); } /** @@ -95,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 index 81e076b..78b17e6 100644 --- a/src/core/Shader.js +++ b/src/core/Shader.js @@ -1,9 +1,7 @@ import { GLShader } from 'pixi-gl-core'; import settings from './settings'; -const { PRECISION } = settings; - -function checkPrecision(src) +function checkPrecision(src, def) { if (src instanceof Array) { @@ -11,14 +9,14 @@ { const copy = src.slice(0); - copy.unshift(`precision ${PRECISION} float;`); + copy.unshift(`precision ${def} float;`); return copy; } } else if (src.substring(0, 9) !== 'precision') { - return `precision ${PRECISION} float;\n${src}`; + return `precision ${def} float;\n${src}`; } return src; @@ -42,6 +40,7 @@ */ constructor(gl, vertexSrc, fragmentSrc) { - super(gl, checkPrecision(vertexSrc), checkPrecision(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 3425b41..f9235e4 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -181,7 +181,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 b3792ef..17fde8d 100644 --- a/src/core/display/Container.js +++ b/src/core/display/Container.js @@ -73,13 +73,14 @@ } child.parent = this; - - // ensure a transform will be recalculated.. - this.transform._parentID = -1; - this._boundsID++; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.push(child); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(this.children.length - 1); child.emit('added', this); @@ -108,9 +109,14 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.splice(index, 0, child); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(index); child.emit('added', this); @@ -174,6 +180,7 @@ removeItems(this.children, currentIndex, 1); // remove from old position this.children.splice(index, 0, child); // add at new position + this.onChildrenChange(index); } @@ -220,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! @@ -244,9 +252,14 @@ { const child = this.getChildAt(index); + // ensure child transform will be recalculated.. child.parent = null; + child.transform._parentID = -1; removeItems(this.children, index, 1); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(index); child.emit('removed', this); @@ -275,8 +288,14 @@ for (let i = 0; i < removed.length; ++i) { removed[i].parent = null; + if (removed[i].transform) + { + removed[i].transform._parentID = -1; + } } + this._boundsID++; + this.onChildrenChange(beginIndex); for (let i = 0; i < removed.length; ++i) diff --git a/src/core/display/DisplayObject.js b/src/core/display/DisplayObject.js index 3affe36..4e6c77d 100644 --- a/src/core/display/DisplayObject.js +++ b/src/core/display/DisplayObject.js @@ -109,7 +109,7 @@ /** * The original, cached mask of the object * - * @member {PIXI.Rectangle} + * @member {PIXI.Graphics|PIXI.Sprite} * @private */ this._mask = null; diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index 95f2af4..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 ); @@ -1065,10 +1061,15 @@ canvasRenderer = new CanvasRenderer(); } - tempMatrix.tx = -bounds.x; - tempMatrix.ty = -bounds.y; + this.transform.updateLocalTransform(); + this.transform.localTransform.copy(tempMatrix); - canvasRenderer.render(this, canvasBuffer, false, tempMatrix); + tempMatrix.invert(); + + tempMatrix.tx -= bounds.x; + tempMatrix.ty -= bounds.y; + + canvasRenderer.render(this, canvasBuffer, true, tempMatrix); const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode); 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 e67680f..867dde2 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/buildCircle.js b/src/core/graphics/webgl/utils/buildCircle.js index 0400d13..abf5d02 100644 --- a/src/core/graphics/webgl/utils/buildCircle.js +++ b/src/core/graphics/webgl/utils/buildCircle.js @@ -33,6 +33,11 @@ height = circleData.height; } + if (width === 0 || height === 0) + { + return; + } + const totalSegs = Math.floor(30 * Math.sqrt(circleData.radius)) || Math.floor(15 * Math.sqrt(circleData.width + circleData.height)); diff --git a/src/core/graphics/webgl/utils/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 90155f9..f03fe15 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -30,6 +30,7 @@ export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; export { default as CanvasGraphicsRenderer } from './graphics/canvas/CanvasGraphicsRenderer'; +export { default as Spritesheet } from './textures/Spritesheet'; export { default as Texture } from './textures/Texture'; export { default as BaseTexture } from './textures/BaseTexture'; export { default as RenderTexture } from './textures/RenderTexture'; diff --git a/src/core/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 ccce680..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); } /** @@ -162,6 +162,9 @@ if (transform) { transform.copy(tempWt); + + // lets not forget to flag the parent transform as dirty... + this._tempDisplayObjectParent.transform._worldID = -1; } else { @@ -169,6 +172,7 @@ } displayObject.parent = this._tempDisplayObjectParent; + displayObject.updateTransform(); displayObject.parent = cacheParent; // displayObject.hitArea = //TODO add a temp hit area @@ -279,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/canvas/utils/CanvasRenderTarget.js b/src/core/renderers/canvas/utils/CanvasRenderTarget.js index 3de0aef..b409891 100644 --- a/src/core/renderers/canvas/utils/CanvasRenderTarget.js +++ b/src/core/renderers/canvas/utils/CanvasRenderTarget.js @@ -1,5 +1,4 @@ import settings from '../../../settings'; -const { RESOLUTION } = settings; /** * Creates a Canvas element of the given size. @@ -30,7 +29,7 @@ */ this.context = this.canvas.getContext('2d'); - this.resolution = resolution || RESOLUTION; + this.resolution = resolution || settings.RESOLUTION; this.resize(width, height); } diff --git a/package.json b/package.json index 1271fef..71ade15 100644 --- a/package.json +++ b/package.json @@ -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/scripts/release.js b/scripts/release.js index c4c8e36..156620c 100644 --- a/scripts/release.js +++ b/scripts/release.js @@ -12,7 +12,8 @@ 'dist/**/*', 'lib/**/*', 'src/**/*', - 'scripts/**/*', + 'scripts/*', + 'scripts/renders/*', 'test/**/*', '*.json', '*.md', diff --git a/scripts/renders/client.js b/scripts/renders/client.js index 16c9565..6faee34 100755 --- a/scripts/renders/client.js +++ b/scripts/renders/client.js @@ -1,6 +1,6 @@ 'use strict'; -require('../../bin/pixi'); +require('../../dist/pixi'); PIXI.utils.skipHello(); const fs = require('fs'); 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 bc730db..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. @@ -33,8 +33,11 @@ * need to call toDataUrl on the webgl context * @param {number} [options.resolution=1] - The resolution / device pixel ratio of the renderer, retina would be 2 * @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 @@ -43,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. */ @@ -73,7 +99,7 @@ */ stop() { - this.ticker.stop(); + this._ticker.stop(); } /** @@ -81,7 +107,7 @@ */ start() { - this.ticker.start(); + this._ticker.start(); } /** @@ -95,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 index 81e076b..78b17e6 100644 --- a/src/core/Shader.js +++ b/src/core/Shader.js @@ -1,9 +1,7 @@ import { GLShader } from 'pixi-gl-core'; import settings from './settings'; -const { PRECISION } = settings; - -function checkPrecision(src) +function checkPrecision(src, def) { if (src instanceof Array) { @@ -11,14 +9,14 @@ { const copy = src.slice(0); - copy.unshift(`precision ${PRECISION} float;`); + copy.unshift(`precision ${def} float;`); return copy; } } else if (src.substring(0, 9) !== 'precision') { - return `precision ${PRECISION} float;\n${src}`; + return `precision ${def} float;\n${src}`; } return src; @@ -42,6 +40,7 @@ */ constructor(gl, vertexSrc, fragmentSrc) { - super(gl, checkPrecision(vertexSrc), checkPrecision(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 3425b41..f9235e4 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -181,7 +181,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 b3792ef..17fde8d 100644 --- a/src/core/display/Container.js +++ b/src/core/display/Container.js @@ -73,13 +73,14 @@ } child.parent = this; - - // ensure a transform will be recalculated.. - this.transform._parentID = -1; - this._boundsID++; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.push(child); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(this.children.length - 1); child.emit('added', this); @@ -108,9 +109,14 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.splice(index, 0, child); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(index); child.emit('added', this); @@ -174,6 +180,7 @@ removeItems(this.children, currentIndex, 1); // remove from old position this.children.splice(index, 0, child); // add at new position + this.onChildrenChange(index); } @@ -220,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! @@ -244,9 +252,14 @@ { const child = this.getChildAt(index); + // ensure child transform will be recalculated.. child.parent = null; + child.transform._parentID = -1; removeItems(this.children, index, 1); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(index); child.emit('removed', this); @@ -275,8 +288,14 @@ for (let i = 0; i < removed.length; ++i) { removed[i].parent = null; + if (removed[i].transform) + { + removed[i].transform._parentID = -1; + } } + this._boundsID++; + this.onChildrenChange(beginIndex); for (let i = 0; i < removed.length; ++i) diff --git a/src/core/display/DisplayObject.js b/src/core/display/DisplayObject.js index 3affe36..4e6c77d 100644 --- a/src/core/display/DisplayObject.js +++ b/src/core/display/DisplayObject.js @@ -109,7 +109,7 @@ /** * The original, cached mask of the object * - * @member {PIXI.Rectangle} + * @member {PIXI.Graphics|PIXI.Sprite} * @private */ this._mask = null; diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index 95f2af4..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 ); @@ -1065,10 +1061,15 @@ canvasRenderer = new CanvasRenderer(); } - tempMatrix.tx = -bounds.x; - tempMatrix.ty = -bounds.y; + this.transform.updateLocalTransform(); + this.transform.localTransform.copy(tempMatrix); - canvasRenderer.render(this, canvasBuffer, false, tempMatrix); + tempMatrix.invert(); + + tempMatrix.tx -= bounds.x; + tempMatrix.ty -= bounds.y; + + canvasRenderer.render(this, canvasBuffer, true, tempMatrix); const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode); 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 e67680f..867dde2 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/buildCircle.js b/src/core/graphics/webgl/utils/buildCircle.js index 0400d13..abf5d02 100644 --- a/src/core/graphics/webgl/utils/buildCircle.js +++ b/src/core/graphics/webgl/utils/buildCircle.js @@ -33,6 +33,11 @@ height = circleData.height; } + if (width === 0 || height === 0) + { + return; + } + const totalSegs = Math.floor(30 * Math.sqrt(circleData.radius)) || Math.floor(15 * Math.sqrt(circleData.width + circleData.height)); diff --git a/src/core/graphics/webgl/utils/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 90155f9..f03fe15 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -30,6 +30,7 @@ export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; export { default as CanvasGraphicsRenderer } from './graphics/canvas/CanvasGraphicsRenderer'; +export { default as Spritesheet } from './textures/Spritesheet'; export { default as Texture } from './textures/Texture'; export { default as BaseTexture } from './textures/BaseTexture'; export { default as RenderTexture } from './textures/RenderTexture'; diff --git a/src/core/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 ccce680..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); } /** @@ -162,6 +162,9 @@ if (transform) { transform.copy(tempWt); + + // lets not forget to flag the parent transform as dirty... + this._tempDisplayObjectParent.transform._worldID = -1; } else { @@ -169,6 +172,7 @@ } displayObject.parent = this._tempDisplayObjectParent; + displayObject.updateTransform(); displayObject.parent = cacheParent; // displayObject.hitArea = //TODO add a temp hit area @@ -279,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/canvas/utils/CanvasRenderTarget.js b/src/core/renderers/canvas/utils/CanvasRenderTarget.js index 3de0aef..b409891 100644 --- a/src/core/renderers/canvas/utils/CanvasRenderTarget.js +++ b/src/core/renderers/canvas/utils/CanvasRenderTarget.js @@ -1,5 +1,4 @@ import settings from '../../../settings'; -const { RESOLUTION } = settings; /** * Creates a Canvas element of the given size. @@ -30,7 +29,7 @@ */ this.context = this.canvas.getContext('2d'); - this.resolution = resolution || RESOLUTION; + this.resolution = resolution || settings.RESOLUTION; this.resize(width, height); } 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 1271fef..71ade15 100644 --- a/package.json +++ b/package.json @@ -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/scripts/release.js b/scripts/release.js index c4c8e36..156620c 100644 --- a/scripts/release.js +++ b/scripts/release.js @@ -12,7 +12,8 @@ 'dist/**/*', 'lib/**/*', 'src/**/*', - 'scripts/**/*', + 'scripts/*', + 'scripts/renders/*', 'test/**/*', '*.json', '*.md', diff --git a/scripts/renders/client.js b/scripts/renders/client.js index 16c9565..6faee34 100755 --- a/scripts/renders/client.js +++ b/scripts/renders/client.js @@ -1,6 +1,6 @@ 'use strict'; -require('../../bin/pixi'); +require('../../dist/pixi'); PIXI.utils.skipHello(); const fs = require('fs'); 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 bc730db..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. @@ -33,8 +33,11 @@ * need to call toDataUrl on the webgl context * @param {number} [options.resolution=1] - The resolution / device pixel ratio of the renderer, retina would be 2 * @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 @@ -43,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. */ @@ -73,7 +99,7 @@ */ stop() { - this.ticker.stop(); + this._ticker.stop(); } /** @@ -81,7 +107,7 @@ */ start() { - this.ticker.start(); + this._ticker.start(); } /** @@ -95,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 index 81e076b..78b17e6 100644 --- a/src/core/Shader.js +++ b/src/core/Shader.js @@ -1,9 +1,7 @@ import { GLShader } from 'pixi-gl-core'; import settings from './settings'; -const { PRECISION } = settings; - -function checkPrecision(src) +function checkPrecision(src, def) { if (src instanceof Array) { @@ -11,14 +9,14 @@ { const copy = src.slice(0); - copy.unshift(`precision ${PRECISION} float;`); + copy.unshift(`precision ${def} float;`); return copy; } } else if (src.substring(0, 9) !== 'precision') { - return `precision ${PRECISION} float;\n${src}`; + return `precision ${def} float;\n${src}`; } return src; @@ -42,6 +40,7 @@ */ constructor(gl, vertexSrc, fragmentSrc) { - super(gl, checkPrecision(vertexSrc), checkPrecision(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 3425b41..f9235e4 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -181,7 +181,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 b3792ef..17fde8d 100644 --- a/src/core/display/Container.js +++ b/src/core/display/Container.js @@ -73,13 +73,14 @@ } child.parent = this; - - // ensure a transform will be recalculated.. - this.transform._parentID = -1; - this._boundsID++; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.push(child); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(this.children.length - 1); child.emit('added', this); @@ -108,9 +109,14 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.splice(index, 0, child); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(index); child.emit('added', this); @@ -174,6 +180,7 @@ removeItems(this.children, currentIndex, 1); // remove from old position this.children.splice(index, 0, child); // add at new position + this.onChildrenChange(index); } @@ -220,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! @@ -244,9 +252,14 @@ { const child = this.getChildAt(index); + // ensure child transform will be recalculated.. child.parent = null; + child.transform._parentID = -1; removeItems(this.children, index, 1); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(index); child.emit('removed', this); @@ -275,8 +288,14 @@ for (let i = 0; i < removed.length; ++i) { removed[i].parent = null; + if (removed[i].transform) + { + removed[i].transform._parentID = -1; + } } + this._boundsID++; + this.onChildrenChange(beginIndex); for (let i = 0; i < removed.length; ++i) diff --git a/src/core/display/DisplayObject.js b/src/core/display/DisplayObject.js index 3affe36..4e6c77d 100644 --- a/src/core/display/DisplayObject.js +++ b/src/core/display/DisplayObject.js @@ -109,7 +109,7 @@ /** * The original, cached mask of the object * - * @member {PIXI.Rectangle} + * @member {PIXI.Graphics|PIXI.Sprite} * @private */ this._mask = null; diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index 95f2af4..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 ); @@ -1065,10 +1061,15 @@ canvasRenderer = new CanvasRenderer(); } - tempMatrix.tx = -bounds.x; - tempMatrix.ty = -bounds.y; + this.transform.updateLocalTransform(); + this.transform.localTransform.copy(tempMatrix); - canvasRenderer.render(this, canvasBuffer, false, tempMatrix); + tempMatrix.invert(); + + tempMatrix.tx -= bounds.x; + tempMatrix.ty -= bounds.y; + + canvasRenderer.render(this, canvasBuffer, true, tempMatrix); const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode); 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 e67680f..867dde2 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/buildCircle.js b/src/core/graphics/webgl/utils/buildCircle.js index 0400d13..abf5d02 100644 --- a/src/core/graphics/webgl/utils/buildCircle.js +++ b/src/core/graphics/webgl/utils/buildCircle.js @@ -33,6 +33,11 @@ height = circleData.height; } + if (width === 0 || height === 0) + { + return; + } + const totalSegs = Math.floor(30 * Math.sqrt(circleData.radius)) || Math.floor(15 * Math.sqrt(circleData.width + circleData.height)); diff --git a/src/core/graphics/webgl/utils/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 90155f9..f03fe15 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -30,6 +30,7 @@ export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; export { default as CanvasGraphicsRenderer } from './graphics/canvas/CanvasGraphicsRenderer'; +export { default as Spritesheet } from './textures/Spritesheet'; export { default as Texture } from './textures/Texture'; export { default as BaseTexture } from './textures/BaseTexture'; export { default as RenderTexture } from './textures/RenderTexture'; diff --git a/src/core/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 ccce680..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); } /** @@ -162,6 +162,9 @@ if (transform) { transform.copy(tempWt); + + // lets not forget to flag the parent transform as dirty... + this._tempDisplayObjectParent.transform._worldID = -1; } else { @@ -169,6 +172,7 @@ } displayObject.parent = this._tempDisplayObjectParent; + displayObject.updateTransform(); displayObject.parent = cacheParent; // displayObject.hitArea = //TODO add a temp hit area @@ -279,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/canvas/utils/CanvasRenderTarget.js b/src/core/renderers/canvas/utils/CanvasRenderTarget.js index 3de0aef..b409891 100644 --- a/src/core/renderers/canvas/utils/CanvasRenderTarget.js +++ b/src/core/renderers/canvas/utils/CanvasRenderTarget.js @@ -1,5 +1,4 @@ import settings from '../../../settings'; -const { RESOLUTION } = settings; /** * Creates a Canvas element of the given size. @@ -30,7 +29,7 @@ */ this.context = this.canvas.getContext('2d'); - this.resolution = resolution || RESOLUTION; + this.resolution = resolution || settings.RESOLUTION; this.resize(width, height); } 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 ce0e1c4..788ed6c 100644 --- a/src/core/renderers/webgl/WebGLRenderer.js +++ b/src/core/renderers/webgl/WebGLRenderer.js @@ -30,8 +30,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 @@ -49,10 +49,19 @@ * enable this if you need to call toDataUrl on the webgl context. * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when * rendering, stopping pixel interpolation. + * @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; + + if (this.legacy) + { + glCore.VertexArrayObject.FORCE_NATIVE = true; + } /** * The type of this renderer as a standardised const @@ -229,7 +238,7 @@ this.emit('context', gl); // setup the width/height properties and gl viewport - this.resize(this.width, this.height); + this.resize(this.screen.width, this.screen.height); } /** @@ -323,16 +332,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) { @@ -376,6 +385,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 @@ -440,9 +469,10 @@ * Changes the current shader to the one given in parameter * * @param {PIXI.Shader} shader - the new shader + * @param {boolean} [autoProject=true] - Whether automatically set the projection matrix * @return {PIXI.WebGLRenderer} Returns itself. */ - bindShader(shader) + bindShader(shader, autoProject) { // TODO cache if (this._activeShader !== shader) @@ -450,8 +480,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 1271fef..71ade15 100644 --- a/package.json +++ b/package.json @@ -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/scripts/release.js b/scripts/release.js index c4c8e36..156620c 100644 --- a/scripts/release.js +++ b/scripts/release.js @@ -12,7 +12,8 @@ 'dist/**/*', 'lib/**/*', 'src/**/*', - 'scripts/**/*', + 'scripts/*', + 'scripts/renders/*', 'test/**/*', '*.json', '*.md', diff --git a/scripts/renders/client.js b/scripts/renders/client.js index 16c9565..6faee34 100755 --- a/scripts/renders/client.js +++ b/scripts/renders/client.js @@ -1,6 +1,6 @@ 'use strict'; -require('../../bin/pixi'); +require('../../dist/pixi'); PIXI.utils.skipHello(); const fs = require('fs'); 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 bc730db..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. @@ -33,8 +33,11 @@ * need to call toDataUrl on the webgl context * @param {number} [options.resolution=1] - The resolution / device pixel ratio of the renderer, retina would be 2 * @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 @@ -43,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. */ @@ -73,7 +99,7 @@ */ stop() { - this.ticker.stop(); + this._ticker.stop(); } /** @@ -81,7 +107,7 @@ */ start() { - this.ticker.start(); + this._ticker.start(); } /** @@ -95,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 index 81e076b..78b17e6 100644 --- a/src/core/Shader.js +++ b/src/core/Shader.js @@ -1,9 +1,7 @@ import { GLShader } from 'pixi-gl-core'; import settings from './settings'; -const { PRECISION } = settings; - -function checkPrecision(src) +function checkPrecision(src, def) { if (src instanceof Array) { @@ -11,14 +9,14 @@ { const copy = src.slice(0); - copy.unshift(`precision ${PRECISION} float;`); + copy.unshift(`precision ${def} float;`); return copy; } } else if (src.substring(0, 9) !== 'precision') { - return `precision ${PRECISION} float;\n${src}`; + return `precision ${def} float;\n${src}`; } return src; @@ -42,6 +40,7 @@ */ constructor(gl, vertexSrc, fragmentSrc) { - super(gl, checkPrecision(vertexSrc), checkPrecision(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 3425b41..f9235e4 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -181,7 +181,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 b3792ef..17fde8d 100644 --- a/src/core/display/Container.js +++ b/src/core/display/Container.js @@ -73,13 +73,14 @@ } child.parent = this; - - // ensure a transform will be recalculated.. - this.transform._parentID = -1; - this._boundsID++; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.push(child); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(this.children.length - 1); child.emit('added', this); @@ -108,9 +109,14 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.splice(index, 0, child); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(index); child.emit('added', this); @@ -174,6 +180,7 @@ removeItems(this.children, currentIndex, 1); // remove from old position this.children.splice(index, 0, child); // add at new position + this.onChildrenChange(index); } @@ -220,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! @@ -244,9 +252,14 @@ { const child = this.getChildAt(index); + // ensure child transform will be recalculated.. child.parent = null; + child.transform._parentID = -1; removeItems(this.children, index, 1); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(index); child.emit('removed', this); @@ -275,8 +288,14 @@ for (let i = 0; i < removed.length; ++i) { removed[i].parent = null; + if (removed[i].transform) + { + removed[i].transform._parentID = -1; + } } + this._boundsID++; + this.onChildrenChange(beginIndex); for (let i = 0; i < removed.length; ++i) diff --git a/src/core/display/DisplayObject.js b/src/core/display/DisplayObject.js index 3affe36..4e6c77d 100644 --- a/src/core/display/DisplayObject.js +++ b/src/core/display/DisplayObject.js @@ -109,7 +109,7 @@ /** * The original, cached mask of the object * - * @member {PIXI.Rectangle} + * @member {PIXI.Graphics|PIXI.Sprite} * @private */ this._mask = null; diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index 95f2af4..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 ); @@ -1065,10 +1061,15 @@ canvasRenderer = new CanvasRenderer(); } - tempMatrix.tx = -bounds.x; - tempMatrix.ty = -bounds.y; + this.transform.updateLocalTransform(); + this.transform.localTransform.copy(tempMatrix); - canvasRenderer.render(this, canvasBuffer, false, tempMatrix); + tempMatrix.invert(); + + tempMatrix.tx -= bounds.x; + tempMatrix.ty -= bounds.y; + + canvasRenderer.render(this, canvasBuffer, true, tempMatrix); const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode); 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 e67680f..867dde2 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/buildCircle.js b/src/core/graphics/webgl/utils/buildCircle.js index 0400d13..abf5d02 100644 --- a/src/core/graphics/webgl/utils/buildCircle.js +++ b/src/core/graphics/webgl/utils/buildCircle.js @@ -33,6 +33,11 @@ height = circleData.height; } + if (width === 0 || height === 0) + { + return; + } + const totalSegs = Math.floor(30 * Math.sqrt(circleData.radius)) || Math.floor(15 * Math.sqrt(circleData.width + circleData.height)); diff --git a/src/core/graphics/webgl/utils/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 90155f9..f03fe15 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -30,6 +30,7 @@ export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; export { default as CanvasGraphicsRenderer } from './graphics/canvas/CanvasGraphicsRenderer'; +export { default as Spritesheet } from './textures/Spritesheet'; export { default as Texture } from './textures/Texture'; export { default as BaseTexture } from './textures/BaseTexture'; export { default as RenderTexture } from './textures/RenderTexture'; diff --git a/src/core/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 ccce680..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); } /** @@ -162,6 +162,9 @@ if (transform) { transform.copy(tempWt); + + // lets not forget to flag the parent transform as dirty... + this._tempDisplayObjectParent.transform._worldID = -1; } else { @@ -169,6 +172,7 @@ } displayObject.parent = this._tempDisplayObjectParent; + displayObject.updateTransform(); displayObject.parent = cacheParent; // displayObject.hitArea = //TODO add a temp hit area @@ -279,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/canvas/utils/CanvasRenderTarget.js b/src/core/renderers/canvas/utils/CanvasRenderTarget.js index 3de0aef..b409891 100644 --- a/src/core/renderers/canvas/utils/CanvasRenderTarget.js +++ b/src/core/renderers/canvas/utils/CanvasRenderTarget.js @@ -1,5 +1,4 @@ import settings from '../../../settings'; -const { RESOLUTION } = settings; /** * Creates a Canvas element of the given size. @@ -30,7 +29,7 @@ */ this.context = this.canvas.getContext('2d'); - this.resolution = resolution || RESOLUTION; + this.resolution = resolution || settings.RESOLUTION; this.resize(width, height); } 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 ce0e1c4..788ed6c 100644 --- a/src/core/renderers/webgl/WebGLRenderer.js +++ b/src/core/renderers/webgl/WebGLRenderer.js @@ -30,8 +30,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 @@ -49,10 +49,19 @@ * enable this if you need to call toDataUrl on the webgl context. * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when * rendering, stopping pixel interpolation. + * @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; + + if (this.legacy) + { + glCore.VertexArrayObject.FORCE_NATIVE = true; + } /** * The type of this renderer as a standardised const @@ -229,7 +238,7 @@ this.emit('context', gl); // setup the width/height properties and gl viewport - this.resize(this.width, this.height); + this.resize(this.screen.width, this.screen.height); } /** @@ -323,16 +332,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) { @@ -376,6 +385,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 @@ -440,9 +469,10 @@ * Changes the current shader to the one given in parameter * * @param {PIXI.Shader} shader - the new shader + * @param {boolean} [autoProject=true] - Whether automatically set the projection matrix * @return {PIXI.WebGLRenderer} Returns itself. */ - bindShader(shader) + bindShader(shader, autoProject) { // TODO cache if (this._activeShader !== shader) @@ -450,8 +480,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 1271fef..71ade15 100644 --- a/package.json +++ b/package.json @@ -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/scripts/release.js b/scripts/release.js index c4c8e36..156620c 100644 --- a/scripts/release.js +++ b/scripts/release.js @@ -12,7 +12,8 @@ 'dist/**/*', 'lib/**/*', 'src/**/*', - 'scripts/**/*', + 'scripts/*', + 'scripts/renders/*', 'test/**/*', '*.json', '*.md', diff --git a/scripts/renders/client.js b/scripts/renders/client.js index 16c9565..6faee34 100755 --- a/scripts/renders/client.js +++ b/scripts/renders/client.js @@ -1,6 +1,6 @@ 'use strict'; -require('../../bin/pixi'); +require('../../dist/pixi'); PIXI.utils.skipHello(); const fs = require('fs'); 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 bc730db..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. @@ -33,8 +33,11 @@ * need to call toDataUrl on the webgl context * @param {number} [options.resolution=1] - The resolution / device pixel ratio of the renderer, retina would be 2 * @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 @@ -43,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. */ @@ -73,7 +99,7 @@ */ stop() { - this.ticker.stop(); + this._ticker.stop(); } /** @@ -81,7 +107,7 @@ */ start() { - this.ticker.start(); + this._ticker.start(); } /** @@ -95,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 index 81e076b..78b17e6 100644 --- a/src/core/Shader.js +++ b/src/core/Shader.js @@ -1,9 +1,7 @@ import { GLShader } from 'pixi-gl-core'; import settings from './settings'; -const { PRECISION } = settings; - -function checkPrecision(src) +function checkPrecision(src, def) { if (src instanceof Array) { @@ -11,14 +9,14 @@ { const copy = src.slice(0); - copy.unshift(`precision ${PRECISION} float;`); + copy.unshift(`precision ${def} float;`); return copy; } } else if (src.substring(0, 9) !== 'precision') { - return `precision ${PRECISION} float;\n${src}`; + return `precision ${def} float;\n${src}`; } return src; @@ -42,6 +40,7 @@ */ constructor(gl, vertexSrc, fragmentSrc) { - super(gl, checkPrecision(vertexSrc), checkPrecision(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 3425b41..f9235e4 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -181,7 +181,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 b3792ef..17fde8d 100644 --- a/src/core/display/Container.js +++ b/src/core/display/Container.js @@ -73,13 +73,14 @@ } child.parent = this; - - // ensure a transform will be recalculated.. - this.transform._parentID = -1; - this._boundsID++; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.push(child); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(this.children.length - 1); child.emit('added', this); @@ -108,9 +109,14 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.splice(index, 0, child); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(index); child.emit('added', this); @@ -174,6 +180,7 @@ removeItems(this.children, currentIndex, 1); // remove from old position this.children.splice(index, 0, child); // add at new position + this.onChildrenChange(index); } @@ -220,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! @@ -244,9 +252,14 @@ { const child = this.getChildAt(index); + // ensure child transform will be recalculated.. child.parent = null; + child.transform._parentID = -1; removeItems(this.children, index, 1); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(index); child.emit('removed', this); @@ -275,8 +288,14 @@ for (let i = 0; i < removed.length; ++i) { removed[i].parent = null; + if (removed[i].transform) + { + removed[i].transform._parentID = -1; + } } + this._boundsID++; + this.onChildrenChange(beginIndex); for (let i = 0; i < removed.length; ++i) diff --git a/src/core/display/DisplayObject.js b/src/core/display/DisplayObject.js index 3affe36..4e6c77d 100644 --- a/src/core/display/DisplayObject.js +++ b/src/core/display/DisplayObject.js @@ -109,7 +109,7 @@ /** * The original, cached mask of the object * - * @member {PIXI.Rectangle} + * @member {PIXI.Graphics|PIXI.Sprite} * @private */ this._mask = null; diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index 95f2af4..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 ); @@ -1065,10 +1061,15 @@ canvasRenderer = new CanvasRenderer(); } - tempMatrix.tx = -bounds.x; - tempMatrix.ty = -bounds.y; + this.transform.updateLocalTransform(); + this.transform.localTransform.copy(tempMatrix); - canvasRenderer.render(this, canvasBuffer, false, tempMatrix); + tempMatrix.invert(); + + tempMatrix.tx -= bounds.x; + tempMatrix.ty -= bounds.y; + + canvasRenderer.render(this, canvasBuffer, true, tempMatrix); const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode); 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 e67680f..867dde2 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/buildCircle.js b/src/core/graphics/webgl/utils/buildCircle.js index 0400d13..abf5d02 100644 --- a/src/core/graphics/webgl/utils/buildCircle.js +++ b/src/core/graphics/webgl/utils/buildCircle.js @@ -33,6 +33,11 @@ height = circleData.height; } + if (width === 0 || height === 0) + { + return; + } + const totalSegs = Math.floor(30 * Math.sqrt(circleData.radius)) || Math.floor(15 * Math.sqrt(circleData.width + circleData.height)); diff --git a/src/core/graphics/webgl/utils/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 90155f9..f03fe15 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -30,6 +30,7 @@ export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; export { default as CanvasGraphicsRenderer } from './graphics/canvas/CanvasGraphicsRenderer'; +export { default as Spritesheet } from './textures/Spritesheet'; export { default as Texture } from './textures/Texture'; export { default as BaseTexture } from './textures/BaseTexture'; export { default as RenderTexture } from './textures/RenderTexture'; diff --git a/src/core/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 ccce680..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); } /** @@ -162,6 +162,9 @@ if (transform) { transform.copy(tempWt); + + // lets not forget to flag the parent transform as dirty... + this._tempDisplayObjectParent.transform._worldID = -1; } else { @@ -169,6 +172,7 @@ } displayObject.parent = this._tempDisplayObjectParent; + displayObject.updateTransform(); displayObject.parent = cacheParent; // displayObject.hitArea = //TODO add a temp hit area @@ -279,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/canvas/utils/CanvasRenderTarget.js b/src/core/renderers/canvas/utils/CanvasRenderTarget.js index 3de0aef..b409891 100644 --- a/src/core/renderers/canvas/utils/CanvasRenderTarget.js +++ b/src/core/renderers/canvas/utils/CanvasRenderTarget.js @@ -1,5 +1,4 @@ import settings from '../../../settings'; -const { RESOLUTION } = settings; /** * Creates a Canvas element of the given size. @@ -30,7 +29,7 @@ */ this.context = this.canvas.getContext('2d'); - this.resolution = resolution || RESOLUTION; + this.resolution = resolution || settings.RESOLUTION; this.resize(width, height); } 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 ce0e1c4..788ed6c 100644 --- a/src/core/renderers/webgl/WebGLRenderer.js +++ b/src/core/renderers/webgl/WebGLRenderer.js @@ -30,8 +30,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 @@ -49,10 +49,19 @@ * enable this if you need to call toDataUrl on the webgl context. * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when * rendering, stopping pixel interpolation. + * @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; + + if (this.legacy) + { + glCore.VertexArrayObject.FORCE_NATIVE = true; + } /** * The type of this renderer as a standardised const @@ -229,7 +238,7 @@ this.emit('context', gl); // setup the width/height properties and gl viewport - this.resize(this.width, this.height); + this.resize(this.screen.width, this.screen.height); } /** @@ -323,16 +332,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) { @@ -376,6 +385,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 @@ -440,9 +469,10 @@ * Changes the current shader to the one given in parameter * * @param {PIXI.Shader} shader - the new shader + * @param {boolean} [autoProject=true] - Whether automatically set the projection matrix * @return {PIXI.WebGLRenderer} Returns itself. */ - bindShader(shader) + bindShader(shader, autoProject) { // TODO cache if (this._activeShader !== shader) @@ -450,8 +480,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 bbab3b3..944f41d 100644 --- a/src/core/renderers/webgl/filters/Filter.js +++ b/src/core/renderers/webgl/filters/Filter.js @@ -1,6 +1,7 @@ import extractUniformsFromSrc from './extractUniformsFromSrc'; import { uid } from '../../../utils'; import { BLEND_MODES } from '../../../const'; +import settings from '../../../settings'; const SOURCE_KEY_MAP = {}; @@ -10,7 +11,7 @@ * @memberof PIXI * @extends PIXI.Shader */ -class Filter +export default class Filter { /** * @param {string} [vertexSrc] - The source of the vertex shader. @@ -35,8 +36,6 @@ this.blendMode = BLEND_MODES.NORMAL; - // pull out the vertex and shader uniforms if they are not specified.. - // currently this does not extract structs only default types this.uniformData = uniforms || extractUniformsFromSrc(this.vertexSrc, this.fragmentSrc, 'projectionMatrix|uSampler'); /** @@ -80,7 +79,7 @@ * * @member {number} */ - this.resolution = 1; + this.resolution = settings.RESOLUTION; /** * If enabled is true the filter is applied, if false it will not. @@ -97,8 +96,11 @@ * @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 { // --- // // this.uniforms.filterMatrix = filterManager.calculateSpriteMatrix(tempMatrix, window.panda ); @@ -170,5 +172,3 @@ ].join('\n'); } } - -export default Filter; diff --git a/package.json b/package.json index 1271fef..71ade15 100644 --- a/package.json +++ b/package.json @@ -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/scripts/release.js b/scripts/release.js index c4c8e36..156620c 100644 --- a/scripts/release.js +++ b/scripts/release.js @@ -12,7 +12,8 @@ 'dist/**/*', 'lib/**/*', 'src/**/*', - 'scripts/**/*', + 'scripts/*', + 'scripts/renders/*', 'test/**/*', '*.json', '*.md', diff --git a/scripts/renders/client.js b/scripts/renders/client.js index 16c9565..6faee34 100755 --- a/scripts/renders/client.js +++ b/scripts/renders/client.js @@ -1,6 +1,6 @@ 'use strict'; -require('../../bin/pixi'); +require('../../dist/pixi'); PIXI.utils.skipHello(); const fs = require('fs'); 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 bc730db..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. @@ -33,8 +33,11 @@ * need to call toDataUrl on the webgl context * @param {number} [options.resolution=1] - The resolution / device pixel ratio of the renderer, retina would be 2 * @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 @@ -43,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. */ @@ -73,7 +99,7 @@ */ stop() { - this.ticker.stop(); + this._ticker.stop(); } /** @@ -81,7 +107,7 @@ */ start() { - this.ticker.start(); + this._ticker.start(); } /** @@ -95,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 index 81e076b..78b17e6 100644 --- a/src/core/Shader.js +++ b/src/core/Shader.js @@ -1,9 +1,7 @@ import { GLShader } from 'pixi-gl-core'; import settings from './settings'; -const { PRECISION } = settings; - -function checkPrecision(src) +function checkPrecision(src, def) { if (src instanceof Array) { @@ -11,14 +9,14 @@ { const copy = src.slice(0); - copy.unshift(`precision ${PRECISION} float;`); + copy.unshift(`precision ${def} float;`); return copy; } } else if (src.substring(0, 9) !== 'precision') { - return `precision ${PRECISION} float;\n${src}`; + return `precision ${def} float;\n${src}`; } return src; @@ -42,6 +40,7 @@ */ constructor(gl, vertexSrc, fragmentSrc) { - super(gl, checkPrecision(vertexSrc), checkPrecision(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 3425b41..f9235e4 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -181,7 +181,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 b3792ef..17fde8d 100644 --- a/src/core/display/Container.js +++ b/src/core/display/Container.js @@ -73,13 +73,14 @@ } child.parent = this; - - // ensure a transform will be recalculated.. - this.transform._parentID = -1; - this._boundsID++; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.push(child); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(this.children.length - 1); child.emit('added', this); @@ -108,9 +109,14 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.splice(index, 0, child); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(index); child.emit('added', this); @@ -174,6 +180,7 @@ removeItems(this.children, currentIndex, 1); // remove from old position this.children.splice(index, 0, child); // add at new position + this.onChildrenChange(index); } @@ -220,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! @@ -244,9 +252,14 @@ { const child = this.getChildAt(index); + // ensure child transform will be recalculated.. child.parent = null; + child.transform._parentID = -1; removeItems(this.children, index, 1); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(index); child.emit('removed', this); @@ -275,8 +288,14 @@ for (let i = 0; i < removed.length; ++i) { removed[i].parent = null; + if (removed[i].transform) + { + removed[i].transform._parentID = -1; + } } + this._boundsID++; + this.onChildrenChange(beginIndex); for (let i = 0; i < removed.length; ++i) diff --git a/src/core/display/DisplayObject.js b/src/core/display/DisplayObject.js index 3affe36..4e6c77d 100644 --- a/src/core/display/DisplayObject.js +++ b/src/core/display/DisplayObject.js @@ -109,7 +109,7 @@ /** * The original, cached mask of the object * - * @member {PIXI.Rectangle} + * @member {PIXI.Graphics|PIXI.Sprite} * @private */ this._mask = null; diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index 95f2af4..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 ); @@ -1065,10 +1061,15 @@ canvasRenderer = new CanvasRenderer(); } - tempMatrix.tx = -bounds.x; - tempMatrix.ty = -bounds.y; + this.transform.updateLocalTransform(); + this.transform.localTransform.copy(tempMatrix); - canvasRenderer.render(this, canvasBuffer, false, tempMatrix); + tempMatrix.invert(); + + tempMatrix.tx -= bounds.x; + tempMatrix.ty -= bounds.y; + + canvasRenderer.render(this, canvasBuffer, true, tempMatrix); const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode); 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 e67680f..867dde2 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/buildCircle.js b/src/core/graphics/webgl/utils/buildCircle.js index 0400d13..abf5d02 100644 --- a/src/core/graphics/webgl/utils/buildCircle.js +++ b/src/core/graphics/webgl/utils/buildCircle.js @@ -33,6 +33,11 @@ height = circleData.height; } + if (width === 0 || height === 0) + { + return; + } + const totalSegs = Math.floor(30 * Math.sqrt(circleData.radius)) || Math.floor(15 * Math.sqrt(circleData.width + circleData.height)); diff --git a/src/core/graphics/webgl/utils/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 90155f9..f03fe15 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -30,6 +30,7 @@ export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; export { default as CanvasGraphicsRenderer } from './graphics/canvas/CanvasGraphicsRenderer'; +export { default as Spritesheet } from './textures/Spritesheet'; export { default as Texture } from './textures/Texture'; export { default as BaseTexture } from './textures/BaseTexture'; export { default as RenderTexture } from './textures/RenderTexture'; diff --git a/src/core/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 ccce680..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); } /** @@ -162,6 +162,9 @@ if (transform) { transform.copy(tempWt); + + // lets not forget to flag the parent transform as dirty... + this._tempDisplayObjectParent.transform._worldID = -1; } else { @@ -169,6 +172,7 @@ } displayObject.parent = this._tempDisplayObjectParent; + displayObject.updateTransform(); displayObject.parent = cacheParent; // displayObject.hitArea = //TODO add a temp hit area @@ -279,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/canvas/utils/CanvasRenderTarget.js b/src/core/renderers/canvas/utils/CanvasRenderTarget.js index 3de0aef..b409891 100644 --- a/src/core/renderers/canvas/utils/CanvasRenderTarget.js +++ b/src/core/renderers/canvas/utils/CanvasRenderTarget.js @@ -1,5 +1,4 @@ import settings from '../../../settings'; -const { RESOLUTION } = settings; /** * Creates a Canvas element of the given size. @@ -30,7 +29,7 @@ */ this.context = this.canvas.getContext('2d'); - this.resolution = resolution || RESOLUTION; + this.resolution = resolution || settings.RESOLUTION; this.resize(width, height); } 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 ce0e1c4..788ed6c 100644 --- a/src/core/renderers/webgl/WebGLRenderer.js +++ b/src/core/renderers/webgl/WebGLRenderer.js @@ -30,8 +30,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 @@ -49,10 +49,19 @@ * enable this if you need to call toDataUrl on the webgl context. * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when * rendering, stopping pixel interpolation. + * @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; + + if (this.legacy) + { + glCore.VertexArrayObject.FORCE_NATIVE = true; + } /** * The type of this renderer as a standardised const @@ -229,7 +238,7 @@ this.emit('context', gl); // setup the width/height properties and gl viewport - this.resize(this.width, this.height); + this.resize(this.screen.width, this.screen.height); } /** @@ -323,16 +332,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) { @@ -376,6 +385,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 @@ -440,9 +469,10 @@ * Changes the current shader to the one given in parameter * * @param {PIXI.Shader} shader - the new shader + * @param {boolean} [autoProject=true] - Whether automatically set the projection matrix * @return {PIXI.WebGLRenderer} Returns itself. */ - bindShader(shader) + bindShader(shader, autoProject) { // TODO cache if (this._activeShader !== shader) @@ -450,8 +480,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 bbab3b3..944f41d 100644 --- a/src/core/renderers/webgl/filters/Filter.js +++ b/src/core/renderers/webgl/filters/Filter.js @@ -1,6 +1,7 @@ import extractUniformsFromSrc from './extractUniformsFromSrc'; import { uid } from '../../../utils'; import { BLEND_MODES } from '../../../const'; +import settings from '../../../settings'; const SOURCE_KEY_MAP = {}; @@ -10,7 +11,7 @@ * @memberof PIXI * @extends PIXI.Shader */ -class Filter +export default class Filter { /** * @param {string} [vertexSrc] - The source of the vertex shader. @@ -35,8 +36,6 @@ this.blendMode = BLEND_MODES.NORMAL; - // pull out the vertex and shader uniforms if they are not specified.. - // currently this does not extract structs only default types this.uniformData = uniforms || extractUniformsFromSrc(this.vertexSrc, this.fragmentSrc, 'projectionMatrix|uSampler'); /** @@ -80,7 +79,7 @@ * * @member {number} */ - this.resolution = 1; + this.resolution = settings.RESOLUTION; /** * If enabled is true the filter is applied, if false it will not. @@ -97,8 +96,11 @@ * @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 { // --- // // this.uniforms.filterMatrix = filterManager.calculateSpriteMatrix(tempMatrix, window.panda ); @@ -170,5 +172,3 @@ ].join('\n'); } } - -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 1271fef..71ade15 100644 --- a/package.json +++ b/package.json @@ -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/scripts/release.js b/scripts/release.js index c4c8e36..156620c 100644 --- a/scripts/release.js +++ b/scripts/release.js @@ -12,7 +12,8 @@ 'dist/**/*', 'lib/**/*', 'src/**/*', - 'scripts/**/*', + 'scripts/*', + 'scripts/renders/*', 'test/**/*', '*.json', '*.md', diff --git a/scripts/renders/client.js b/scripts/renders/client.js index 16c9565..6faee34 100755 --- a/scripts/renders/client.js +++ b/scripts/renders/client.js @@ -1,6 +1,6 @@ 'use strict'; -require('../../bin/pixi'); +require('../../dist/pixi'); PIXI.utils.skipHello(); const fs = require('fs'); 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 bc730db..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. @@ -33,8 +33,11 @@ * need to call toDataUrl on the webgl context * @param {number} [options.resolution=1] - The resolution / device pixel ratio of the renderer, retina would be 2 * @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 @@ -43,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. */ @@ -73,7 +99,7 @@ */ stop() { - this.ticker.stop(); + this._ticker.stop(); } /** @@ -81,7 +107,7 @@ */ start() { - this.ticker.start(); + this._ticker.start(); } /** @@ -95,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 index 81e076b..78b17e6 100644 --- a/src/core/Shader.js +++ b/src/core/Shader.js @@ -1,9 +1,7 @@ import { GLShader } from 'pixi-gl-core'; import settings from './settings'; -const { PRECISION } = settings; - -function checkPrecision(src) +function checkPrecision(src, def) { if (src instanceof Array) { @@ -11,14 +9,14 @@ { const copy = src.slice(0); - copy.unshift(`precision ${PRECISION} float;`); + copy.unshift(`precision ${def} float;`); return copy; } } else if (src.substring(0, 9) !== 'precision') { - return `precision ${PRECISION} float;\n${src}`; + return `precision ${def} float;\n${src}`; } return src; @@ -42,6 +40,7 @@ */ constructor(gl, vertexSrc, fragmentSrc) { - super(gl, checkPrecision(vertexSrc), checkPrecision(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 3425b41..f9235e4 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -181,7 +181,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 b3792ef..17fde8d 100644 --- a/src/core/display/Container.js +++ b/src/core/display/Container.js @@ -73,13 +73,14 @@ } child.parent = this; - - // ensure a transform will be recalculated.. - this.transform._parentID = -1; - this._boundsID++; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.push(child); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(this.children.length - 1); child.emit('added', this); @@ -108,9 +109,14 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.splice(index, 0, child); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(index); child.emit('added', this); @@ -174,6 +180,7 @@ removeItems(this.children, currentIndex, 1); // remove from old position this.children.splice(index, 0, child); // add at new position + this.onChildrenChange(index); } @@ -220,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! @@ -244,9 +252,14 @@ { const child = this.getChildAt(index); + // ensure child transform will be recalculated.. child.parent = null; + child.transform._parentID = -1; removeItems(this.children, index, 1); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(index); child.emit('removed', this); @@ -275,8 +288,14 @@ for (let i = 0; i < removed.length; ++i) { removed[i].parent = null; + if (removed[i].transform) + { + removed[i].transform._parentID = -1; + } } + this._boundsID++; + this.onChildrenChange(beginIndex); for (let i = 0; i < removed.length; ++i) diff --git a/src/core/display/DisplayObject.js b/src/core/display/DisplayObject.js index 3affe36..4e6c77d 100644 --- a/src/core/display/DisplayObject.js +++ b/src/core/display/DisplayObject.js @@ -109,7 +109,7 @@ /** * The original, cached mask of the object * - * @member {PIXI.Rectangle} + * @member {PIXI.Graphics|PIXI.Sprite} * @private */ this._mask = null; diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index 95f2af4..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 ); @@ -1065,10 +1061,15 @@ canvasRenderer = new CanvasRenderer(); } - tempMatrix.tx = -bounds.x; - tempMatrix.ty = -bounds.y; + this.transform.updateLocalTransform(); + this.transform.localTransform.copy(tempMatrix); - canvasRenderer.render(this, canvasBuffer, false, tempMatrix); + tempMatrix.invert(); + + tempMatrix.tx -= bounds.x; + tempMatrix.ty -= bounds.y; + + canvasRenderer.render(this, canvasBuffer, true, tempMatrix); const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode); 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 e67680f..867dde2 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/buildCircle.js b/src/core/graphics/webgl/utils/buildCircle.js index 0400d13..abf5d02 100644 --- a/src/core/graphics/webgl/utils/buildCircle.js +++ b/src/core/graphics/webgl/utils/buildCircle.js @@ -33,6 +33,11 @@ height = circleData.height; } + if (width === 0 || height === 0) + { + return; + } + const totalSegs = Math.floor(30 * Math.sqrt(circleData.radius)) || Math.floor(15 * Math.sqrt(circleData.width + circleData.height)); diff --git a/src/core/graphics/webgl/utils/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 90155f9..f03fe15 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -30,6 +30,7 @@ export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; export { default as CanvasGraphicsRenderer } from './graphics/canvas/CanvasGraphicsRenderer'; +export { default as Spritesheet } from './textures/Spritesheet'; export { default as Texture } from './textures/Texture'; export { default as BaseTexture } from './textures/BaseTexture'; export { default as RenderTexture } from './textures/RenderTexture'; diff --git a/src/core/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 ccce680..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); } /** @@ -162,6 +162,9 @@ if (transform) { transform.copy(tempWt); + + // lets not forget to flag the parent transform as dirty... + this._tempDisplayObjectParent.transform._worldID = -1; } else { @@ -169,6 +172,7 @@ } displayObject.parent = this._tempDisplayObjectParent; + displayObject.updateTransform(); displayObject.parent = cacheParent; // displayObject.hitArea = //TODO add a temp hit area @@ -279,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/canvas/utils/CanvasRenderTarget.js b/src/core/renderers/canvas/utils/CanvasRenderTarget.js index 3de0aef..b409891 100644 --- a/src/core/renderers/canvas/utils/CanvasRenderTarget.js +++ b/src/core/renderers/canvas/utils/CanvasRenderTarget.js @@ -1,5 +1,4 @@ import settings from '../../../settings'; -const { RESOLUTION } = settings; /** * Creates a Canvas element of the given size. @@ -30,7 +29,7 @@ */ this.context = this.canvas.getContext('2d'); - this.resolution = resolution || RESOLUTION; + this.resolution = resolution || settings.RESOLUTION; this.resize(width, height); } 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 ce0e1c4..788ed6c 100644 --- a/src/core/renderers/webgl/WebGLRenderer.js +++ b/src/core/renderers/webgl/WebGLRenderer.js @@ -30,8 +30,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 @@ -49,10 +49,19 @@ * enable this if you need to call toDataUrl on the webgl context. * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when * rendering, stopping pixel interpolation. + * @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; + + if (this.legacy) + { + glCore.VertexArrayObject.FORCE_NATIVE = true; + } /** * The type of this renderer as a standardised const @@ -229,7 +238,7 @@ this.emit('context', gl); // setup the width/height properties and gl viewport - this.resize(this.width, this.height); + this.resize(this.screen.width, this.screen.height); } /** @@ -323,16 +332,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) { @@ -376,6 +385,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 @@ -440,9 +469,10 @@ * Changes the current shader to the one given in parameter * * @param {PIXI.Shader} shader - the new shader + * @param {boolean} [autoProject=true] - Whether automatically set the projection matrix * @return {PIXI.WebGLRenderer} Returns itself. */ - bindShader(shader) + bindShader(shader, autoProject) { // TODO cache if (this._activeShader !== shader) @@ -450,8 +480,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 bbab3b3..944f41d 100644 --- a/src/core/renderers/webgl/filters/Filter.js +++ b/src/core/renderers/webgl/filters/Filter.js @@ -1,6 +1,7 @@ import extractUniformsFromSrc from './extractUniformsFromSrc'; import { uid } from '../../../utils'; import { BLEND_MODES } from '../../../const'; +import settings from '../../../settings'; const SOURCE_KEY_MAP = {}; @@ -10,7 +11,7 @@ * @memberof PIXI * @extends PIXI.Shader */ -class Filter +export default class Filter { /** * @param {string} [vertexSrc] - The source of the vertex shader. @@ -35,8 +36,6 @@ this.blendMode = BLEND_MODES.NORMAL; - // pull out the vertex and shader uniforms if they are not specified.. - // currently this does not extract structs only default types this.uniformData = uniforms || extractUniformsFromSrc(this.vertexSrc, this.fragmentSrc, 'projectionMatrix|uSampler'); /** @@ -80,7 +79,7 @@ * * @member {number} */ - this.resolution = 1; + this.resolution = settings.RESOLUTION; /** * If enabled is true the filter is applied, if false it will not. @@ -97,8 +96,11 @@ * @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 { // --- // // this.uniforms.filterMatrix = filterManager.calculateSpriteMatrix(tempMatrix, window.panda ); @@ -170,5 +172,3 @@ ].join('\n'); } } - -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 dc7072a..6c7422d 100644 --- a/src/core/renderers/webgl/managers/FilterManager.js +++ b/src/core/renderers/webgl/managers/FilterManager.js @@ -154,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 @@ -176,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; @@ -184,7 +184,7 @@ flop = t; } - filters[i].apply(this, flip, lastState.renderTarget, true); + filters[i].apply(this, flip, lastState.renderTarget, false, currentState); this.freePotRenderTarget(flip); this.freePotRenderTarget(flop); @@ -291,9 +291,13 @@ let textureCount = 1; let currentState; - if (shader.uniforms.data.filterArea) + // 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; @@ -306,9 +310,9 @@ // use this to clamp displaced texture coords so they belong to filterArea // see displacementFilter fragment shader for an example - if (shader.uniforms.data.filterClamp) + if (shader.uniforms.filterClamp) { - currentState = this.filterData.stack[this.filterData.index]; + currentState = currentState || this.filterData.stack[this.filterData.index]; const filterClamp = shader.uniforms.filterClamp; @@ -485,7 +489,7 @@ */ destroy() { - this.shaderCache = []; + this.shaderCache = {}; this.emptyPool(); } diff --git a/package.json b/package.json index 1271fef..71ade15 100644 --- a/package.json +++ b/package.json @@ -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/scripts/release.js b/scripts/release.js index c4c8e36..156620c 100644 --- a/scripts/release.js +++ b/scripts/release.js @@ -12,7 +12,8 @@ 'dist/**/*', 'lib/**/*', 'src/**/*', - 'scripts/**/*', + 'scripts/*', + 'scripts/renders/*', 'test/**/*', '*.json', '*.md', diff --git a/scripts/renders/client.js b/scripts/renders/client.js index 16c9565..6faee34 100755 --- a/scripts/renders/client.js +++ b/scripts/renders/client.js @@ -1,6 +1,6 @@ 'use strict'; -require('../../bin/pixi'); +require('../../dist/pixi'); PIXI.utils.skipHello(); const fs = require('fs'); 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 bc730db..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. @@ -33,8 +33,11 @@ * need to call toDataUrl on the webgl context * @param {number} [options.resolution=1] - The resolution / device pixel ratio of the renderer, retina would be 2 * @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 @@ -43,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. */ @@ -73,7 +99,7 @@ */ stop() { - this.ticker.stop(); + this._ticker.stop(); } /** @@ -81,7 +107,7 @@ */ start() { - this.ticker.start(); + this._ticker.start(); } /** @@ -95,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 index 81e076b..78b17e6 100644 --- a/src/core/Shader.js +++ b/src/core/Shader.js @@ -1,9 +1,7 @@ import { GLShader } from 'pixi-gl-core'; import settings from './settings'; -const { PRECISION } = settings; - -function checkPrecision(src) +function checkPrecision(src, def) { if (src instanceof Array) { @@ -11,14 +9,14 @@ { const copy = src.slice(0); - copy.unshift(`precision ${PRECISION} float;`); + copy.unshift(`precision ${def} float;`); return copy; } } else if (src.substring(0, 9) !== 'precision') { - return `precision ${PRECISION} float;\n${src}`; + return `precision ${def} float;\n${src}`; } return src; @@ -42,6 +40,7 @@ */ constructor(gl, vertexSrc, fragmentSrc) { - super(gl, checkPrecision(vertexSrc), checkPrecision(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 3425b41..f9235e4 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -181,7 +181,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 b3792ef..17fde8d 100644 --- a/src/core/display/Container.js +++ b/src/core/display/Container.js @@ -73,13 +73,14 @@ } child.parent = this; - - // ensure a transform will be recalculated.. - this.transform._parentID = -1; - this._boundsID++; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.push(child); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(this.children.length - 1); child.emit('added', this); @@ -108,9 +109,14 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.splice(index, 0, child); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(index); child.emit('added', this); @@ -174,6 +180,7 @@ removeItems(this.children, currentIndex, 1); // remove from old position this.children.splice(index, 0, child); // add at new position + this.onChildrenChange(index); } @@ -220,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! @@ -244,9 +252,14 @@ { const child = this.getChildAt(index); + // ensure child transform will be recalculated.. child.parent = null; + child.transform._parentID = -1; removeItems(this.children, index, 1); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(index); child.emit('removed', this); @@ -275,8 +288,14 @@ for (let i = 0; i < removed.length; ++i) { removed[i].parent = null; + if (removed[i].transform) + { + removed[i].transform._parentID = -1; + } } + this._boundsID++; + this.onChildrenChange(beginIndex); for (let i = 0; i < removed.length; ++i) diff --git a/src/core/display/DisplayObject.js b/src/core/display/DisplayObject.js index 3affe36..4e6c77d 100644 --- a/src/core/display/DisplayObject.js +++ b/src/core/display/DisplayObject.js @@ -109,7 +109,7 @@ /** * The original, cached mask of the object * - * @member {PIXI.Rectangle} + * @member {PIXI.Graphics|PIXI.Sprite} * @private */ this._mask = null; diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index 95f2af4..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 ); @@ -1065,10 +1061,15 @@ canvasRenderer = new CanvasRenderer(); } - tempMatrix.tx = -bounds.x; - tempMatrix.ty = -bounds.y; + this.transform.updateLocalTransform(); + this.transform.localTransform.copy(tempMatrix); - canvasRenderer.render(this, canvasBuffer, false, tempMatrix); + tempMatrix.invert(); + + tempMatrix.tx -= bounds.x; + tempMatrix.ty -= bounds.y; + + canvasRenderer.render(this, canvasBuffer, true, tempMatrix); const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode); 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 e67680f..867dde2 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/buildCircle.js b/src/core/graphics/webgl/utils/buildCircle.js index 0400d13..abf5d02 100644 --- a/src/core/graphics/webgl/utils/buildCircle.js +++ b/src/core/graphics/webgl/utils/buildCircle.js @@ -33,6 +33,11 @@ height = circleData.height; } + if (width === 0 || height === 0) + { + return; + } + const totalSegs = Math.floor(30 * Math.sqrt(circleData.radius)) || Math.floor(15 * Math.sqrt(circleData.width + circleData.height)); diff --git a/src/core/graphics/webgl/utils/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 90155f9..f03fe15 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -30,6 +30,7 @@ export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; export { default as CanvasGraphicsRenderer } from './graphics/canvas/CanvasGraphicsRenderer'; +export { default as Spritesheet } from './textures/Spritesheet'; export { default as Texture } from './textures/Texture'; export { default as BaseTexture } from './textures/BaseTexture'; export { default as RenderTexture } from './textures/RenderTexture'; diff --git a/src/core/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 ccce680..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); } /** @@ -162,6 +162,9 @@ if (transform) { transform.copy(tempWt); + + // lets not forget to flag the parent transform as dirty... + this._tempDisplayObjectParent.transform._worldID = -1; } else { @@ -169,6 +172,7 @@ } displayObject.parent = this._tempDisplayObjectParent; + displayObject.updateTransform(); displayObject.parent = cacheParent; // displayObject.hitArea = //TODO add a temp hit area @@ -279,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/canvas/utils/CanvasRenderTarget.js b/src/core/renderers/canvas/utils/CanvasRenderTarget.js index 3de0aef..b409891 100644 --- a/src/core/renderers/canvas/utils/CanvasRenderTarget.js +++ b/src/core/renderers/canvas/utils/CanvasRenderTarget.js @@ -1,5 +1,4 @@ import settings from '../../../settings'; -const { RESOLUTION } = settings; /** * Creates a Canvas element of the given size. @@ -30,7 +29,7 @@ */ this.context = this.canvas.getContext('2d'); - this.resolution = resolution || RESOLUTION; + this.resolution = resolution || settings.RESOLUTION; this.resize(width, height); } 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 ce0e1c4..788ed6c 100644 --- a/src/core/renderers/webgl/WebGLRenderer.js +++ b/src/core/renderers/webgl/WebGLRenderer.js @@ -30,8 +30,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 @@ -49,10 +49,19 @@ * enable this if you need to call toDataUrl on the webgl context. * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when * rendering, stopping pixel interpolation. + * @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; + + if (this.legacy) + { + glCore.VertexArrayObject.FORCE_NATIVE = true; + } /** * The type of this renderer as a standardised const @@ -229,7 +238,7 @@ this.emit('context', gl); // setup the width/height properties and gl viewport - this.resize(this.width, this.height); + this.resize(this.screen.width, this.screen.height); } /** @@ -323,16 +332,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) { @@ -376,6 +385,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 @@ -440,9 +469,10 @@ * Changes the current shader to the one given in parameter * * @param {PIXI.Shader} shader - the new shader + * @param {boolean} [autoProject=true] - Whether automatically set the projection matrix * @return {PIXI.WebGLRenderer} Returns itself. */ - bindShader(shader) + bindShader(shader, autoProject) { // TODO cache if (this._activeShader !== shader) @@ -450,8 +480,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 bbab3b3..944f41d 100644 --- a/src/core/renderers/webgl/filters/Filter.js +++ b/src/core/renderers/webgl/filters/Filter.js @@ -1,6 +1,7 @@ import extractUniformsFromSrc from './extractUniformsFromSrc'; import { uid } from '../../../utils'; import { BLEND_MODES } from '../../../const'; +import settings from '../../../settings'; const SOURCE_KEY_MAP = {}; @@ -10,7 +11,7 @@ * @memberof PIXI * @extends PIXI.Shader */ -class Filter +export default class Filter { /** * @param {string} [vertexSrc] - The source of the vertex shader. @@ -35,8 +36,6 @@ this.blendMode = BLEND_MODES.NORMAL; - // pull out the vertex and shader uniforms if they are not specified.. - // currently this does not extract structs only default types this.uniformData = uniforms || extractUniformsFromSrc(this.vertexSrc, this.fragmentSrc, 'projectionMatrix|uSampler'); /** @@ -80,7 +79,7 @@ * * @member {number} */ - this.resolution = 1; + this.resolution = settings.RESOLUTION; /** * If enabled is true the filter is applied, if false it will not. @@ -97,8 +96,11 @@ * @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 { // --- // // this.uniforms.filterMatrix = filterManager.calculateSpriteMatrix(tempMatrix, window.panda ); @@ -170,5 +172,3 @@ ].join('\n'); } } - -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 dc7072a..6c7422d 100644 --- a/src/core/renderers/webgl/managers/FilterManager.js +++ b/src/core/renderers/webgl/managers/FilterManager.js @@ -154,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 @@ -176,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; @@ -184,7 +184,7 @@ flop = t; } - filters[i].apply(this, flip, lastState.renderTarget, true); + filters[i].apply(this, flip, lastState.renderTarget, false, currentState); this.freePotRenderTarget(flip); this.freePotRenderTarget(flop); @@ -291,9 +291,13 @@ let textureCount = 1; let currentState; - if (shader.uniforms.data.filterArea) + // 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; @@ -306,9 +310,9 @@ // use this to clamp displaced texture coords so they belong to filterArea // see displacementFilter fragment shader for an example - if (shader.uniforms.data.filterClamp) + if (shader.uniforms.filterClamp) { - currentState = this.filterData.stack[this.filterData.index]; + currentState = currentState || this.filterData.stack[this.filterData.index]; const filterClamp = shader.uniforms.filterClamp; @@ -485,7 +489,7 @@ */ destroy() { - this.shaderCache = []; + this.shaderCache = {}; this.emptyPool(); } diff --git a/src/core/renderers/webgl/utils/RenderTarget.js b/src/core/renderers/webgl/utils/RenderTarget.js index afe51ab..6d2467f 100644 --- a/src/core/renderers/webgl/utils/RenderTarget.js +++ b/src/core/renderers/webgl/utils/RenderTarget.js @@ -124,7 +124,7 @@ * @default PIXI.settings.SCALE_MODE * @see PIXI.SCALE_MODES */ - this.scaleMode = scaleMode || settings.SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; /** * Whether this object is the root element or not diff --git a/package.json b/package.json index 1271fef..71ade15 100644 --- a/package.json +++ b/package.json @@ -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/scripts/release.js b/scripts/release.js index c4c8e36..156620c 100644 --- a/scripts/release.js +++ b/scripts/release.js @@ -12,7 +12,8 @@ 'dist/**/*', 'lib/**/*', 'src/**/*', - 'scripts/**/*', + 'scripts/*', + 'scripts/renders/*', 'test/**/*', '*.json', '*.md', diff --git a/scripts/renders/client.js b/scripts/renders/client.js index 16c9565..6faee34 100755 --- a/scripts/renders/client.js +++ b/scripts/renders/client.js @@ -1,6 +1,6 @@ 'use strict'; -require('../../bin/pixi'); +require('../../dist/pixi'); PIXI.utils.skipHello(); const fs = require('fs'); 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 bc730db..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. @@ -33,8 +33,11 @@ * need to call toDataUrl on the webgl context * @param {number} [options.resolution=1] - The resolution / device pixel ratio of the renderer, retina would be 2 * @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 @@ -43,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. */ @@ -73,7 +99,7 @@ */ stop() { - this.ticker.stop(); + this._ticker.stop(); } /** @@ -81,7 +107,7 @@ */ start() { - this.ticker.start(); + this._ticker.start(); } /** @@ -95,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 index 81e076b..78b17e6 100644 --- a/src/core/Shader.js +++ b/src/core/Shader.js @@ -1,9 +1,7 @@ import { GLShader } from 'pixi-gl-core'; import settings from './settings'; -const { PRECISION } = settings; - -function checkPrecision(src) +function checkPrecision(src, def) { if (src instanceof Array) { @@ -11,14 +9,14 @@ { const copy = src.slice(0); - copy.unshift(`precision ${PRECISION} float;`); + copy.unshift(`precision ${def} float;`); return copy; } } else if (src.substring(0, 9) !== 'precision') { - return `precision ${PRECISION} float;\n${src}`; + return `precision ${def} float;\n${src}`; } return src; @@ -42,6 +40,7 @@ */ constructor(gl, vertexSrc, fragmentSrc) { - super(gl, checkPrecision(vertexSrc), checkPrecision(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 3425b41..f9235e4 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -181,7 +181,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 b3792ef..17fde8d 100644 --- a/src/core/display/Container.js +++ b/src/core/display/Container.js @@ -73,13 +73,14 @@ } child.parent = this; - - // ensure a transform will be recalculated.. - this.transform._parentID = -1; - this._boundsID++; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.push(child); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(this.children.length - 1); child.emit('added', this); @@ -108,9 +109,14 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.splice(index, 0, child); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(index); child.emit('added', this); @@ -174,6 +180,7 @@ removeItems(this.children, currentIndex, 1); // remove from old position this.children.splice(index, 0, child); // add at new position + this.onChildrenChange(index); } @@ -220,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! @@ -244,9 +252,14 @@ { const child = this.getChildAt(index); + // ensure child transform will be recalculated.. child.parent = null; + child.transform._parentID = -1; removeItems(this.children, index, 1); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(index); child.emit('removed', this); @@ -275,8 +288,14 @@ for (let i = 0; i < removed.length; ++i) { removed[i].parent = null; + if (removed[i].transform) + { + removed[i].transform._parentID = -1; + } } + this._boundsID++; + this.onChildrenChange(beginIndex); for (let i = 0; i < removed.length; ++i) diff --git a/src/core/display/DisplayObject.js b/src/core/display/DisplayObject.js index 3affe36..4e6c77d 100644 --- a/src/core/display/DisplayObject.js +++ b/src/core/display/DisplayObject.js @@ -109,7 +109,7 @@ /** * The original, cached mask of the object * - * @member {PIXI.Rectangle} + * @member {PIXI.Graphics|PIXI.Sprite} * @private */ this._mask = null; diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index 95f2af4..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 ); @@ -1065,10 +1061,15 @@ canvasRenderer = new CanvasRenderer(); } - tempMatrix.tx = -bounds.x; - tempMatrix.ty = -bounds.y; + this.transform.updateLocalTransform(); + this.transform.localTransform.copy(tempMatrix); - canvasRenderer.render(this, canvasBuffer, false, tempMatrix); + tempMatrix.invert(); + + tempMatrix.tx -= bounds.x; + tempMatrix.ty -= bounds.y; + + canvasRenderer.render(this, canvasBuffer, true, tempMatrix); const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode); 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 e67680f..867dde2 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/buildCircle.js b/src/core/graphics/webgl/utils/buildCircle.js index 0400d13..abf5d02 100644 --- a/src/core/graphics/webgl/utils/buildCircle.js +++ b/src/core/graphics/webgl/utils/buildCircle.js @@ -33,6 +33,11 @@ height = circleData.height; } + if (width === 0 || height === 0) + { + return; + } + const totalSegs = Math.floor(30 * Math.sqrt(circleData.radius)) || Math.floor(15 * Math.sqrt(circleData.width + circleData.height)); diff --git a/src/core/graphics/webgl/utils/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 90155f9..f03fe15 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -30,6 +30,7 @@ export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; export { default as CanvasGraphicsRenderer } from './graphics/canvas/CanvasGraphicsRenderer'; +export { default as Spritesheet } from './textures/Spritesheet'; export { default as Texture } from './textures/Texture'; export { default as BaseTexture } from './textures/BaseTexture'; export { default as RenderTexture } from './textures/RenderTexture'; diff --git a/src/core/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 ccce680..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); } /** @@ -162,6 +162,9 @@ if (transform) { transform.copy(tempWt); + + // lets not forget to flag the parent transform as dirty... + this._tempDisplayObjectParent.transform._worldID = -1; } else { @@ -169,6 +172,7 @@ } displayObject.parent = this._tempDisplayObjectParent; + displayObject.updateTransform(); displayObject.parent = cacheParent; // displayObject.hitArea = //TODO add a temp hit area @@ -279,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/canvas/utils/CanvasRenderTarget.js b/src/core/renderers/canvas/utils/CanvasRenderTarget.js index 3de0aef..b409891 100644 --- a/src/core/renderers/canvas/utils/CanvasRenderTarget.js +++ b/src/core/renderers/canvas/utils/CanvasRenderTarget.js @@ -1,5 +1,4 @@ import settings from '../../../settings'; -const { RESOLUTION } = settings; /** * Creates a Canvas element of the given size. @@ -30,7 +29,7 @@ */ this.context = this.canvas.getContext('2d'); - this.resolution = resolution || RESOLUTION; + this.resolution = resolution || settings.RESOLUTION; this.resize(width, height); } 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 ce0e1c4..788ed6c 100644 --- a/src/core/renderers/webgl/WebGLRenderer.js +++ b/src/core/renderers/webgl/WebGLRenderer.js @@ -30,8 +30,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 @@ -49,10 +49,19 @@ * enable this if you need to call toDataUrl on the webgl context. * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when * rendering, stopping pixel interpolation. + * @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; + + if (this.legacy) + { + glCore.VertexArrayObject.FORCE_NATIVE = true; + } /** * The type of this renderer as a standardised const @@ -229,7 +238,7 @@ this.emit('context', gl); // setup the width/height properties and gl viewport - this.resize(this.width, this.height); + this.resize(this.screen.width, this.screen.height); } /** @@ -323,16 +332,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) { @@ -376,6 +385,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 @@ -440,9 +469,10 @@ * Changes the current shader to the one given in parameter * * @param {PIXI.Shader} shader - the new shader + * @param {boolean} [autoProject=true] - Whether automatically set the projection matrix * @return {PIXI.WebGLRenderer} Returns itself. */ - bindShader(shader) + bindShader(shader, autoProject) { // TODO cache if (this._activeShader !== shader) @@ -450,8 +480,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 bbab3b3..944f41d 100644 --- a/src/core/renderers/webgl/filters/Filter.js +++ b/src/core/renderers/webgl/filters/Filter.js @@ -1,6 +1,7 @@ import extractUniformsFromSrc from './extractUniformsFromSrc'; import { uid } from '../../../utils'; import { BLEND_MODES } from '../../../const'; +import settings from '../../../settings'; const SOURCE_KEY_MAP = {}; @@ -10,7 +11,7 @@ * @memberof PIXI * @extends PIXI.Shader */ -class Filter +export default class Filter { /** * @param {string} [vertexSrc] - The source of the vertex shader. @@ -35,8 +36,6 @@ this.blendMode = BLEND_MODES.NORMAL; - // pull out the vertex and shader uniforms if they are not specified.. - // currently this does not extract structs only default types this.uniformData = uniforms || extractUniformsFromSrc(this.vertexSrc, this.fragmentSrc, 'projectionMatrix|uSampler'); /** @@ -80,7 +79,7 @@ * * @member {number} */ - this.resolution = 1; + this.resolution = settings.RESOLUTION; /** * If enabled is true the filter is applied, if false it will not. @@ -97,8 +96,11 @@ * @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 { // --- // // this.uniforms.filterMatrix = filterManager.calculateSpriteMatrix(tempMatrix, window.panda ); @@ -170,5 +172,3 @@ ].join('\n'); } } - -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 dc7072a..6c7422d 100644 --- a/src/core/renderers/webgl/managers/FilterManager.js +++ b/src/core/renderers/webgl/managers/FilterManager.js @@ -154,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 @@ -176,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; @@ -184,7 +184,7 @@ flop = t; } - filters[i].apply(this, flip, lastState.renderTarget, true); + filters[i].apply(this, flip, lastState.renderTarget, false, currentState); this.freePotRenderTarget(flip); this.freePotRenderTarget(flop); @@ -291,9 +291,13 @@ let textureCount = 1; let currentState; - if (shader.uniforms.data.filterArea) + // 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; @@ -306,9 +310,9 @@ // use this to clamp displaced texture coords so they belong to filterArea // see displacementFilter fragment shader for an example - if (shader.uniforms.data.filterClamp) + if (shader.uniforms.filterClamp) { - currentState = this.filterData.stack[this.filterData.index]; + currentState = currentState || this.filterData.stack[this.filterData.index]; const filterClamp = shader.uniforms.filterClamp; @@ -485,7 +489,7 @@ */ destroy() { - this.shaderCache = []; + this.shaderCache = {}; this.emptyPool(); } diff --git a/src/core/renderers/webgl/utils/RenderTarget.js b/src/core/renderers/webgl/utils/RenderTarget.js index afe51ab..6d2467f 100644 --- a/src/core/renderers/webgl/utils/RenderTarget.js +++ b/src/core/renderers/webgl/utils/RenderTarget.js @@ -124,7 +124,7 @@ * @default PIXI.settings.SCALE_MODE * @see PIXI.SCALE_MODES */ - this.scaleMode = scaleMode || settings.SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; /** * Whether this object is the root element or not 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 1271fef..71ade15 100644 --- a/package.json +++ b/package.json @@ -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/scripts/release.js b/scripts/release.js index c4c8e36..156620c 100644 --- a/scripts/release.js +++ b/scripts/release.js @@ -12,7 +12,8 @@ 'dist/**/*', 'lib/**/*', 'src/**/*', - 'scripts/**/*', + 'scripts/*', + 'scripts/renders/*', 'test/**/*', '*.json', '*.md', diff --git a/scripts/renders/client.js b/scripts/renders/client.js index 16c9565..6faee34 100755 --- a/scripts/renders/client.js +++ b/scripts/renders/client.js @@ -1,6 +1,6 @@ 'use strict'; -require('../../bin/pixi'); +require('../../dist/pixi'); PIXI.utils.skipHello(); const fs = require('fs'); 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 bc730db..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. @@ -33,8 +33,11 @@ * need to call toDataUrl on the webgl context * @param {number} [options.resolution=1] - The resolution / device pixel ratio of the renderer, retina would be 2 * @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 @@ -43,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. */ @@ -73,7 +99,7 @@ */ stop() { - this.ticker.stop(); + this._ticker.stop(); } /** @@ -81,7 +107,7 @@ */ start() { - this.ticker.start(); + this._ticker.start(); } /** @@ -95,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 index 81e076b..78b17e6 100644 --- a/src/core/Shader.js +++ b/src/core/Shader.js @@ -1,9 +1,7 @@ import { GLShader } from 'pixi-gl-core'; import settings from './settings'; -const { PRECISION } = settings; - -function checkPrecision(src) +function checkPrecision(src, def) { if (src instanceof Array) { @@ -11,14 +9,14 @@ { const copy = src.slice(0); - copy.unshift(`precision ${PRECISION} float;`); + copy.unshift(`precision ${def} float;`); return copy; } } else if (src.substring(0, 9) !== 'precision') { - return `precision ${PRECISION} float;\n${src}`; + return `precision ${def} float;\n${src}`; } return src; @@ -42,6 +40,7 @@ */ constructor(gl, vertexSrc, fragmentSrc) { - super(gl, checkPrecision(vertexSrc), checkPrecision(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 3425b41..f9235e4 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -181,7 +181,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 b3792ef..17fde8d 100644 --- a/src/core/display/Container.js +++ b/src/core/display/Container.js @@ -73,13 +73,14 @@ } child.parent = this; - - // ensure a transform will be recalculated.. - this.transform._parentID = -1; - this._boundsID++; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.push(child); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(this.children.length - 1); child.emit('added', this); @@ -108,9 +109,14 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.splice(index, 0, child); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(index); child.emit('added', this); @@ -174,6 +180,7 @@ removeItems(this.children, currentIndex, 1); // remove from old position this.children.splice(index, 0, child); // add at new position + this.onChildrenChange(index); } @@ -220,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! @@ -244,9 +252,14 @@ { const child = this.getChildAt(index); + // ensure child transform will be recalculated.. child.parent = null; + child.transform._parentID = -1; removeItems(this.children, index, 1); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(index); child.emit('removed', this); @@ -275,8 +288,14 @@ for (let i = 0; i < removed.length; ++i) { removed[i].parent = null; + if (removed[i].transform) + { + removed[i].transform._parentID = -1; + } } + this._boundsID++; + this.onChildrenChange(beginIndex); for (let i = 0; i < removed.length; ++i) diff --git a/src/core/display/DisplayObject.js b/src/core/display/DisplayObject.js index 3affe36..4e6c77d 100644 --- a/src/core/display/DisplayObject.js +++ b/src/core/display/DisplayObject.js @@ -109,7 +109,7 @@ /** * The original, cached mask of the object * - * @member {PIXI.Rectangle} + * @member {PIXI.Graphics|PIXI.Sprite} * @private */ this._mask = null; diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index 95f2af4..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 ); @@ -1065,10 +1061,15 @@ canvasRenderer = new CanvasRenderer(); } - tempMatrix.tx = -bounds.x; - tempMatrix.ty = -bounds.y; + this.transform.updateLocalTransform(); + this.transform.localTransform.copy(tempMatrix); - canvasRenderer.render(this, canvasBuffer, false, tempMatrix); + tempMatrix.invert(); + + tempMatrix.tx -= bounds.x; + tempMatrix.ty -= bounds.y; + + canvasRenderer.render(this, canvasBuffer, true, tempMatrix); const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode); 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 e67680f..867dde2 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/buildCircle.js b/src/core/graphics/webgl/utils/buildCircle.js index 0400d13..abf5d02 100644 --- a/src/core/graphics/webgl/utils/buildCircle.js +++ b/src/core/graphics/webgl/utils/buildCircle.js @@ -33,6 +33,11 @@ height = circleData.height; } + if (width === 0 || height === 0) + { + return; + } + const totalSegs = Math.floor(30 * Math.sqrt(circleData.radius)) || Math.floor(15 * Math.sqrt(circleData.width + circleData.height)); diff --git a/src/core/graphics/webgl/utils/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 90155f9..f03fe15 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -30,6 +30,7 @@ export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; export { default as CanvasGraphicsRenderer } from './graphics/canvas/CanvasGraphicsRenderer'; +export { default as Spritesheet } from './textures/Spritesheet'; export { default as Texture } from './textures/Texture'; export { default as BaseTexture } from './textures/BaseTexture'; export { default as RenderTexture } from './textures/RenderTexture'; diff --git a/src/core/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 ccce680..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); } /** @@ -162,6 +162,9 @@ if (transform) { transform.copy(tempWt); + + // lets not forget to flag the parent transform as dirty... + this._tempDisplayObjectParent.transform._worldID = -1; } else { @@ -169,6 +172,7 @@ } displayObject.parent = this._tempDisplayObjectParent; + displayObject.updateTransform(); displayObject.parent = cacheParent; // displayObject.hitArea = //TODO add a temp hit area @@ -279,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/canvas/utils/CanvasRenderTarget.js b/src/core/renderers/canvas/utils/CanvasRenderTarget.js index 3de0aef..b409891 100644 --- a/src/core/renderers/canvas/utils/CanvasRenderTarget.js +++ b/src/core/renderers/canvas/utils/CanvasRenderTarget.js @@ -1,5 +1,4 @@ import settings from '../../../settings'; -const { RESOLUTION } = settings; /** * Creates a Canvas element of the given size. @@ -30,7 +29,7 @@ */ this.context = this.canvas.getContext('2d'); - this.resolution = resolution || RESOLUTION; + this.resolution = resolution || settings.RESOLUTION; this.resize(width, height); } 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 ce0e1c4..788ed6c 100644 --- a/src/core/renderers/webgl/WebGLRenderer.js +++ b/src/core/renderers/webgl/WebGLRenderer.js @@ -30,8 +30,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 @@ -49,10 +49,19 @@ * enable this if you need to call toDataUrl on the webgl context. * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when * rendering, stopping pixel interpolation. + * @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; + + if (this.legacy) + { + glCore.VertexArrayObject.FORCE_NATIVE = true; + } /** * The type of this renderer as a standardised const @@ -229,7 +238,7 @@ this.emit('context', gl); // setup the width/height properties and gl viewport - this.resize(this.width, this.height); + this.resize(this.screen.width, this.screen.height); } /** @@ -323,16 +332,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) { @@ -376,6 +385,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 @@ -440,9 +469,10 @@ * Changes the current shader to the one given in parameter * * @param {PIXI.Shader} shader - the new shader + * @param {boolean} [autoProject=true] - Whether automatically set the projection matrix * @return {PIXI.WebGLRenderer} Returns itself. */ - bindShader(shader) + bindShader(shader, autoProject) { // TODO cache if (this._activeShader !== shader) @@ -450,8 +480,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 bbab3b3..944f41d 100644 --- a/src/core/renderers/webgl/filters/Filter.js +++ b/src/core/renderers/webgl/filters/Filter.js @@ -1,6 +1,7 @@ import extractUniformsFromSrc from './extractUniformsFromSrc'; import { uid } from '../../../utils'; import { BLEND_MODES } from '../../../const'; +import settings from '../../../settings'; const SOURCE_KEY_MAP = {}; @@ -10,7 +11,7 @@ * @memberof PIXI * @extends PIXI.Shader */ -class Filter +export default class Filter { /** * @param {string} [vertexSrc] - The source of the vertex shader. @@ -35,8 +36,6 @@ this.blendMode = BLEND_MODES.NORMAL; - // pull out the vertex and shader uniforms if they are not specified.. - // currently this does not extract structs only default types this.uniformData = uniforms || extractUniformsFromSrc(this.vertexSrc, this.fragmentSrc, 'projectionMatrix|uSampler'); /** @@ -80,7 +79,7 @@ * * @member {number} */ - this.resolution = 1; + this.resolution = settings.RESOLUTION; /** * If enabled is true the filter is applied, if false it will not. @@ -97,8 +96,11 @@ * @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 { // --- // // this.uniforms.filterMatrix = filterManager.calculateSpriteMatrix(tempMatrix, window.panda ); @@ -170,5 +172,3 @@ ].join('\n'); } } - -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 dc7072a..6c7422d 100644 --- a/src/core/renderers/webgl/managers/FilterManager.js +++ b/src/core/renderers/webgl/managers/FilterManager.js @@ -154,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 @@ -176,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; @@ -184,7 +184,7 @@ flop = t; } - filters[i].apply(this, flip, lastState.renderTarget, true); + filters[i].apply(this, flip, lastState.renderTarget, false, currentState); this.freePotRenderTarget(flip); this.freePotRenderTarget(flop); @@ -291,9 +291,13 @@ let textureCount = 1; let currentState; - if (shader.uniforms.data.filterArea) + // 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; @@ -306,9 +310,9 @@ // use this to clamp displaced texture coords so they belong to filterArea // see displacementFilter fragment shader for an example - if (shader.uniforms.data.filterClamp) + if (shader.uniforms.filterClamp) { - currentState = this.filterData.stack[this.filterData.index]; + currentState = currentState || this.filterData.stack[this.filterData.index]; const filterClamp = shader.uniforms.filterClamp; @@ -485,7 +489,7 @@ */ destroy() { - this.shaderCache = []; + this.shaderCache = {}; this.emptyPool(); } diff --git a/src/core/renderers/webgl/utils/RenderTarget.js b/src/core/renderers/webgl/utils/RenderTarget.js index afe51ab..6d2467f 100644 --- a/src/core/renderers/webgl/utils/RenderTarget.js +++ b/src/core/renderers/webgl/utils/RenderTarget.js @@ -124,7 +124,7 @@ * @default PIXI.settings.SCALE_MODE * @see PIXI.SCALE_MODES */ - this.scaleMode = scaleMode || settings.SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; /** * Whether this object is the root element or not 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 36ea5bb..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; @@ -346,8 +346,8 @@ /** * Gets the local bounds of the sprite object. * - * @param {Rectangle} rect - The output rectangle. - * @return {Rectangle} The bounds. + * @param {PIXI.Rectangle} rect - The output rectangle. + * @return {PIXI.Rectangle} The bounds. */ getLocalBounds(rect) { @@ -440,7 +440,7 @@ * * @static * @param {number|string|PIXI.BaseTexture|HTMLCanvasElement|HTMLVideoElement} source Source to create texture from - * @return {PIXI.Texture} 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 1271fef..71ade15 100644 --- a/package.json +++ b/package.json @@ -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/scripts/release.js b/scripts/release.js index c4c8e36..156620c 100644 --- a/scripts/release.js +++ b/scripts/release.js @@ -12,7 +12,8 @@ 'dist/**/*', 'lib/**/*', 'src/**/*', - 'scripts/**/*', + 'scripts/*', + 'scripts/renders/*', 'test/**/*', '*.json', '*.md', diff --git a/scripts/renders/client.js b/scripts/renders/client.js index 16c9565..6faee34 100755 --- a/scripts/renders/client.js +++ b/scripts/renders/client.js @@ -1,6 +1,6 @@ 'use strict'; -require('../../bin/pixi'); +require('../../dist/pixi'); PIXI.utils.skipHello(); const fs = require('fs'); 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 bc730db..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. @@ -33,8 +33,11 @@ * need to call toDataUrl on the webgl context * @param {number} [options.resolution=1] - The resolution / device pixel ratio of the renderer, retina would be 2 * @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 @@ -43,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. */ @@ -73,7 +99,7 @@ */ stop() { - this.ticker.stop(); + this._ticker.stop(); } /** @@ -81,7 +107,7 @@ */ start() { - this.ticker.start(); + this._ticker.start(); } /** @@ -95,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 index 81e076b..78b17e6 100644 --- a/src/core/Shader.js +++ b/src/core/Shader.js @@ -1,9 +1,7 @@ import { GLShader } from 'pixi-gl-core'; import settings from './settings'; -const { PRECISION } = settings; - -function checkPrecision(src) +function checkPrecision(src, def) { if (src instanceof Array) { @@ -11,14 +9,14 @@ { const copy = src.slice(0); - copy.unshift(`precision ${PRECISION} float;`); + copy.unshift(`precision ${def} float;`); return copy; } } else if (src.substring(0, 9) !== 'precision') { - return `precision ${PRECISION} float;\n${src}`; + return `precision ${def} float;\n${src}`; } return src; @@ -42,6 +40,7 @@ */ constructor(gl, vertexSrc, fragmentSrc) { - super(gl, checkPrecision(vertexSrc), checkPrecision(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 3425b41..f9235e4 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -181,7 +181,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 b3792ef..17fde8d 100644 --- a/src/core/display/Container.js +++ b/src/core/display/Container.js @@ -73,13 +73,14 @@ } child.parent = this; - - // ensure a transform will be recalculated.. - this.transform._parentID = -1; - this._boundsID++; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.push(child); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(this.children.length - 1); child.emit('added', this); @@ -108,9 +109,14 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.splice(index, 0, child); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(index); child.emit('added', this); @@ -174,6 +180,7 @@ removeItems(this.children, currentIndex, 1); // remove from old position this.children.splice(index, 0, child); // add at new position + this.onChildrenChange(index); } @@ -220,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! @@ -244,9 +252,14 @@ { const child = this.getChildAt(index); + // ensure child transform will be recalculated.. child.parent = null; + child.transform._parentID = -1; removeItems(this.children, index, 1); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(index); child.emit('removed', this); @@ -275,8 +288,14 @@ for (let i = 0; i < removed.length; ++i) { removed[i].parent = null; + if (removed[i].transform) + { + removed[i].transform._parentID = -1; + } } + this._boundsID++; + this.onChildrenChange(beginIndex); for (let i = 0; i < removed.length; ++i) diff --git a/src/core/display/DisplayObject.js b/src/core/display/DisplayObject.js index 3affe36..4e6c77d 100644 --- a/src/core/display/DisplayObject.js +++ b/src/core/display/DisplayObject.js @@ -109,7 +109,7 @@ /** * The original, cached mask of the object * - * @member {PIXI.Rectangle} + * @member {PIXI.Graphics|PIXI.Sprite} * @private */ this._mask = null; diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index 95f2af4..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 ); @@ -1065,10 +1061,15 @@ canvasRenderer = new CanvasRenderer(); } - tempMatrix.tx = -bounds.x; - tempMatrix.ty = -bounds.y; + this.transform.updateLocalTransform(); + this.transform.localTransform.copy(tempMatrix); - canvasRenderer.render(this, canvasBuffer, false, tempMatrix); + tempMatrix.invert(); + + tempMatrix.tx -= bounds.x; + tempMatrix.ty -= bounds.y; + + canvasRenderer.render(this, canvasBuffer, true, tempMatrix); const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode); 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 e67680f..867dde2 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/buildCircle.js b/src/core/graphics/webgl/utils/buildCircle.js index 0400d13..abf5d02 100644 --- a/src/core/graphics/webgl/utils/buildCircle.js +++ b/src/core/graphics/webgl/utils/buildCircle.js @@ -33,6 +33,11 @@ height = circleData.height; } + if (width === 0 || height === 0) + { + return; + } + const totalSegs = Math.floor(30 * Math.sqrt(circleData.radius)) || Math.floor(15 * Math.sqrt(circleData.width + circleData.height)); diff --git a/src/core/graphics/webgl/utils/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 90155f9..f03fe15 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -30,6 +30,7 @@ export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; export { default as CanvasGraphicsRenderer } from './graphics/canvas/CanvasGraphicsRenderer'; +export { default as Spritesheet } from './textures/Spritesheet'; export { default as Texture } from './textures/Texture'; export { default as BaseTexture } from './textures/BaseTexture'; export { default as RenderTexture } from './textures/RenderTexture'; diff --git a/src/core/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 ccce680..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); } /** @@ -162,6 +162,9 @@ if (transform) { transform.copy(tempWt); + + // lets not forget to flag the parent transform as dirty... + this._tempDisplayObjectParent.transform._worldID = -1; } else { @@ -169,6 +172,7 @@ } displayObject.parent = this._tempDisplayObjectParent; + displayObject.updateTransform(); displayObject.parent = cacheParent; // displayObject.hitArea = //TODO add a temp hit area @@ -279,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/canvas/utils/CanvasRenderTarget.js b/src/core/renderers/canvas/utils/CanvasRenderTarget.js index 3de0aef..b409891 100644 --- a/src/core/renderers/canvas/utils/CanvasRenderTarget.js +++ b/src/core/renderers/canvas/utils/CanvasRenderTarget.js @@ -1,5 +1,4 @@ import settings from '../../../settings'; -const { RESOLUTION } = settings; /** * Creates a Canvas element of the given size. @@ -30,7 +29,7 @@ */ this.context = this.canvas.getContext('2d'); - this.resolution = resolution || RESOLUTION; + this.resolution = resolution || settings.RESOLUTION; this.resize(width, height); } 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 ce0e1c4..788ed6c 100644 --- a/src/core/renderers/webgl/WebGLRenderer.js +++ b/src/core/renderers/webgl/WebGLRenderer.js @@ -30,8 +30,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 @@ -49,10 +49,19 @@ * enable this if you need to call toDataUrl on the webgl context. * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when * rendering, stopping pixel interpolation. + * @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; + + if (this.legacy) + { + glCore.VertexArrayObject.FORCE_NATIVE = true; + } /** * The type of this renderer as a standardised const @@ -229,7 +238,7 @@ this.emit('context', gl); // setup the width/height properties and gl viewport - this.resize(this.width, this.height); + this.resize(this.screen.width, this.screen.height); } /** @@ -323,16 +332,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) { @@ -376,6 +385,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 @@ -440,9 +469,10 @@ * Changes the current shader to the one given in parameter * * @param {PIXI.Shader} shader - the new shader + * @param {boolean} [autoProject=true] - Whether automatically set the projection matrix * @return {PIXI.WebGLRenderer} Returns itself. */ - bindShader(shader) + bindShader(shader, autoProject) { // TODO cache if (this._activeShader !== shader) @@ -450,8 +480,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 bbab3b3..944f41d 100644 --- a/src/core/renderers/webgl/filters/Filter.js +++ b/src/core/renderers/webgl/filters/Filter.js @@ -1,6 +1,7 @@ import extractUniformsFromSrc from './extractUniformsFromSrc'; import { uid } from '../../../utils'; import { BLEND_MODES } from '../../../const'; +import settings from '../../../settings'; const SOURCE_KEY_MAP = {}; @@ -10,7 +11,7 @@ * @memberof PIXI * @extends PIXI.Shader */ -class Filter +export default class Filter { /** * @param {string} [vertexSrc] - The source of the vertex shader. @@ -35,8 +36,6 @@ this.blendMode = BLEND_MODES.NORMAL; - // pull out the vertex and shader uniforms if they are not specified.. - // currently this does not extract structs only default types this.uniformData = uniforms || extractUniformsFromSrc(this.vertexSrc, this.fragmentSrc, 'projectionMatrix|uSampler'); /** @@ -80,7 +79,7 @@ * * @member {number} */ - this.resolution = 1; + this.resolution = settings.RESOLUTION; /** * If enabled is true the filter is applied, if false it will not. @@ -97,8 +96,11 @@ * @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 { // --- // // this.uniforms.filterMatrix = filterManager.calculateSpriteMatrix(tempMatrix, window.panda ); @@ -170,5 +172,3 @@ ].join('\n'); } } - -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 dc7072a..6c7422d 100644 --- a/src/core/renderers/webgl/managers/FilterManager.js +++ b/src/core/renderers/webgl/managers/FilterManager.js @@ -154,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 @@ -176,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; @@ -184,7 +184,7 @@ flop = t; } - filters[i].apply(this, flip, lastState.renderTarget, true); + filters[i].apply(this, flip, lastState.renderTarget, false, currentState); this.freePotRenderTarget(flip); this.freePotRenderTarget(flop); @@ -291,9 +291,13 @@ let textureCount = 1; let currentState; - if (shader.uniforms.data.filterArea) + // 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; @@ -306,9 +310,9 @@ // use this to clamp displaced texture coords so they belong to filterArea // see displacementFilter fragment shader for an example - if (shader.uniforms.data.filterClamp) + if (shader.uniforms.filterClamp) { - currentState = this.filterData.stack[this.filterData.index]; + currentState = currentState || this.filterData.stack[this.filterData.index]; const filterClamp = shader.uniforms.filterClamp; @@ -485,7 +489,7 @@ */ destroy() { - this.shaderCache = []; + this.shaderCache = {}; this.emptyPool(); } diff --git a/src/core/renderers/webgl/utils/RenderTarget.js b/src/core/renderers/webgl/utils/RenderTarget.js index afe51ab..6d2467f 100644 --- a/src/core/renderers/webgl/utils/RenderTarget.js +++ b/src/core/renderers/webgl/utils/RenderTarget.js @@ -124,7 +124,7 @@ * @default PIXI.settings.SCALE_MODE * @see PIXI.SCALE_MODES */ - this.scaleMode = scaleMode || settings.SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; /** * Whether this object is the root element or not 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 36ea5bb..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; @@ -346,8 +346,8 @@ /** * Gets the local bounds of the sprite object. * - * @param {Rectangle} rect - The output rectangle. - * @return {Rectangle} The bounds. + * @param {PIXI.Rectangle} rect - The output rectangle. + * @return {PIXI.Rectangle} The bounds. */ getLocalBounds(rect) { @@ -440,7 +440,7 @@ * * @static * @param {number|string|PIXI.BaseTexture|HTMLCanvasElement|HTMLVideoElement} source Source to create texture from - * @return {PIXI.Texture} 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 1271fef..71ade15 100644 --- a/package.json +++ b/package.json @@ -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/scripts/release.js b/scripts/release.js index c4c8e36..156620c 100644 --- a/scripts/release.js +++ b/scripts/release.js @@ -12,7 +12,8 @@ 'dist/**/*', 'lib/**/*', 'src/**/*', - 'scripts/**/*', + 'scripts/*', + 'scripts/renders/*', 'test/**/*', '*.json', '*.md', diff --git a/scripts/renders/client.js b/scripts/renders/client.js index 16c9565..6faee34 100755 --- a/scripts/renders/client.js +++ b/scripts/renders/client.js @@ -1,6 +1,6 @@ 'use strict'; -require('../../bin/pixi'); +require('../../dist/pixi'); PIXI.utils.skipHello(); const fs = require('fs'); 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 bc730db..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. @@ -33,8 +33,11 @@ * need to call toDataUrl on the webgl context * @param {number} [options.resolution=1] - The resolution / device pixel ratio of the renderer, retina would be 2 * @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 @@ -43,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. */ @@ -73,7 +99,7 @@ */ stop() { - this.ticker.stop(); + this._ticker.stop(); } /** @@ -81,7 +107,7 @@ */ start() { - this.ticker.start(); + this._ticker.start(); } /** @@ -95,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 index 81e076b..78b17e6 100644 --- a/src/core/Shader.js +++ b/src/core/Shader.js @@ -1,9 +1,7 @@ import { GLShader } from 'pixi-gl-core'; import settings from './settings'; -const { PRECISION } = settings; - -function checkPrecision(src) +function checkPrecision(src, def) { if (src instanceof Array) { @@ -11,14 +9,14 @@ { const copy = src.slice(0); - copy.unshift(`precision ${PRECISION} float;`); + copy.unshift(`precision ${def} float;`); return copy; } } else if (src.substring(0, 9) !== 'precision') { - return `precision ${PRECISION} float;\n${src}`; + return `precision ${def} float;\n${src}`; } return src; @@ -42,6 +40,7 @@ */ constructor(gl, vertexSrc, fragmentSrc) { - super(gl, checkPrecision(vertexSrc), checkPrecision(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 3425b41..f9235e4 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -181,7 +181,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 b3792ef..17fde8d 100644 --- a/src/core/display/Container.js +++ b/src/core/display/Container.js @@ -73,13 +73,14 @@ } child.parent = this; - - // ensure a transform will be recalculated.. - this.transform._parentID = -1; - this._boundsID++; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.push(child); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(this.children.length - 1); child.emit('added', this); @@ -108,9 +109,14 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.splice(index, 0, child); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(index); child.emit('added', this); @@ -174,6 +180,7 @@ removeItems(this.children, currentIndex, 1); // remove from old position this.children.splice(index, 0, child); // add at new position + this.onChildrenChange(index); } @@ -220,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! @@ -244,9 +252,14 @@ { const child = this.getChildAt(index); + // ensure child transform will be recalculated.. child.parent = null; + child.transform._parentID = -1; removeItems(this.children, index, 1); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(index); child.emit('removed', this); @@ -275,8 +288,14 @@ for (let i = 0; i < removed.length; ++i) { removed[i].parent = null; + if (removed[i].transform) + { + removed[i].transform._parentID = -1; + } } + this._boundsID++; + this.onChildrenChange(beginIndex); for (let i = 0; i < removed.length; ++i) diff --git a/src/core/display/DisplayObject.js b/src/core/display/DisplayObject.js index 3affe36..4e6c77d 100644 --- a/src/core/display/DisplayObject.js +++ b/src/core/display/DisplayObject.js @@ -109,7 +109,7 @@ /** * The original, cached mask of the object * - * @member {PIXI.Rectangle} + * @member {PIXI.Graphics|PIXI.Sprite} * @private */ this._mask = null; diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index 95f2af4..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 ); @@ -1065,10 +1061,15 @@ canvasRenderer = new CanvasRenderer(); } - tempMatrix.tx = -bounds.x; - tempMatrix.ty = -bounds.y; + this.transform.updateLocalTransform(); + this.transform.localTransform.copy(tempMatrix); - canvasRenderer.render(this, canvasBuffer, false, tempMatrix); + tempMatrix.invert(); + + tempMatrix.tx -= bounds.x; + tempMatrix.ty -= bounds.y; + + canvasRenderer.render(this, canvasBuffer, true, tempMatrix); const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode); 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 e67680f..867dde2 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/buildCircle.js b/src/core/graphics/webgl/utils/buildCircle.js index 0400d13..abf5d02 100644 --- a/src/core/graphics/webgl/utils/buildCircle.js +++ b/src/core/graphics/webgl/utils/buildCircle.js @@ -33,6 +33,11 @@ height = circleData.height; } + if (width === 0 || height === 0) + { + return; + } + const totalSegs = Math.floor(30 * Math.sqrt(circleData.radius)) || Math.floor(15 * Math.sqrt(circleData.width + circleData.height)); diff --git a/src/core/graphics/webgl/utils/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 90155f9..f03fe15 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -30,6 +30,7 @@ export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; export { default as CanvasGraphicsRenderer } from './graphics/canvas/CanvasGraphicsRenderer'; +export { default as Spritesheet } from './textures/Spritesheet'; export { default as Texture } from './textures/Texture'; export { default as BaseTexture } from './textures/BaseTexture'; export { default as RenderTexture } from './textures/RenderTexture'; diff --git a/src/core/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 ccce680..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); } /** @@ -162,6 +162,9 @@ if (transform) { transform.copy(tempWt); + + // lets not forget to flag the parent transform as dirty... + this._tempDisplayObjectParent.transform._worldID = -1; } else { @@ -169,6 +172,7 @@ } displayObject.parent = this._tempDisplayObjectParent; + displayObject.updateTransform(); displayObject.parent = cacheParent; // displayObject.hitArea = //TODO add a temp hit area @@ -279,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/canvas/utils/CanvasRenderTarget.js b/src/core/renderers/canvas/utils/CanvasRenderTarget.js index 3de0aef..b409891 100644 --- a/src/core/renderers/canvas/utils/CanvasRenderTarget.js +++ b/src/core/renderers/canvas/utils/CanvasRenderTarget.js @@ -1,5 +1,4 @@ import settings from '../../../settings'; -const { RESOLUTION } = settings; /** * Creates a Canvas element of the given size. @@ -30,7 +29,7 @@ */ this.context = this.canvas.getContext('2d'); - this.resolution = resolution || RESOLUTION; + this.resolution = resolution || settings.RESOLUTION; this.resize(width, height); } 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 ce0e1c4..788ed6c 100644 --- a/src/core/renderers/webgl/WebGLRenderer.js +++ b/src/core/renderers/webgl/WebGLRenderer.js @@ -30,8 +30,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 @@ -49,10 +49,19 @@ * enable this if you need to call toDataUrl on the webgl context. * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when * rendering, stopping pixel interpolation. + * @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; + + if (this.legacy) + { + glCore.VertexArrayObject.FORCE_NATIVE = true; + } /** * The type of this renderer as a standardised const @@ -229,7 +238,7 @@ this.emit('context', gl); // setup the width/height properties and gl viewport - this.resize(this.width, this.height); + this.resize(this.screen.width, this.screen.height); } /** @@ -323,16 +332,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) { @@ -376,6 +385,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 @@ -440,9 +469,10 @@ * Changes the current shader to the one given in parameter * * @param {PIXI.Shader} shader - the new shader + * @param {boolean} [autoProject=true] - Whether automatically set the projection matrix * @return {PIXI.WebGLRenderer} Returns itself. */ - bindShader(shader) + bindShader(shader, autoProject) { // TODO cache if (this._activeShader !== shader) @@ -450,8 +480,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 bbab3b3..944f41d 100644 --- a/src/core/renderers/webgl/filters/Filter.js +++ b/src/core/renderers/webgl/filters/Filter.js @@ -1,6 +1,7 @@ import extractUniformsFromSrc from './extractUniformsFromSrc'; import { uid } from '../../../utils'; import { BLEND_MODES } from '../../../const'; +import settings from '../../../settings'; const SOURCE_KEY_MAP = {}; @@ -10,7 +11,7 @@ * @memberof PIXI * @extends PIXI.Shader */ -class Filter +export default class Filter { /** * @param {string} [vertexSrc] - The source of the vertex shader. @@ -35,8 +36,6 @@ this.blendMode = BLEND_MODES.NORMAL; - // pull out the vertex and shader uniforms if they are not specified.. - // currently this does not extract structs only default types this.uniformData = uniforms || extractUniformsFromSrc(this.vertexSrc, this.fragmentSrc, 'projectionMatrix|uSampler'); /** @@ -80,7 +79,7 @@ * * @member {number} */ - this.resolution = 1; + this.resolution = settings.RESOLUTION; /** * If enabled is true the filter is applied, if false it will not. @@ -97,8 +96,11 @@ * @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 { // --- // // this.uniforms.filterMatrix = filterManager.calculateSpriteMatrix(tempMatrix, window.panda ); @@ -170,5 +172,3 @@ ].join('\n'); } } - -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 dc7072a..6c7422d 100644 --- a/src/core/renderers/webgl/managers/FilterManager.js +++ b/src/core/renderers/webgl/managers/FilterManager.js @@ -154,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 @@ -176,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; @@ -184,7 +184,7 @@ flop = t; } - filters[i].apply(this, flip, lastState.renderTarget, true); + filters[i].apply(this, flip, lastState.renderTarget, false, currentState); this.freePotRenderTarget(flip); this.freePotRenderTarget(flop); @@ -291,9 +291,13 @@ let textureCount = 1; let currentState; - if (shader.uniforms.data.filterArea) + // 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; @@ -306,9 +310,9 @@ // use this to clamp displaced texture coords so they belong to filterArea // see displacementFilter fragment shader for an example - if (shader.uniforms.data.filterClamp) + if (shader.uniforms.filterClamp) { - currentState = this.filterData.stack[this.filterData.index]; + currentState = currentState || this.filterData.stack[this.filterData.index]; const filterClamp = shader.uniforms.filterClamp; @@ -485,7 +489,7 @@ */ destroy() { - this.shaderCache = []; + this.shaderCache = {}; this.emptyPool(); } diff --git a/src/core/renderers/webgl/utils/RenderTarget.js b/src/core/renderers/webgl/utils/RenderTarget.js index afe51ab..6d2467f 100644 --- a/src/core/renderers/webgl/utils/RenderTarget.js +++ b/src/core/renderers/webgl/utils/RenderTarget.js @@ -124,7 +124,7 @@ * @default PIXI.settings.SCALE_MODE * @see PIXI.SCALE_MODES */ - this.scaleMode = scaleMode || settings.SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; /** * Whether this object is the root element or not 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 36ea5bb..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; @@ -346,8 +346,8 @@ /** * Gets the local bounds of the sprite object. * - * @param {Rectangle} rect - The output rectangle. - * @return {Rectangle} The bounds. + * @param {PIXI.Rectangle} rect - The output rectangle. + * @return {PIXI.Rectangle} The bounds. */ getLocalBounds(rect) { @@ -440,7 +440,7 @@ * * @static * @param {number|string|PIXI.BaseTexture|HTMLCanvasElement|HTMLVideoElement} source Source to create texture from - * @return {PIXI.Texture} 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 e77b39a..90b08c0 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++) @@ -103,11 +102,18 @@ { const gl = this.renderer.gl; - // step 1: first check max textures the GPU can handle. - this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); + if (this.renderer.legacy) + { + this.MAX_TEXTURES = 1; + } + else + { + // step 1: first check max textures the GPU can handle. + this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); - // step 2: check the maximum number of if statements the shader can have too.. - this.MAX_TEXTURES = checkMaxIfStatmentsInShader(this.MAX_TEXTURES, gl); + // step 2: check the maximum number of if statements the shader can have too.. + this.MAX_TEXTURES = checkMaxIfStatmentsInShader(this.MAX_TEXTURES, gl); + } const shader = this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); @@ -130,8 +136,12 @@ .addIndex(this.indexBuffer) .addAttribute(this.vertexBuffers[i], shader.attributes.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) .addAttribute(this.vertexBuffers[i], shader.attributes.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) - .addAttribute(this.vertexBuffers[i], shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4) - .addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + .addAttribute(this.vertexBuffers[i], shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); + + if (shader.attributes.aTextureId) + { + this.vaos[i].addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + } /* eslint-enable max-len */ } @@ -378,8 +388,12 @@ .addIndex(this.indexBuffer) .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) - .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4) - .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); + + if (this.shader.attributes.aTextureId) + { + this.vaos[this.vertexCount].addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + } /* eslint-enable max-len */ } diff --git a/package.json b/package.json index 1271fef..71ade15 100644 --- a/package.json +++ b/package.json @@ -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/scripts/release.js b/scripts/release.js index c4c8e36..156620c 100644 --- a/scripts/release.js +++ b/scripts/release.js @@ -12,7 +12,8 @@ 'dist/**/*', 'lib/**/*', 'src/**/*', - 'scripts/**/*', + 'scripts/*', + 'scripts/renders/*', 'test/**/*', '*.json', '*.md', diff --git a/scripts/renders/client.js b/scripts/renders/client.js index 16c9565..6faee34 100755 --- a/scripts/renders/client.js +++ b/scripts/renders/client.js @@ -1,6 +1,6 @@ 'use strict'; -require('../../bin/pixi'); +require('../../dist/pixi'); PIXI.utils.skipHello(); const fs = require('fs'); 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 bc730db..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. @@ -33,8 +33,11 @@ * need to call toDataUrl on the webgl context * @param {number} [options.resolution=1] - The resolution / device pixel ratio of the renderer, retina would be 2 * @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 @@ -43,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. */ @@ -73,7 +99,7 @@ */ stop() { - this.ticker.stop(); + this._ticker.stop(); } /** @@ -81,7 +107,7 @@ */ start() { - this.ticker.start(); + this._ticker.start(); } /** @@ -95,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 index 81e076b..78b17e6 100644 --- a/src/core/Shader.js +++ b/src/core/Shader.js @@ -1,9 +1,7 @@ import { GLShader } from 'pixi-gl-core'; import settings from './settings'; -const { PRECISION } = settings; - -function checkPrecision(src) +function checkPrecision(src, def) { if (src instanceof Array) { @@ -11,14 +9,14 @@ { const copy = src.slice(0); - copy.unshift(`precision ${PRECISION} float;`); + copy.unshift(`precision ${def} float;`); return copy; } } else if (src.substring(0, 9) !== 'precision') { - return `precision ${PRECISION} float;\n${src}`; + return `precision ${def} float;\n${src}`; } return src; @@ -42,6 +40,7 @@ */ constructor(gl, vertexSrc, fragmentSrc) { - super(gl, checkPrecision(vertexSrc), checkPrecision(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 3425b41..f9235e4 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -181,7 +181,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 b3792ef..17fde8d 100644 --- a/src/core/display/Container.js +++ b/src/core/display/Container.js @@ -73,13 +73,14 @@ } child.parent = this; - - // ensure a transform will be recalculated.. - this.transform._parentID = -1; - this._boundsID++; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.push(child); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(this.children.length - 1); child.emit('added', this); @@ -108,9 +109,14 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.splice(index, 0, child); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(index); child.emit('added', this); @@ -174,6 +180,7 @@ removeItems(this.children, currentIndex, 1); // remove from old position this.children.splice(index, 0, child); // add at new position + this.onChildrenChange(index); } @@ -220,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! @@ -244,9 +252,14 @@ { const child = this.getChildAt(index); + // ensure child transform will be recalculated.. child.parent = null; + child.transform._parentID = -1; removeItems(this.children, index, 1); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(index); child.emit('removed', this); @@ -275,8 +288,14 @@ for (let i = 0; i < removed.length; ++i) { removed[i].parent = null; + if (removed[i].transform) + { + removed[i].transform._parentID = -1; + } } + this._boundsID++; + this.onChildrenChange(beginIndex); for (let i = 0; i < removed.length; ++i) diff --git a/src/core/display/DisplayObject.js b/src/core/display/DisplayObject.js index 3affe36..4e6c77d 100644 --- a/src/core/display/DisplayObject.js +++ b/src/core/display/DisplayObject.js @@ -109,7 +109,7 @@ /** * The original, cached mask of the object * - * @member {PIXI.Rectangle} + * @member {PIXI.Graphics|PIXI.Sprite} * @private */ this._mask = null; diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index 95f2af4..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 ); @@ -1065,10 +1061,15 @@ canvasRenderer = new CanvasRenderer(); } - tempMatrix.tx = -bounds.x; - tempMatrix.ty = -bounds.y; + this.transform.updateLocalTransform(); + this.transform.localTransform.copy(tempMatrix); - canvasRenderer.render(this, canvasBuffer, false, tempMatrix); + tempMatrix.invert(); + + tempMatrix.tx -= bounds.x; + tempMatrix.ty -= bounds.y; + + canvasRenderer.render(this, canvasBuffer, true, tempMatrix); const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode); 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 e67680f..867dde2 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/buildCircle.js b/src/core/graphics/webgl/utils/buildCircle.js index 0400d13..abf5d02 100644 --- a/src/core/graphics/webgl/utils/buildCircle.js +++ b/src/core/graphics/webgl/utils/buildCircle.js @@ -33,6 +33,11 @@ height = circleData.height; } + if (width === 0 || height === 0) + { + return; + } + const totalSegs = Math.floor(30 * Math.sqrt(circleData.radius)) || Math.floor(15 * Math.sqrt(circleData.width + circleData.height)); diff --git a/src/core/graphics/webgl/utils/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 90155f9..f03fe15 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -30,6 +30,7 @@ export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; export { default as CanvasGraphicsRenderer } from './graphics/canvas/CanvasGraphicsRenderer'; +export { default as Spritesheet } from './textures/Spritesheet'; export { default as Texture } from './textures/Texture'; export { default as BaseTexture } from './textures/BaseTexture'; export { default as RenderTexture } from './textures/RenderTexture'; diff --git a/src/core/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 ccce680..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); } /** @@ -162,6 +162,9 @@ if (transform) { transform.copy(tempWt); + + // lets not forget to flag the parent transform as dirty... + this._tempDisplayObjectParent.transform._worldID = -1; } else { @@ -169,6 +172,7 @@ } displayObject.parent = this._tempDisplayObjectParent; + displayObject.updateTransform(); displayObject.parent = cacheParent; // displayObject.hitArea = //TODO add a temp hit area @@ -279,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/canvas/utils/CanvasRenderTarget.js b/src/core/renderers/canvas/utils/CanvasRenderTarget.js index 3de0aef..b409891 100644 --- a/src/core/renderers/canvas/utils/CanvasRenderTarget.js +++ b/src/core/renderers/canvas/utils/CanvasRenderTarget.js @@ -1,5 +1,4 @@ import settings from '../../../settings'; -const { RESOLUTION } = settings; /** * Creates a Canvas element of the given size. @@ -30,7 +29,7 @@ */ this.context = this.canvas.getContext('2d'); - this.resolution = resolution || RESOLUTION; + this.resolution = resolution || settings.RESOLUTION; this.resize(width, height); } 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 ce0e1c4..788ed6c 100644 --- a/src/core/renderers/webgl/WebGLRenderer.js +++ b/src/core/renderers/webgl/WebGLRenderer.js @@ -30,8 +30,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 @@ -49,10 +49,19 @@ * enable this if you need to call toDataUrl on the webgl context. * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when * rendering, stopping pixel interpolation. + * @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; + + if (this.legacy) + { + glCore.VertexArrayObject.FORCE_NATIVE = true; + } /** * The type of this renderer as a standardised const @@ -229,7 +238,7 @@ this.emit('context', gl); // setup the width/height properties and gl viewport - this.resize(this.width, this.height); + this.resize(this.screen.width, this.screen.height); } /** @@ -323,16 +332,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) { @@ -376,6 +385,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 @@ -440,9 +469,10 @@ * Changes the current shader to the one given in parameter * * @param {PIXI.Shader} shader - the new shader + * @param {boolean} [autoProject=true] - Whether automatically set the projection matrix * @return {PIXI.WebGLRenderer} Returns itself. */ - bindShader(shader) + bindShader(shader, autoProject) { // TODO cache if (this._activeShader !== shader) @@ -450,8 +480,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 bbab3b3..944f41d 100644 --- a/src/core/renderers/webgl/filters/Filter.js +++ b/src/core/renderers/webgl/filters/Filter.js @@ -1,6 +1,7 @@ import extractUniformsFromSrc from './extractUniformsFromSrc'; import { uid } from '../../../utils'; import { BLEND_MODES } from '../../../const'; +import settings from '../../../settings'; const SOURCE_KEY_MAP = {}; @@ -10,7 +11,7 @@ * @memberof PIXI * @extends PIXI.Shader */ -class Filter +export default class Filter { /** * @param {string} [vertexSrc] - The source of the vertex shader. @@ -35,8 +36,6 @@ this.blendMode = BLEND_MODES.NORMAL; - // pull out the vertex and shader uniforms if they are not specified.. - // currently this does not extract structs only default types this.uniformData = uniforms || extractUniformsFromSrc(this.vertexSrc, this.fragmentSrc, 'projectionMatrix|uSampler'); /** @@ -80,7 +79,7 @@ * * @member {number} */ - this.resolution = 1; + this.resolution = settings.RESOLUTION; /** * If enabled is true the filter is applied, if false it will not. @@ -97,8 +96,11 @@ * @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 { // --- // // this.uniforms.filterMatrix = filterManager.calculateSpriteMatrix(tempMatrix, window.panda ); @@ -170,5 +172,3 @@ ].join('\n'); } } - -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 dc7072a..6c7422d 100644 --- a/src/core/renderers/webgl/managers/FilterManager.js +++ b/src/core/renderers/webgl/managers/FilterManager.js @@ -154,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 @@ -176,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; @@ -184,7 +184,7 @@ flop = t; } - filters[i].apply(this, flip, lastState.renderTarget, true); + filters[i].apply(this, flip, lastState.renderTarget, false, currentState); this.freePotRenderTarget(flip); this.freePotRenderTarget(flop); @@ -291,9 +291,13 @@ let textureCount = 1; let currentState; - if (shader.uniforms.data.filterArea) + // 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; @@ -306,9 +310,9 @@ // use this to clamp displaced texture coords so they belong to filterArea // see displacementFilter fragment shader for an example - if (shader.uniforms.data.filterClamp) + if (shader.uniforms.filterClamp) { - currentState = this.filterData.stack[this.filterData.index]; + currentState = currentState || this.filterData.stack[this.filterData.index]; const filterClamp = shader.uniforms.filterClamp; @@ -485,7 +489,7 @@ */ destroy() { - this.shaderCache = []; + this.shaderCache = {}; this.emptyPool(); } diff --git a/src/core/renderers/webgl/utils/RenderTarget.js b/src/core/renderers/webgl/utils/RenderTarget.js index afe51ab..6d2467f 100644 --- a/src/core/renderers/webgl/utils/RenderTarget.js +++ b/src/core/renderers/webgl/utils/RenderTarget.js @@ -124,7 +124,7 @@ * @default PIXI.settings.SCALE_MODE * @see PIXI.SCALE_MODES */ - this.scaleMode = scaleMode || settings.SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; /** * Whether this object is the root element or not 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 36ea5bb..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; @@ -346,8 +346,8 @@ /** * Gets the local bounds of the sprite object. * - * @param {Rectangle} rect - The output rectangle. - * @return {Rectangle} The bounds. + * @param {PIXI.Rectangle} rect - The output rectangle. + * @return {PIXI.Rectangle} The bounds. */ getLocalBounds(rect) { @@ -440,7 +440,7 @@ * * @static * @param {number|string|PIXI.BaseTexture|HTMLCanvasElement|HTMLVideoElement} source Source to create texture from - * @return {PIXI.Texture} 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 e77b39a..90b08c0 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++) @@ -103,11 +102,18 @@ { const gl = this.renderer.gl; - // step 1: first check max textures the GPU can handle. - this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); + if (this.renderer.legacy) + { + this.MAX_TEXTURES = 1; + } + else + { + // step 1: first check max textures the GPU can handle. + this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); - // step 2: check the maximum number of if statements the shader can have too.. - this.MAX_TEXTURES = checkMaxIfStatmentsInShader(this.MAX_TEXTURES, gl); + // step 2: check the maximum number of if statements the shader can have too.. + this.MAX_TEXTURES = checkMaxIfStatmentsInShader(this.MAX_TEXTURES, gl); + } const shader = this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); @@ -130,8 +136,12 @@ .addIndex(this.indexBuffer) .addAttribute(this.vertexBuffers[i], shader.attributes.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) .addAttribute(this.vertexBuffers[i], shader.attributes.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) - .addAttribute(this.vertexBuffers[i], shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4) - .addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + .addAttribute(this.vertexBuffers[i], shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); + + if (shader.attributes.aTextureId) + { + this.vaos[i].addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + } /* eslint-enable max-len */ } @@ -378,8 +388,12 @@ .addIndex(this.indexBuffer) .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) - .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4) - .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); + + if (this.shader.attributes.aTextureId) + { + this.vaos[this.vertexCount].addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + } /* eslint-enable max-len */ } diff --git a/src/core/text/Text.js b/src/core/text/Text.js index 013625a..cec8c32 100644 --- a/src/core/text/Text.js +++ b/src/core/text/Text.js @@ -6,6 +6,7 @@ import { TEXT_GRADIENT } from '../const'; import settings from '../settings'; import TextStyle from './TextStyle'; +import trimCanvas from '../utils/trimCanvas'; const defaultDestroyOptions = { texture: true, @@ -57,7 +58,7 @@ /** * The canvas 2d context that everything is drawn with - * @member {HTMLCanvasElement} + * @member {CanvasRenderingContext2D} */ this.context = this.canvas.getContext('2d'); @@ -189,10 +190,12 @@ if (style.dropShadow) { + this.context.shadowBlur = style.dropShadowBlur; + this.context.globalAlpha = style.dropShadowAlpha; + if (style.dropShadowBlur > 0) { this.context.shadowColor = style.dropShadowColor; - this.context.shadowBlur = style.dropShadowBlur; } else { @@ -237,6 +240,10 @@ } } + // reset the shadow blur and alpha that was set by the drop shadow, for the regular text + this.context.shadowBlur = 0; + this.context.globalAlpha = 1; + // set canvas text styles this.context.fillStyle = this._generateFillStyle(style, lines); @@ -326,6 +333,15 @@ */ updateTexture() { + if (this._style.trim) + { + const trimmed = trimCanvas(this.canvas); + + this.canvas.width = trimmed.width; + this.canvas.height = trimmed.height; + this.context.putImageData(trimmed.data, 0, 0); + } + const texture = this._texture; const style = this._style; @@ -475,6 +491,19 @@ } /** + * Gets the local bounds of the text object. + * + * @param {Rectangle} rect - The output rectangle. + * @return {Rectangle} The bounds. + */ + getLocalBounds(rect) + { + this.updateText(true); + + return super.getLocalBounds.call(this, rect); + } + + /** * calculates the bounds of the Text as a rectangle. The bounds calculation takes the worldTransform into account. */ _calculateBounds() @@ -525,6 +554,29 @@ const width = this.canvas.width / this.resolution; const height = this.canvas.height / this.resolution; + // make a copy of the style settings, so we can manipulate them later + const fill = style.fill.slice(); + const fillGradientStops = style.fillGradientStops.slice(); + + // wanting to evenly distribute the fills. So an array of 4 colours should give fills of 0.25, 0.5 and 0.75 + if (!fillGradientStops.length) + { + const lengthPlus1 = fill.length + 1; + + for (let i = 1; i < lengthPlus1; ++i) + { + fillGradientStops.push(i / lengthPlus1); + } + } + + // stop the bleeding of the last gradient on the line above to the top gradient of the this line + // by hard defining the first gradient colour at point 0, and last gradient colour at point 1 + fill.unshift(style.fill[0]); + fillGradientStops.unshift(0); + + fill.push(style.fill[style.fill.length - 1]); + fillGradientStops.push(1); + if (style.fillGradientType === TEXT_GRADIENT.LINEAR_VERTICAL) { // start the gradient at the top center of the canvas, and end at the bottom middle of the canvas @@ -532,15 +584,22 @@ // we need to repeat the gradient so that each individual line of text has the same vertical gradient effect // ['#FF0000', '#00FF00', '#0000FF'] over 2 lines would create stops at 0.125, 0.25, 0.375, 0.625, 0.75, 0.875 - totalIterations = (style.fill.length + 1) * lines.length; + totalIterations = (fill.length + 1) * lines.length; currentIteration = 0; for (let i = 0; i < lines.length; i++) { currentIteration += 1; - for (let j = 0; j < style.fill.length; j++) + for (let j = 0; j < fill.length; j++) { - stop = (currentIteration / totalIterations); - gradient.addColorStop(stop, style.fill[j]); + if (fillGradientStops[j]) + { + stop = (fillGradientStops[j] / lines.length) + (i / lines.length); + } + else + { + stop = currentIteration / totalIterations; + } + gradient.addColorStop(stop, fill[j]); currentIteration++; } } @@ -552,13 +611,20 @@ // can just evenly space out the gradients in this case, as multiple lines makes no difference // to an even left to right gradient - totalIterations = style.fill.length + 1; + totalIterations = fill.length + 1; currentIteration = 1; - for (let i = 0; i < style.fill.length; i++) + for (let i = 0; i < fill.length; i++) { - stop = currentIteration / totalIterations; - gradient.addColorStop(stop, style.fill[i]); + if (fillGradientStops[i]) + { + stop = fillGradientStops[i]; + } + else + { + stop = currentIteration / totalIterations; + } + gradient.addColorStop(stop, fill[i]); currentIteration++; } } @@ -569,7 +635,7 @@ /** * Destroys this text object. * Note* Unlike a Sprite, a Text object will automatically destroy its baseTexture and texture as - * the majorety of the time the texture will not be shared with any other Sprites. + * the majority of the time the texture will not be shared with any other Sprites. * * @param {object|boolean} [options] - Options parameter. A boolean will act as if all options * have been set to that value @@ -680,7 +746,7 @@ set text(text) // eslint-disable-line require-jsdoc { - text = String(text || ' '); + text = String(text === '' || text === null || text === undefined ? ' ' : text); if (this._text === text) { diff --git a/package.json b/package.json index 1271fef..71ade15 100644 --- a/package.json +++ b/package.json @@ -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/scripts/release.js b/scripts/release.js index c4c8e36..156620c 100644 --- a/scripts/release.js +++ b/scripts/release.js @@ -12,7 +12,8 @@ 'dist/**/*', 'lib/**/*', 'src/**/*', - 'scripts/**/*', + 'scripts/*', + 'scripts/renders/*', 'test/**/*', '*.json', '*.md', diff --git a/scripts/renders/client.js b/scripts/renders/client.js index 16c9565..6faee34 100755 --- a/scripts/renders/client.js +++ b/scripts/renders/client.js @@ -1,6 +1,6 @@ 'use strict'; -require('../../bin/pixi'); +require('../../dist/pixi'); PIXI.utils.skipHello(); const fs = require('fs'); 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 bc730db..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. @@ -33,8 +33,11 @@ * need to call toDataUrl on the webgl context * @param {number} [options.resolution=1] - The resolution / device pixel ratio of the renderer, retina would be 2 * @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 @@ -43,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. */ @@ -73,7 +99,7 @@ */ stop() { - this.ticker.stop(); + this._ticker.stop(); } /** @@ -81,7 +107,7 @@ */ start() { - this.ticker.start(); + this._ticker.start(); } /** @@ -95,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 index 81e076b..78b17e6 100644 --- a/src/core/Shader.js +++ b/src/core/Shader.js @@ -1,9 +1,7 @@ import { GLShader } from 'pixi-gl-core'; import settings from './settings'; -const { PRECISION } = settings; - -function checkPrecision(src) +function checkPrecision(src, def) { if (src instanceof Array) { @@ -11,14 +9,14 @@ { const copy = src.slice(0); - copy.unshift(`precision ${PRECISION} float;`); + copy.unshift(`precision ${def} float;`); return copy; } } else if (src.substring(0, 9) !== 'precision') { - return `precision ${PRECISION} float;\n${src}`; + return `precision ${def} float;\n${src}`; } return src; @@ -42,6 +40,7 @@ */ constructor(gl, vertexSrc, fragmentSrc) { - super(gl, checkPrecision(vertexSrc), checkPrecision(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 3425b41..f9235e4 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -181,7 +181,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 b3792ef..17fde8d 100644 --- a/src/core/display/Container.js +++ b/src/core/display/Container.js @@ -73,13 +73,14 @@ } child.parent = this; - - // ensure a transform will be recalculated.. - this.transform._parentID = -1; - this._boundsID++; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.push(child); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(this.children.length - 1); child.emit('added', this); @@ -108,9 +109,14 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.splice(index, 0, child); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(index); child.emit('added', this); @@ -174,6 +180,7 @@ removeItems(this.children, currentIndex, 1); // remove from old position this.children.splice(index, 0, child); // add at new position + this.onChildrenChange(index); } @@ -220,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! @@ -244,9 +252,14 @@ { const child = this.getChildAt(index); + // ensure child transform will be recalculated.. child.parent = null; + child.transform._parentID = -1; removeItems(this.children, index, 1); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(index); child.emit('removed', this); @@ -275,8 +288,14 @@ for (let i = 0; i < removed.length; ++i) { removed[i].parent = null; + if (removed[i].transform) + { + removed[i].transform._parentID = -1; + } } + this._boundsID++; + this.onChildrenChange(beginIndex); for (let i = 0; i < removed.length; ++i) diff --git a/src/core/display/DisplayObject.js b/src/core/display/DisplayObject.js index 3affe36..4e6c77d 100644 --- a/src/core/display/DisplayObject.js +++ b/src/core/display/DisplayObject.js @@ -109,7 +109,7 @@ /** * The original, cached mask of the object * - * @member {PIXI.Rectangle} + * @member {PIXI.Graphics|PIXI.Sprite} * @private */ this._mask = null; diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index 95f2af4..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 ); @@ -1065,10 +1061,15 @@ canvasRenderer = new CanvasRenderer(); } - tempMatrix.tx = -bounds.x; - tempMatrix.ty = -bounds.y; + this.transform.updateLocalTransform(); + this.transform.localTransform.copy(tempMatrix); - canvasRenderer.render(this, canvasBuffer, false, tempMatrix); + tempMatrix.invert(); + + tempMatrix.tx -= bounds.x; + tempMatrix.ty -= bounds.y; + + canvasRenderer.render(this, canvasBuffer, true, tempMatrix); const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode); 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 e67680f..867dde2 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/buildCircle.js b/src/core/graphics/webgl/utils/buildCircle.js index 0400d13..abf5d02 100644 --- a/src/core/graphics/webgl/utils/buildCircle.js +++ b/src/core/graphics/webgl/utils/buildCircle.js @@ -33,6 +33,11 @@ height = circleData.height; } + if (width === 0 || height === 0) + { + return; + } + const totalSegs = Math.floor(30 * Math.sqrt(circleData.radius)) || Math.floor(15 * Math.sqrt(circleData.width + circleData.height)); diff --git a/src/core/graphics/webgl/utils/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 90155f9..f03fe15 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -30,6 +30,7 @@ export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; export { default as CanvasGraphicsRenderer } from './graphics/canvas/CanvasGraphicsRenderer'; +export { default as Spritesheet } from './textures/Spritesheet'; export { default as Texture } from './textures/Texture'; export { default as BaseTexture } from './textures/BaseTexture'; export { default as RenderTexture } from './textures/RenderTexture'; diff --git a/src/core/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 ccce680..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); } /** @@ -162,6 +162,9 @@ if (transform) { transform.copy(tempWt); + + // lets not forget to flag the parent transform as dirty... + this._tempDisplayObjectParent.transform._worldID = -1; } else { @@ -169,6 +172,7 @@ } displayObject.parent = this._tempDisplayObjectParent; + displayObject.updateTransform(); displayObject.parent = cacheParent; // displayObject.hitArea = //TODO add a temp hit area @@ -279,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/canvas/utils/CanvasRenderTarget.js b/src/core/renderers/canvas/utils/CanvasRenderTarget.js index 3de0aef..b409891 100644 --- a/src/core/renderers/canvas/utils/CanvasRenderTarget.js +++ b/src/core/renderers/canvas/utils/CanvasRenderTarget.js @@ -1,5 +1,4 @@ import settings from '../../../settings'; -const { RESOLUTION } = settings; /** * Creates a Canvas element of the given size. @@ -30,7 +29,7 @@ */ this.context = this.canvas.getContext('2d'); - this.resolution = resolution || RESOLUTION; + this.resolution = resolution || settings.RESOLUTION; this.resize(width, height); } 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 ce0e1c4..788ed6c 100644 --- a/src/core/renderers/webgl/WebGLRenderer.js +++ b/src/core/renderers/webgl/WebGLRenderer.js @@ -30,8 +30,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 @@ -49,10 +49,19 @@ * enable this if you need to call toDataUrl on the webgl context. * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when * rendering, stopping pixel interpolation. + * @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; + + if (this.legacy) + { + glCore.VertexArrayObject.FORCE_NATIVE = true; + } /** * The type of this renderer as a standardised const @@ -229,7 +238,7 @@ this.emit('context', gl); // setup the width/height properties and gl viewport - this.resize(this.width, this.height); + this.resize(this.screen.width, this.screen.height); } /** @@ -323,16 +332,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) { @@ -376,6 +385,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 @@ -440,9 +469,10 @@ * Changes the current shader to the one given in parameter * * @param {PIXI.Shader} shader - the new shader + * @param {boolean} [autoProject=true] - Whether automatically set the projection matrix * @return {PIXI.WebGLRenderer} Returns itself. */ - bindShader(shader) + bindShader(shader, autoProject) { // TODO cache if (this._activeShader !== shader) @@ -450,8 +480,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 bbab3b3..944f41d 100644 --- a/src/core/renderers/webgl/filters/Filter.js +++ b/src/core/renderers/webgl/filters/Filter.js @@ -1,6 +1,7 @@ import extractUniformsFromSrc from './extractUniformsFromSrc'; import { uid } from '../../../utils'; import { BLEND_MODES } from '../../../const'; +import settings from '../../../settings'; const SOURCE_KEY_MAP = {}; @@ -10,7 +11,7 @@ * @memberof PIXI * @extends PIXI.Shader */ -class Filter +export default class Filter { /** * @param {string} [vertexSrc] - The source of the vertex shader. @@ -35,8 +36,6 @@ this.blendMode = BLEND_MODES.NORMAL; - // pull out the vertex and shader uniforms if they are not specified.. - // currently this does not extract structs only default types this.uniformData = uniforms || extractUniformsFromSrc(this.vertexSrc, this.fragmentSrc, 'projectionMatrix|uSampler'); /** @@ -80,7 +79,7 @@ * * @member {number} */ - this.resolution = 1; + this.resolution = settings.RESOLUTION; /** * If enabled is true the filter is applied, if false it will not. @@ -97,8 +96,11 @@ * @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 { // --- // // this.uniforms.filterMatrix = filterManager.calculateSpriteMatrix(tempMatrix, window.panda ); @@ -170,5 +172,3 @@ ].join('\n'); } } - -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 dc7072a..6c7422d 100644 --- a/src/core/renderers/webgl/managers/FilterManager.js +++ b/src/core/renderers/webgl/managers/FilterManager.js @@ -154,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 @@ -176,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; @@ -184,7 +184,7 @@ flop = t; } - filters[i].apply(this, flip, lastState.renderTarget, true); + filters[i].apply(this, flip, lastState.renderTarget, false, currentState); this.freePotRenderTarget(flip); this.freePotRenderTarget(flop); @@ -291,9 +291,13 @@ let textureCount = 1; let currentState; - if (shader.uniforms.data.filterArea) + // 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; @@ -306,9 +310,9 @@ // use this to clamp displaced texture coords so they belong to filterArea // see displacementFilter fragment shader for an example - if (shader.uniforms.data.filterClamp) + if (shader.uniforms.filterClamp) { - currentState = this.filterData.stack[this.filterData.index]; + currentState = currentState || this.filterData.stack[this.filterData.index]; const filterClamp = shader.uniforms.filterClamp; @@ -485,7 +489,7 @@ */ destroy() { - this.shaderCache = []; + this.shaderCache = {}; this.emptyPool(); } diff --git a/src/core/renderers/webgl/utils/RenderTarget.js b/src/core/renderers/webgl/utils/RenderTarget.js index afe51ab..6d2467f 100644 --- a/src/core/renderers/webgl/utils/RenderTarget.js +++ b/src/core/renderers/webgl/utils/RenderTarget.js @@ -124,7 +124,7 @@ * @default PIXI.settings.SCALE_MODE * @see PIXI.SCALE_MODES */ - this.scaleMode = scaleMode || settings.SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; /** * Whether this object is the root element or not 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 36ea5bb..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; @@ -346,8 +346,8 @@ /** * Gets the local bounds of the sprite object. * - * @param {Rectangle} rect - The output rectangle. - * @return {Rectangle} The bounds. + * @param {PIXI.Rectangle} rect - The output rectangle. + * @return {PIXI.Rectangle} The bounds. */ getLocalBounds(rect) { @@ -440,7 +440,7 @@ * * @static * @param {number|string|PIXI.BaseTexture|HTMLCanvasElement|HTMLVideoElement} source Source to create texture from - * @return {PIXI.Texture} 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 e77b39a..90b08c0 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++) @@ -103,11 +102,18 @@ { const gl = this.renderer.gl; - // step 1: first check max textures the GPU can handle. - this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); + if (this.renderer.legacy) + { + this.MAX_TEXTURES = 1; + } + else + { + // step 1: first check max textures the GPU can handle. + this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); - // step 2: check the maximum number of if statements the shader can have too.. - this.MAX_TEXTURES = checkMaxIfStatmentsInShader(this.MAX_TEXTURES, gl); + // step 2: check the maximum number of if statements the shader can have too.. + this.MAX_TEXTURES = checkMaxIfStatmentsInShader(this.MAX_TEXTURES, gl); + } const shader = this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); @@ -130,8 +136,12 @@ .addIndex(this.indexBuffer) .addAttribute(this.vertexBuffers[i], shader.attributes.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) .addAttribute(this.vertexBuffers[i], shader.attributes.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) - .addAttribute(this.vertexBuffers[i], shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4) - .addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + .addAttribute(this.vertexBuffers[i], shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); + + if (shader.attributes.aTextureId) + { + this.vaos[i].addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + } /* eslint-enable max-len */ } @@ -378,8 +388,12 @@ .addIndex(this.indexBuffer) .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) - .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4) - .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); + + if (this.shader.attributes.aTextureId) + { + this.vaos[this.vertexCount].addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + } /* eslint-enable max-len */ } diff --git a/src/core/text/Text.js b/src/core/text/Text.js index 013625a..cec8c32 100644 --- a/src/core/text/Text.js +++ b/src/core/text/Text.js @@ -6,6 +6,7 @@ import { TEXT_GRADIENT } from '../const'; import settings from '../settings'; import TextStyle from './TextStyle'; +import trimCanvas from '../utils/trimCanvas'; const defaultDestroyOptions = { texture: true, @@ -57,7 +58,7 @@ /** * The canvas 2d context that everything is drawn with - * @member {HTMLCanvasElement} + * @member {CanvasRenderingContext2D} */ this.context = this.canvas.getContext('2d'); @@ -189,10 +190,12 @@ if (style.dropShadow) { + this.context.shadowBlur = style.dropShadowBlur; + this.context.globalAlpha = style.dropShadowAlpha; + if (style.dropShadowBlur > 0) { this.context.shadowColor = style.dropShadowColor; - this.context.shadowBlur = style.dropShadowBlur; } else { @@ -237,6 +240,10 @@ } } + // reset the shadow blur and alpha that was set by the drop shadow, for the regular text + this.context.shadowBlur = 0; + this.context.globalAlpha = 1; + // set canvas text styles this.context.fillStyle = this._generateFillStyle(style, lines); @@ -326,6 +333,15 @@ */ updateTexture() { + if (this._style.trim) + { + const trimmed = trimCanvas(this.canvas); + + this.canvas.width = trimmed.width; + this.canvas.height = trimmed.height; + this.context.putImageData(trimmed.data, 0, 0); + } + const texture = this._texture; const style = this._style; @@ -475,6 +491,19 @@ } /** + * Gets the local bounds of the text object. + * + * @param {Rectangle} rect - The output rectangle. + * @return {Rectangle} The bounds. + */ + getLocalBounds(rect) + { + this.updateText(true); + + return super.getLocalBounds.call(this, rect); + } + + /** * calculates the bounds of the Text as a rectangle. The bounds calculation takes the worldTransform into account. */ _calculateBounds() @@ -525,6 +554,29 @@ const width = this.canvas.width / this.resolution; const height = this.canvas.height / this.resolution; + // make a copy of the style settings, so we can manipulate them later + const fill = style.fill.slice(); + const fillGradientStops = style.fillGradientStops.slice(); + + // wanting to evenly distribute the fills. So an array of 4 colours should give fills of 0.25, 0.5 and 0.75 + if (!fillGradientStops.length) + { + const lengthPlus1 = fill.length + 1; + + for (let i = 1; i < lengthPlus1; ++i) + { + fillGradientStops.push(i / lengthPlus1); + } + } + + // stop the bleeding of the last gradient on the line above to the top gradient of the this line + // by hard defining the first gradient colour at point 0, and last gradient colour at point 1 + fill.unshift(style.fill[0]); + fillGradientStops.unshift(0); + + fill.push(style.fill[style.fill.length - 1]); + fillGradientStops.push(1); + if (style.fillGradientType === TEXT_GRADIENT.LINEAR_VERTICAL) { // start the gradient at the top center of the canvas, and end at the bottom middle of the canvas @@ -532,15 +584,22 @@ // we need to repeat the gradient so that each individual line of text has the same vertical gradient effect // ['#FF0000', '#00FF00', '#0000FF'] over 2 lines would create stops at 0.125, 0.25, 0.375, 0.625, 0.75, 0.875 - totalIterations = (style.fill.length + 1) * lines.length; + totalIterations = (fill.length + 1) * lines.length; currentIteration = 0; for (let i = 0; i < lines.length; i++) { currentIteration += 1; - for (let j = 0; j < style.fill.length; j++) + for (let j = 0; j < fill.length; j++) { - stop = (currentIteration / totalIterations); - gradient.addColorStop(stop, style.fill[j]); + if (fillGradientStops[j]) + { + stop = (fillGradientStops[j] / lines.length) + (i / lines.length); + } + else + { + stop = currentIteration / totalIterations; + } + gradient.addColorStop(stop, fill[j]); currentIteration++; } } @@ -552,13 +611,20 @@ // can just evenly space out the gradients in this case, as multiple lines makes no difference // to an even left to right gradient - totalIterations = style.fill.length + 1; + totalIterations = fill.length + 1; currentIteration = 1; - for (let i = 0; i < style.fill.length; i++) + for (let i = 0; i < fill.length; i++) { - stop = currentIteration / totalIterations; - gradient.addColorStop(stop, style.fill[i]); + if (fillGradientStops[i]) + { + stop = fillGradientStops[i]; + } + else + { + stop = currentIteration / totalIterations; + } + gradient.addColorStop(stop, fill[i]); currentIteration++; } } @@ -569,7 +635,7 @@ /** * Destroys this text object. * Note* Unlike a Sprite, a Text object will automatically destroy its baseTexture and texture as - * the majorety of the time the texture will not be shared with any other Sprites. + * the majority of the time the texture will not be shared with any other Sprites. * * @param {object|boolean} [options] - Options parameter. A boolean will act as if all options * have been set to that value @@ -680,7 +746,7 @@ set text(text) // eslint-disable-line require-jsdoc { - text = String(text || ' '); + text = String(text === '' || text === null || text === undefined ? ' ' : text); if (this._text === text) { diff --git a/src/core/text/TextStyle.js b/src/core/text/TextStyle.js index 3b25528..890950b 100644 --- a/src/core/text/TextStyle.js +++ b/src/core/text/TextStyle.js @@ -8,12 +8,14 @@ align: 'left', breakWords: false, dropShadow: false, + dropShadowAlpha: 1, dropShadowAngle: Math.PI / 6, dropShadowBlur: 0, dropShadowColor: '#000000', dropShadowDistance: 5, fill: 'black', fillGradientType: TEXT_GRADIENT.LINEAR_VERTICAL, + fillGradientStops: [], fontFamily: 'Arial', fontSize: 26, fontStyle: 'normal', @@ -27,6 +29,7 @@ stroke: 'black', strokeThickness: 0, textBaseline: 'alphabetic', + trim: false, wordWrap: false, wordWrapWidth: 100, }; @@ -47,6 +50,7 @@ * @param {boolean} [style.breakWords=false] - Indicates if lines can be wrapped within words, it * needs wordWrap to be set to true * @param {boolean} [style.dropShadow=false] - Set a drop shadow for the text + * @param {number} [style.dropShadowAlpha=1] - Set alpha for the drop shadow * @param {number} [style.dropShadowAngle=Math.PI/6] - Set a angle of the drop shadow * @param {number} [style.dropShadowBlur=0] - Set a shadow blur radius * @param {string} [style.dropShadowColor='#000000'] - A fill style to be used on the dropshadow e.g 'red', '#00FF00' @@ -55,8 +59,10 @@ * fillstyle that will be used on the text e.g 'red', '#00FF00'. Can be an array to create a gradient * eg ['#000000','#FFFFFF'] * {@link https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/fillStyle|MDN} - * @param {number} [style.fillGradientType=PIXI.TEXT_GRADIENT.LINEAR_VERTICAL] - If fills styles are - * supplied, this can change the type/direction of the gradient. See {@link PIXI.TEXT_GRADIENT} for possible values + * @param {number} [style.fillGradientType=PIXI.TEXT_GRADIENT.LINEAR_VERTICAL] - If fill is an array of colours + * to create a gradient, this can change the type/direction of the gradient. See {@link PIXI.TEXT_GRADIENT} + * @param {number[]} [style.fillGradientStops] - If fill is an array of colours to create a gradient, this array can set + * the stop points (numbers between 0 and 1) for the color, overriding the default behaviour of evenly spacing them. * @param {string|string[]} [style.fontFamily='Arial'] - The font family * @param {number|string} [style.fontSize=26] - The font size (as a number it converts to px, but as a string, * equivalents are '26px','20pt','160%' or '1.6em') @@ -76,6 +82,7 @@ * e.g 'blue', '#FCFF00' * @param {number} [style.strokeThickness=0] - A number that represents the thickness of the stroke. * Default is 0 (no stroke) + * @param {boolean} [style.trim=false] - Trim transparent borders * @param {string} [style.textBaseline='alphabetic'] - The baseline of the text that is rendered. * @param {boolean} [style.wordWrap=false] - Indicates if word wrap should be used * @param {number} [style.wordWrapWidth=100] - The width at which text will wrap, it needs wordWrap to be set to true @@ -152,6 +159,19 @@ } } + get dropShadowAlpha() + { + return this._dropShadowAlpha; + } + set dropShadowAlpha(dropShadowAlpha) + { + if (this._dropShadowAlpha !== dropShadowAlpha) + { + this._dropShadowAlpha = dropShadowAlpha; + this.styleID++; + } + } + get dropShadowAngle() { return this._dropShadowAngle; @@ -232,6 +252,19 @@ } } + get fillGradientStops() + { + return this._fillGradientStops; + } + set fillGradientStops(fillGradientStops) + { + if (!areArraysEqual(this._fillGradientStops,fillGradientStops)) + { + this._fillGradientStops = fillGradientStops; + this.styleID++; + } + } + get fontFamily() { return this._fontFamily; @@ -402,6 +435,19 @@ } } + get trim() + { + return this._trim; + } + set trim(trim) + { + if (this._trim !== trim) + { + this._trim = trim; + this.styleID++; + } + } + get wordWrap() { return this._wordWrap; @@ -475,3 +521,34 @@ return color; } } + +/** + * Utility function to convert hexadecimal colors to strings, and simply return the color if it's a string. + * This version can also convert array of colors + * + * @param {Array} array1 First array to compare + * @param {Array} array2 Second array to compare + * @return {boolean} Do the arrays contain the same values in the same order + */ +function areArraysEqual(array1, array2) +{ + if (!Array.isArray(array1) || !Array.isArray(array2)) + { + return false; + } + + if (array1.length !== array2.length) + { + return false; + } + + for (let i = 0; i < array1.length; ++i) + { + if (array1[i] !== array2[i]) + { + return false; + } + } + + return true; +} diff --git a/package.json b/package.json index 1271fef..71ade15 100644 --- a/package.json +++ b/package.json @@ -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/scripts/release.js b/scripts/release.js index c4c8e36..156620c 100644 --- a/scripts/release.js +++ b/scripts/release.js @@ -12,7 +12,8 @@ 'dist/**/*', 'lib/**/*', 'src/**/*', - 'scripts/**/*', + 'scripts/*', + 'scripts/renders/*', 'test/**/*', '*.json', '*.md', diff --git a/scripts/renders/client.js b/scripts/renders/client.js index 16c9565..6faee34 100755 --- a/scripts/renders/client.js +++ b/scripts/renders/client.js @@ -1,6 +1,6 @@ 'use strict'; -require('../../bin/pixi'); +require('../../dist/pixi'); PIXI.utils.skipHello(); const fs = require('fs'); 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 bc730db..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. @@ -33,8 +33,11 @@ * need to call toDataUrl on the webgl context * @param {number} [options.resolution=1] - The resolution / device pixel ratio of the renderer, retina would be 2 * @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 @@ -43,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. */ @@ -73,7 +99,7 @@ */ stop() { - this.ticker.stop(); + this._ticker.stop(); } /** @@ -81,7 +107,7 @@ */ start() { - this.ticker.start(); + this._ticker.start(); } /** @@ -95,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 index 81e076b..78b17e6 100644 --- a/src/core/Shader.js +++ b/src/core/Shader.js @@ -1,9 +1,7 @@ import { GLShader } from 'pixi-gl-core'; import settings from './settings'; -const { PRECISION } = settings; - -function checkPrecision(src) +function checkPrecision(src, def) { if (src instanceof Array) { @@ -11,14 +9,14 @@ { const copy = src.slice(0); - copy.unshift(`precision ${PRECISION} float;`); + copy.unshift(`precision ${def} float;`); return copy; } } else if (src.substring(0, 9) !== 'precision') { - return `precision ${PRECISION} float;\n${src}`; + return `precision ${def} float;\n${src}`; } return src; @@ -42,6 +40,7 @@ */ constructor(gl, vertexSrc, fragmentSrc) { - super(gl, checkPrecision(vertexSrc), checkPrecision(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 3425b41..f9235e4 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -181,7 +181,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 b3792ef..17fde8d 100644 --- a/src/core/display/Container.js +++ b/src/core/display/Container.js @@ -73,13 +73,14 @@ } child.parent = this; - - // ensure a transform will be recalculated.. - this.transform._parentID = -1; - this._boundsID++; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.push(child); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(this.children.length - 1); child.emit('added', this); @@ -108,9 +109,14 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.splice(index, 0, child); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(index); child.emit('added', this); @@ -174,6 +180,7 @@ removeItems(this.children, currentIndex, 1); // remove from old position this.children.splice(index, 0, child); // add at new position + this.onChildrenChange(index); } @@ -220,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! @@ -244,9 +252,14 @@ { const child = this.getChildAt(index); + // ensure child transform will be recalculated.. child.parent = null; + child.transform._parentID = -1; removeItems(this.children, index, 1); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(index); child.emit('removed', this); @@ -275,8 +288,14 @@ for (let i = 0; i < removed.length; ++i) { removed[i].parent = null; + if (removed[i].transform) + { + removed[i].transform._parentID = -1; + } } + this._boundsID++; + this.onChildrenChange(beginIndex); for (let i = 0; i < removed.length; ++i) diff --git a/src/core/display/DisplayObject.js b/src/core/display/DisplayObject.js index 3affe36..4e6c77d 100644 --- a/src/core/display/DisplayObject.js +++ b/src/core/display/DisplayObject.js @@ -109,7 +109,7 @@ /** * The original, cached mask of the object * - * @member {PIXI.Rectangle} + * @member {PIXI.Graphics|PIXI.Sprite} * @private */ this._mask = null; diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index 95f2af4..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 ); @@ -1065,10 +1061,15 @@ canvasRenderer = new CanvasRenderer(); } - tempMatrix.tx = -bounds.x; - tempMatrix.ty = -bounds.y; + this.transform.updateLocalTransform(); + this.transform.localTransform.copy(tempMatrix); - canvasRenderer.render(this, canvasBuffer, false, tempMatrix); + tempMatrix.invert(); + + tempMatrix.tx -= bounds.x; + tempMatrix.ty -= bounds.y; + + canvasRenderer.render(this, canvasBuffer, true, tempMatrix); const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode); 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 e67680f..867dde2 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/buildCircle.js b/src/core/graphics/webgl/utils/buildCircle.js index 0400d13..abf5d02 100644 --- a/src/core/graphics/webgl/utils/buildCircle.js +++ b/src/core/graphics/webgl/utils/buildCircle.js @@ -33,6 +33,11 @@ height = circleData.height; } + if (width === 0 || height === 0) + { + return; + } + const totalSegs = Math.floor(30 * Math.sqrt(circleData.radius)) || Math.floor(15 * Math.sqrt(circleData.width + circleData.height)); diff --git a/src/core/graphics/webgl/utils/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 90155f9..f03fe15 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -30,6 +30,7 @@ export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; export { default as CanvasGraphicsRenderer } from './graphics/canvas/CanvasGraphicsRenderer'; +export { default as Spritesheet } from './textures/Spritesheet'; export { default as Texture } from './textures/Texture'; export { default as BaseTexture } from './textures/BaseTexture'; export { default as RenderTexture } from './textures/RenderTexture'; diff --git a/src/core/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 ccce680..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); } /** @@ -162,6 +162,9 @@ if (transform) { transform.copy(tempWt); + + // lets not forget to flag the parent transform as dirty... + this._tempDisplayObjectParent.transform._worldID = -1; } else { @@ -169,6 +172,7 @@ } displayObject.parent = this._tempDisplayObjectParent; + displayObject.updateTransform(); displayObject.parent = cacheParent; // displayObject.hitArea = //TODO add a temp hit area @@ -279,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/canvas/utils/CanvasRenderTarget.js b/src/core/renderers/canvas/utils/CanvasRenderTarget.js index 3de0aef..b409891 100644 --- a/src/core/renderers/canvas/utils/CanvasRenderTarget.js +++ b/src/core/renderers/canvas/utils/CanvasRenderTarget.js @@ -1,5 +1,4 @@ import settings from '../../../settings'; -const { RESOLUTION } = settings; /** * Creates a Canvas element of the given size. @@ -30,7 +29,7 @@ */ this.context = this.canvas.getContext('2d'); - this.resolution = resolution || RESOLUTION; + this.resolution = resolution || settings.RESOLUTION; this.resize(width, height); } 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 ce0e1c4..788ed6c 100644 --- a/src/core/renderers/webgl/WebGLRenderer.js +++ b/src/core/renderers/webgl/WebGLRenderer.js @@ -30,8 +30,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 @@ -49,10 +49,19 @@ * enable this if you need to call toDataUrl on the webgl context. * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when * rendering, stopping pixel interpolation. + * @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; + + if (this.legacy) + { + glCore.VertexArrayObject.FORCE_NATIVE = true; + } /** * The type of this renderer as a standardised const @@ -229,7 +238,7 @@ this.emit('context', gl); // setup the width/height properties and gl viewport - this.resize(this.width, this.height); + this.resize(this.screen.width, this.screen.height); } /** @@ -323,16 +332,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) { @@ -376,6 +385,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 @@ -440,9 +469,10 @@ * Changes the current shader to the one given in parameter * * @param {PIXI.Shader} shader - the new shader + * @param {boolean} [autoProject=true] - Whether automatically set the projection matrix * @return {PIXI.WebGLRenderer} Returns itself. */ - bindShader(shader) + bindShader(shader, autoProject) { // TODO cache if (this._activeShader !== shader) @@ -450,8 +480,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 bbab3b3..944f41d 100644 --- a/src/core/renderers/webgl/filters/Filter.js +++ b/src/core/renderers/webgl/filters/Filter.js @@ -1,6 +1,7 @@ import extractUniformsFromSrc from './extractUniformsFromSrc'; import { uid } from '../../../utils'; import { BLEND_MODES } from '../../../const'; +import settings from '../../../settings'; const SOURCE_KEY_MAP = {}; @@ -10,7 +11,7 @@ * @memberof PIXI * @extends PIXI.Shader */ -class Filter +export default class Filter { /** * @param {string} [vertexSrc] - The source of the vertex shader. @@ -35,8 +36,6 @@ this.blendMode = BLEND_MODES.NORMAL; - // pull out the vertex and shader uniforms if they are not specified.. - // currently this does not extract structs only default types this.uniformData = uniforms || extractUniformsFromSrc(this.vertexSrc, this.fragmentSrc, 'projectionMatrix|uSampler'); /** @@ -80,7 +79,7 @@ * * @member {number} */ - this.resolution = 1; + this.resolution = settings.RESOLUTION; /** * If enabled is true the filter is applied, if false it will not. @@ -97,8 +96,11 @@ * @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 { // --- // // this.uniforms.filterMatrix = filterManager.calculateSpriteMatrix(tempMatrix, window.panda ); @@ -170,5 +172,3 @@ ].join('\n'); } } - -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 dc7072a..6c7422d 100644 --- a/src/core/renderers/webgl/managers/FilterManager.js +++ b/src/core/renderers/webgl/managers/FilterManager.js @@ -154,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 @@ -176,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; @@ -184,7 +184,7 @@ flop = t; } - filters[i].apply(this, flip, lastState.renderTarget, true); + filters[i].apply(this, flip, lastState.renderTarget, false, currentState); this.freePotRenderTarget(flip); this.freePotRenderTarget(flop); @@ -291,9 +291,13 @@ let textureCount = 1; let currentState; - if (shader.uniforms.data.filterArea) + // 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; @@ -306,9 +310,9 @@ // use this to clamp displaced texture coords so they belong to filterArea // see displacementFilter fragment shader for an example - if (shader.uniforms.data.filterClamp) + if (shader.uniforms.filterClamp) { - currentState = this.filterData.stack[this.filterData.index]; + currentState = currentState || this.filterData.stack[this.filterData.index]; const filterClamp = shader.uniforms.filterClamp; @@ -485,7 +489,7 @@ */ destroy() { - this.shaderCache = []; + this.shaderCache = {}; this.emptyPool(); } diff --git a/src/core/renderers/webgl/utils/RenderTarget.js b/src/core/renderers/webgl/utils/RenderTarget.js index afe51ab..6d2467f 100644 --- a/src/core/renderers/webgl/utils/RenderTarget.js +++ b/src/core/renderers/webgl/utils/RenderTarget.js @@ -124,7 +124,7 @@ * @default PIXI.settings.SCALE_MODE * @see PIXI.SCALE_MODES */ - this.scaleMode = scaleMode || settings.SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; /** * Whether this object is the root element or not 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 36ea5bb..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; @@ -346,8 +346,8 @@ /** * Gets the local bounds of the sprite object. * - * @param {Rectangle} rect - The output rectangle. - * @return {Rectangle} The bounds. + * @param {PIXI.Rectangle} rect - The output rectangle. + * @return {PIXI.Rectangle} The bounds. */ getLocalBounds(rect) { @@ -440,7 +440,7 @@ * * @static * @param {number|string|PIXI.BaseTexture|HTMLCanvasElement|HTMLVideoElement} source Source to create texture from - * @return {PIXI.Texture} 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 e77b39a..90b08c0 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++) @@ -103,11 +102,18 @@ { const gl = this.renderer.gl; - // step 1: first check max textures the GPU can handle. - this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); + if (this.renderer.legacy) + { + this.MAX_TEXTURES = 1; + } + else + { + // step 1: first check max textures the GPU can handle. + this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); - // step 2: check the maximum number of if statements the shader can have too.. - this.MAX_TEXTURES = checkMaxIfStatmentsInShader(this.MAX_TEXTURES, gl); + // step 2: check the maximum number of if statements the shader can have too.. + this.MAX_TEXTURES = checkMaxIfStatmentsInShader(this.MAX_TEXTURES, gl); + } const shader = this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); @@ -130,8 +136,12 @@ .addIndex(this.indexBuffer) .addAttribute(this.vertexBuffers[i], shader.attributes.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) .addAttribute(this.vertexBuffers[i], shader.attributes.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) - .addAttribute(this.vertexBuffers[i], shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4) - .addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + .addAttribute(this.vertexBuffers[i], shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); + + if (shader.attributes.aTextureId) + { + this.vaos[i].addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + } /* eslint-enable max-len */ } @@ -378,8 +388,12 @@ .addIndex(this.indexBuffer) .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) - .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4) - .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); + + if (this.shader.attributes.aTextureId) + { + this.vaos[this.vertexCount].addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + } /* eslint-enable max-len */ } diff --git a/src/core/text/Text.js b/src/core/text/Text.js index 013625a..cec8c32 100644 --- a/src/core/text/Text.js +++ b/src/core/text/Text.js @@ -6,6 +6,7 @@ import { TEXT_GRADIENT } from '../const'; import settings from '../settings'; import TextStyle from './TextStyle'; +import trimCanvas from '../utils/trimCanvas'; const defaultDestroyOptions = { texture: true, @@ -57,7 +58,7 @@ /** * The canvas 2d context that everything is drawn with - * @member {HTMLCanvasElement} + * @member {CanvasRenderingContext2D} */ this.context = this.canvas.getContext('2d'); @@ -189,10 +190,12 @@ if (style.dropShadow) { + this.context.shadowBlur = style.dropShadowBlur; + this.context.globalAlpha = style.dropShadowAlpha; + if (style.dropShadowBlur > 0) { this.context.shadowColor = style.dropShadowColor; - this.context.shadowBlur = style.dropShadowBlur; } else { @@ -237,6 +240,10 @@ } } + // reset the shadow blur and alpha that was set by the drop shadow, for the regular text + this.context.shadowBlur = 0; + this.context.globalAlpha = 1; + // set canvas text styles this.context.fillStyle = this._generateFillStyle(style, lines); @@ -326,6 +333,15 @@ */ updateTexture() { + if (this._style.trim) + { + const trimmed = trimCanvas(this.canvas); + + this.canvas.width = trimmed.width; + this.canvas.height = trimmed.height; + this.context.putImageData(trimmed.data, 0, 0); + } + const texture = this._texture; const style = this._style; @@ -475,6 +491,19 @@ } /** + * Gets the local bounds of the text object. + * + * @param {Rectangle} rect - The output rectangle. + * @return {Rectangle} The bounds. + */ + getLocalBounds(rect) + { + this.updateText(true); + + return super.getLocalBounds.call(this, rect); + } + + /** * calculates the bounds of the Text as a rectangle. The bounds calculation takes the worldTransform into account. */ _calculateBounds() @@ -525,6 +554,29 @@ const width = this.canvas.width / this.resolution; const height = this.canvas.height / this.resolution; + // make a copy of the style settings, so we can manipulate them later + const fill = style.fill.slice(); + const fillGradientStops = style.fillGradientStops.slice(); + + // wanting to evenly distribute the fills. So an array of 4 colours should give fills of 0.25, 0.5 and 0.75 + if (!fillGradientStops.length) + { + const lengthPlus1 = fill.length + 1; + + for (let i = 1; i < lengthPlus1; ++i) + { + fillGradientStops.push(i / lengthPlus1); + } + } + + // stop the bleeding of the last gradient on the line above to the top gradient of the this line + // by hard defining the first gradient colour at point 0, and last gradient colour at point 1 + fill.unshift(style.fill[0]); + fillGradientStops.unshift(0); + + fill.push(style.fill[style.fill.length - 1]); + fillGradientStops.push(1); + if (style.fillGradientType === TEXT_GRADIENT.LINEAR_VERTICAL) { // start the gradient at the top center of the canvas, and end at the bottom middle of the canvas @@ -532,15 +584,22 @@ // we need to repeat the gradient so that each individual line of text has the same vertical gradient effect // ['#FF0000', '#00FF00', '#0000FF'] over 2 lines would create stops at 0.125, 0.25, 0.375, 0.625, 0.75, 0.875 - totalIterations = (style.fill.length + 1) * lines.length; + totalIterations = (fill.length + 1) * lines.length; currentIteration = 0; for (let i = 0; i < lines.length; i++) { currentIteration += 1; - for (let j = 0; j < style.fill.length; j++) + for (let j = 0; j < fill.length; j++) { - stop = (currentIteration / totalIterations); - gradient.addColorStop(stop, style.fill[j]); + if (fillGradientStops[j]) + { + stop = (fillGradientStops[j] / lines.length) + (i / lines.length); + } + else + { + stop = currentIteration / totalIterations; + } + gradient.addColorStop(stop, fill[j]); currentIteration++; } } @@ -552,13 +611,20 @@ // can just evenly space out the gradients in this case, as multiple lines makes no difference // to an even left to right gradient - totalIterations = style.fill.length + 1; + totalIterations = fill.length + 1; currentIteration = 1; - for (let i = 0; i < style.fill.length; i++) + for (let i = 0; i < fill.length; i++) { - stop = currentIteration / totalIterations; - gradient.addColorStop(stop, style.fill[i]); + if (fillGradientStops[i]) + { + stop = fillGradientStops[i]; + } + else + { + stop = currentIteration / totalIterations; + } + gradient.addColorStop(stop, fill[i]); currentIteration++; } } @@ -569,7 +635,7 @@ /** * Destroys this text object. * Note* Unlike a Sprite, a Text object will automatically destroy its baseTexture and texture as - * the majorety of the time the texture will not be shared with any other Sprites. + * the majority of the time the texture will not be shared with any other Sprites. * * @param {object|boolean} [options] - Options parameter. A boolean will act as if all options * have been set to that value @@ -680,7 +746,7 @@ set text(text) // eslint-disable-line require-jsdoc { - text = String(text || ' '); + text = String(text === '' || text === null || text === undefined ? ' ' : text); if (this._text === text) { diff --git a/src/core/text/TextStyle.js b/src/core/text/TextStyle.js index 3b25528..890950b 100644 --- a/src/core/text/TextStyle.js +++ b/src/core/text/TextStyle.js @@ -8,12 +8,14 @@ align: 'left', breakWords: false, dropShadow: false, + dropShadowAlpha: 1, dropShadowAngle: Math.PI / 6, dropShadowBlur: 0, dropShadowColor: '#000000', dropShadowDistance: 5, fill: 'black', fillGradientType: TEXT_GRADIENT.LINEAR_VERTICAL, + fillGradientStops: [], fontFamily: 'Arial', fontSize: 26, fontStyle: 'normal', @@ -27,6 +29,7 @@ stroke: 'black', strokeThickness: 0, textBaseline: 'alphabetic', + trim: false, wordWrap: false, wordWrapWidth: 100, }; @@ -47,6 +50,7 @@ * @param {boolean} [style.breakWords=false] - Indicates if lines can be wrapped within words, it * needs wordWrap to be set to true * @param {boolean} [style.dropShadow=false] - Set a drop shadow for the text + * @param {number} [style.dropShadowAlpha=1] - Set alpha for the drop shadow * @param {number} [style.dropShadowAngle=Math.PI/6] - Set a angle of the drop shadow * @param {number} [style.dropShadowBlur=0] - Set a shadow blur radius * @param {string} [style.dropShadowColor='#000000'] - A fill style to be used on the dropshadow e.g 'red', '#00FF00' @@ -55,8 +59,10 @@ * fillstyle that will be used on the text e.g 'red', '#00FF00'. Can be an array to create a gradient * eg ['#000000','#FFFFFF'] * {@link https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/fillStyle|MDN} - * @param {number} [style.fillGradientType=PIXI.TEXT_GRADIENT.LINEAR_VERTICAL] - If fills styles are - * supplied, this can change the type/direction of the gradient. See {@link PIXI.TEXT_GRADIENT} for possible values + * @param {number} [style.fillGradientType=PIXI.TEXT_GRADIENT.LINEAR_VERTICAL] - If fill is an array of colours + * to create a gradient, this can change the type/direction of the gradient. See {@link PIXI.TEXT_GRADIENT} + * @param {number[]} [style.fillGradientStops] - If fill is an array of colours to create a gradient, this array can set + * the stop points (numbers between 0 and 1) for the color, overriding the default behaviour of evenly spacing them. * @param {string|string[]} [style.fontFamily='Arial'] - The font family * @param {number|string} [style.fontSize=26] - The font size (as a number it converts to px, but as a string, * equivalents are '26px','20pt','160%' or '1.6em') @@ -76,6 +82,7 @@ * e.g 'blue', '#FCFF00' * @param {number} [style.strokeThickness=0] - A number that represents the thickness of the stroke. * Default is 0 (no stroke) + * @param {boolean} [style.trim=false] - Trim transparent borders * @param {string} [style.textBaseline='alphabetic'] - The baseline of the text that is rendered. * @param {boolean} [style.wordWrap=false] - Indicates if word wrap should be used * @param {number} [style.wordWrapWidth=100] - The width at which text will wrap, it needs wordWrap to be set to true @@ -152,6 +159,19 @@ } } + get dropShadowAlpha() + { + return this._dropShadowAlpha; + } + set dropShadowAlpha(dropShadowAlpha) + { + if (this._dropShadowAlpha !== dropShadowAlpha) + { + this._dropShadowAlpha = dropShadowAlpha; + this.styleID++; + } + } + get dropShadowAngle() { return this._dropShadowAngle; @@ -232,6 +252,19 @@ } } + get fillGradientStops() + { + return this._fillGradientStops; + } + set fillGradientStops(fillGradientStops) + { + if (!areArraysEqual(this._fillGradientStops,fillGradientStops)) + { + this._fillGradientStops = fillGradientStops; + this.styleID++; + } + } + get fontFamily() { return this._fontFamily; @@ -402,6 +435,19 @@ } } + get trim() + { + return this._trim; + } + set trim(trim) + { + if (this._trim !== trim) + { + this._trim = trim; + this.styleID++; + } + } + get wordWrap() { return this._wordWrap; @@ -475,3 +521,34 @@ return color; } } + +/** + * Utility function to convert hexadecimal colors to strings, and simply return the color if it's a string. + * This version can also convert array of colors + * + * @param {Array} array1 First array to compare + * @param {Array} array2 Second array to compare + * @return {boolean} Do the arrays contain the same values in the same order + */ +function areArraysEqual(array1, array2) +{ + if (!Array.isArray(array1) || !Array.isArray(array2)) + { + return false; + } + + if (array1.length !== array2.length) + { + return false; + } + + for (let i = 0; i < array1.length; ++i) + { + if (array1[i] !== array2[i]) + { + return false; + } + } + + return true; +} diff --git a/src/core/textures/BaseRenderTexture.js b/src/core/textures/BaseRenderTexture.js index 5d50146..aa727f0 100644 --- a/src/core/textures/BaseRenderTexture.js +++ b/src/core/textures/BaseRenderTexture.js @@ -1,8 +1,6 @@ import BaseTexture from './BaseTexture'; import settings from '../settings'; -const { RESOLUTION, SCALE_MODE } = settings; - /** * A BaseRenderTexture is a special texture that allows any Pixi display object to be rendered to it. * @@ -54,7 +52,7 @@ { super(null, scaleMode); - this.resolution = resolution || RESOLUTION; + this.resolution = resolution || settings.RESOLUTION; this.width = width; this.height = height; @@ -62,7 +60,7 @@ this.realWidth = this.width * this.resolution; this.realHeight = this.height * this.resolution; - this.scaleMode = scaleMode || SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; this.hasLoaded = true; /** diff --git a/package.json b/package.json index 1271fef..71ade15 100644 --- a/package.json +++ b/package.json @@ -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/scripts/release.js b/scripts/release.js index c4c8e36..156620c 100644 --- a/scripts/release.js +++ b/scripts/release.js @@ -12,7 +12,8 @@ 'dist/**/*', 'lib/**/*', 'src/**/*', - 'scripts/**/*', + 'scripts/*', + 'scripts/renders/*', 'test/**/*', '*.json', '*.md', diff --git a/scripts/renders/client.js b/scripts/renders/client.js index 16c9565..6faee34 100755 --- a/scripts/renders/client.js +++ b/scripts/renders/client.js @@ -1,6 +1,6 @@ 'use strict'; -require('../../bin/pixi'); +require('../../dist/pixi'); PIXI.utils.skipHello(); const fs = require('fs'); 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 bc730db..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. @@ -33,8 +33,11 @@ * need to call toDataUrl on the webgl context * @param {number} [options.resolution=1] - The resolution / device pixel ratio of the renderer, retina would be 2 * @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 @@ -43,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. */ @@ -73,7 +99,7 @@ */ stop() { - this.ticker.stop(); + this._ticker.stop(); } /** @@ -81,7 +107,7 @@ */ start() { - this.ticker.start(); + this._ticker.start(); } /** @@ -95,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 index 81e076b..78b17e6 100644 --- a/src/core/Shader.js +++ b/src/core/Shader.js @@ -1,9 +1,7 @@ import { GLShader } from 'pixi-gl-core'; import settings from './settings'; -const { PRECISION } = settings; - -function checkPrecision(src) +function checkPrecision(src, def) { if (src instanceof Array) { @@ -11,14 +9,14 @@ { const copy = src.slice(0); - copy.unshift(`precision ${PRECISION} float;`); + copy.unshift(`precision ${def} float;`); return copy; } } else if (src.substring(0, 9) !== 'precision') { - return `precision ${PRECISION} float;\n${src}`; + return `precision ${def} float;\n${src}`; } return src; @@ -42,6 +40,7 @@ */ constructor(gl, vertexSrc, fragmentSrc) { - super(gl, checkPrecision(vertexSrc), checkPrecision(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 3425b41..f9235e4 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -181,7 +181,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 b3792ef..17fde8d 100644 --- a/src/core/display/Container.js +++ b/src/core/display/Container.js @@ -73,13 +73,14 @@ } child.parent = this; - - // ensure a transform will be recalculated.. - this.transform._parentID = -1; - this._boundsID++; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.push(child); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(this.children.length - 1); child.emit('added', this); @@ -108,9 +109,14 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.splice(index, 0, child); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(index); child.emit('added', this); @@ -174,6 +180,7 @@ removeItems(this.children, currentIndex, 1); // remove from old position this.children.splice(index, 0, child); // add at new position + this.onChildrenChange(index); } @@ -220,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! @@ -244,9 +252,14 @@ { const child = this.getChildAt(index); + // ensure child transform will be recalculated.. child.parent = null; + child.transform._parentID = -1; removeItems(this.children, index, 1); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(index); child.emit('removed', this); @@ -275,8 +288,14 @@ for (let i = 0; i < removed.length; ++i) { removed[i].parent = null; + if (removed[i].transform) + { + removed[i].transform._parentID = -1; + } } + this._boundsID++; + this.onChildrenChange(beginIndex); for (let i = 0; i < removed.length; ++i) diff --git a/src/core/display/DisplayObject.js b/src/core/display/DisplayObject.js index 3affe36..4e6c77d 100644 --- a/src/core/display/DisplayObject.js +++ b/src/core/display/DisplayObject.js @@ -109,7 +109,7 @@ /** * The original, cached mask of the object * - * @member {PIXI.Rectangle} + * @member {PIXI.Graphics|PIXI.Sprite} * @private */ this._mask = null; diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index 95f2af4..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 ); @@ -1065,10 +1061,15 @@ canvasRenderer = new CanvasRenderer(); } - tempMatrix.tx = -bounds.x; - tempMatrix.ty = -bounds.y; + this.transform.updateLocalTransform(); + this.transform.localTransform.copy(tempMatrix); - canvasRenderer.render(this, canvasBuffer, false, tempMatrix); + tempMatrix.invert(); + + tempMatrix.tx -= bounds.x; + tempMatrix.ty -= bounds.y; + + canvasRenderer.render(this, canvasBuffer, true, tempMatrix); const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode); 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 e67680f..867dde2 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/buildCircle.js b/src/core/graphics/webgl/utils/buildCircle.js index 0400d13..abf5d02 100644 --- a/src/core/graphics/webgl/utils/buildCircle.js +++ b/src/core/graphics/webgl/utils/buildCircle.js @@ -33,6 +33,11 @@ height = circleData.height; } + if (width === 0 || height === 0) + { + return; + } + const totalSegs = Math.floor(30 * Math.sqrt(circleData.radius)) || Math.floor(15 * Math.sqrt(circleData.width + circleData.height)); diff --git a/src/core/graphics/webgl/utils/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 90155f9..f03fe15 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -30,6 +30,7 @@ export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; export { default as CanvasGraphicsRenderer } from './graphics/canvas/CanvasGraphicsRenderer'; +export { default as Spritesheet } from './textures/Spritesheet'; export { default as Texture } from './textures/Texture'; export { default as BaseTexture } from './textures/BaseTexture'; export { default as RenderTexture } from './textures/RenderTexture'; diff --git a/src/core/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 ccce680..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); } /** @@ -162,6 +162,9 @@ if (transform) { transform.copy(tempWt); + + // lets not forget to flag the parent transform as dirty... + this._tempDisplayObjectParent.transform._worldID = -1; } else { @@ -169,6 +172,7 @@ } displayObject.parent = this._tempDisplayObjectParent; + displayObject.updateTransform(); displayObject.parent = cacheParent; // displayObject.hitArea = //TODO add a temp hit area @@ -279,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/canvas/utils/CanvasRenderTarget.js b/src/core/renderers/canvas/utils/CanvasRenderTarget.js index 3de0aef..b409891 100644 --- a/src/core/renderers/canvas/utils/CanvasRenderTarget.js +++ b/src/core/renderers/canvas/utils/CanvasRenderTarget.js @@ -1,5 +1,4 @@ import settings from '../../../settings'; -const { RESOLUTION } = settings; /** * Creates a Canvas element of the given size. @@ -30,7 +29,7 @@ */ this.context = this.canvas.getContext('2d'); - this.resolution = resolution || RESOLUTION; + this.resolution = resolution || settings.RESOLUTION; this.resize(width, height); } 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 ce0e1c4..788ed6c 100644 --- a/src/core/renderers/webgl/WebGLRenderer.js +++ b/src/core/renderers/webgl/WebGLRenderer.js @@ -30,8 +30,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 @@ -49,10 +49,19 @@ * enable this if you need to call toDataUrl on the webgl context. * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when * rendering, stopping pixel interpolation. + * @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; + + if (this.legacy) + { + glCore.VertexArrayObject.FORCE_NATIVE = true; + } /** * The type of this renderer as a standardised const @@ -229,7 +238,7 @@ this.emit('context', gl); // setup the width/height properties and gl viewport - this.resize(this.width, this.height); + this.resize(this.screen.width, this.screen.height); } /** @@ -323,16 +332,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) { @@ -376,6 +385,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 @@ -440,9 +469,10 @@ * Changes the current shader to the one given in parameter * * @param {PIXI.Shader} shader - the new shader + * @param {boolean} [autoProject=true] - Whether automatically set the projection matrix * @return {PIXI.WebGLRenderer} Returns itself. */ - bindShader(shader) + bindShader(shader, autoProject) { // TODO cache if (this._activeShader !== shader) @@ -450,8 +480,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 bbab3b3..944f41d 100644 --- a/src/core/renderers/webgl/filters/Filter.js +++ b/src/core/renderers/webgl/filters/Filter.js @@ -1,6 +1,7 @@ import extractUniformsFromSrc from './extractUniformsFromSrc'; import { uid } from '../../../utils'; import { BLEND_MODES } from '../../../const'; +import settings from '../../../settings'; const SOURCE_KEY_MAP = {}; @@ -10,7 +11,7 @@ * @memberof PIXI * @extends PIXI.Shader */ -class Filter +export default class Filter { /** * @param {string} [vertexSrc] - The source of the vertex shader. @@ -35,8 +36,6 @@ this.blendMode = BLEND_MODES.NORMAL; - // pull out the vertex and shader uniforms if they are not specified.. - // currently this does not extract structs only default types this.uniformData = uniforms || extractUniformsFromSrc(this.vertexSrc, this.fragmentSrc, 'projectionMatrix|uSampler'); /** @@ -80,7 +79,7 @@ * * @member {number} */ - this.resolution = 1; + this.resolution = settings.RESOLUTION; /** * If enabled is true the filter is applied, if false it will not. @@ -97,8 +96,11 @@ * @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 { // --- // // this.uniforms.filterMatrix = filterManager.calculateSpriteMatrix(tempMatrix, window.panda ); @@ -170,5 +172,3 @@ ].join('\n'); } } - -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 dc7072a..6c7422d 100644 --- a/src/core/renderers/webgl/managers/FilterManager.js +++ b/src/core/renderers/webgl/managers/FilterManager.js @@ -154,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 @@ -176,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; @@ -184,7 +184,7 @@ flop = t; } - filters[i].apply(this, flip, lastState.renderTarget, true); + filters[i].apply(this, flip, lastState.renderTarget, false, currentState); this.freePotRenderTarget(flip); this.freePotRenderTarget(flop); @@ -291,9 +291,13 @@ let textureCount = 1; let currentState; - if (shader.uniforms.data.filterArea) + // 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; @@ -306,9 +310,9 @@ // use this to clamp displaced texture coords so they belong to filterArea // see displacementFilter fragment shader for an example - if (shader.uniforms.data.filterClamp) + if (shader.uniforms.filterClamp) { - currentState = this.filterData.stack[this.filterData.index]; + currentState = currentState || this.filterData.stack[this.filterData.index]; const filterClamp = shader.uniforms.filterClamp; @@ -485,7 +489,7 @@ */ destroy() { - this.shaderCache = []; + this.shaderCache = {}; this.emptyPool(); } diff --git a/src/core/renderers/webgl/utils/RenderTarget.js b/src/core/renderers/webgl/utils/RenderTarget.js index afe51ab..6d2467f 100644 --- a/src/core/renderers/webgl/utils/RenderTarget.js +++ b/src/core/renderers/webgl/utils/RenderTarget.js @@ -124,7 +124,7 @@ * @default PIXI.settings.SCALE_MODE * @see PIXI.SCALE_MODES */ - this.scaleMode = scaleMode || settings.SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; /** * Whether this object is the root element or not 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 36ea5bb..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; @@ -346,8 +346,8 @@ /** * Gets the local bounds of the sprite object. * - * @param {Rectangle} rect - The output rectangle. - * @return {Rectangle} The bounds. + * @param {PIXI.Rectangle} rect - The output rectangle. + * @return {PIXI.Rectangle} The bounds. */ getLocalBounds(rect) { @@ -440,7 +440,7 @@ * * @static * @param {number|string|PIXI.BaseTexture|HTMLCanvasElement|HTMLVideoElement} source Source to create texture from - * @return {PIXI.Texture} 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 e77b39a..90b08c0 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++) @@ -103,11 +102,18 @@ { const gl = this.renderer.gl; - // step 1: first check max textures the GPU can handle. - this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); + if (this.renderer.legacy) + { + this.MAX_TEXTURES = 1; + } + else + { + // step 1: first check max textures the GPU can handle. + this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); - // step 2: check the maximum number of if statements the shader can have too.. - this.MAX_TEXTURES = checkMaxIfStatmentsInShader(this.MAX_TEXTURES, gl); + // step 2: check the maximum number of if statements the shader can have too.. + this.MAX_TEXTURES = checkMaxIfStatmentsInShader(this.MAX_TEXTURES, gl); + } const shader = this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); @@ -130,8 +136,12 @@ .addIndex(this.indexBuffer) .addAttribute(this.vertexBuffers[i], shader.attributes.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) .addAttribute(this.vertexBuffers[i], shader.attributes.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) - .addAttribute(this.vertexBuffers[i], shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4) - .addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + .addAttribute(this.vertexBuffers[i], shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); + + if (shader.attributes.aTextureId) + { + this.vaos[i].addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + } /* eslint-enable max-len */ } @@ -378,8 +388,12 @@ .addIndex(this.indexBuffer) .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) - .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4) - .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); + + if (this.shader.attributes.aTextureId) + { + this.vaos[this.vertexCount].addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + } /* eslint-enable max-len */ } diff --git a/src/core/text/Text.js b/src/core/text/Text.js index 013625a..cec8c32 100644 --- a/src/core/text/Text.js +++ b/src/core/text/Text.js @@ -6,6 +6,7 @@ import { TEXT_GRADIENT } from '../const'; import settings from '../settings'; import TextStyle from './TextStyle'; +import trimCanvas from '../utils/trimCanvas'; const defaultDestroyOptions = { texture: true, @@ -57,7 +58,7 @@ /** * The canvas 2d context that everything is drawn with - * @member {HTMLCanvasElement} + * @member {CanvasRenderingContext2D} */ this.context = this.canvas.getContext('2d'); @@ -189,10 +190,12 @@ if (style.dropShadow) { + this.context.shadowBlur = style.dropShadowBlur; + this.context.globalAlpha = style.dropShadowAlpha; + if (style.dropShadowBlur > 0) { this.context.shadowColor = style.dropShadowColor; - this.context.shadowBlur = style.dropShadowBlur; } else { @@ -237,6 +240,10 @@ } } + // reset the shadow blur and alpha that was set by the drop shadow, for the regular text + this.context.shadowBlur = 0; + this.context.globalAlpha = 1; + // set canvas text styles this.context.fillStyle = this._generateFillStyle(style, lines); @@ -326,6 +333,15 @@ */ updateTexture() { + if (this._style.trim) + { + const trimmed = trimCanvas(this.canvas); + + this.canvas.width = trimmed.width; + this.canvas.height = trimmed.height; + this.context.putImageData(trimmed.data, 0, 0); + } + const texture = this._texture; const style = this._style; @@ -475,6 +491,19 @@ } /** + * Gets the local bounds of the text object. + * + * @param {Rectangle} rect - The output rectangle. + * @return {Rectangle} The bounds. + */ + getLocalBounds(rect) + { + this.updateText(true); + + return super.getLocalBounds.call(this, rect); + } + + /** * calculates the bounds of the Text as a rectangle. The bounds calculation takes the worldTransform into account. */ _calculateBounds() @@ -525,6 +554,29 @@ const width = this.canvas.width / this.resolution; const height = this.canvas.height / this.resolution; + // make a copy of the style settings, so we can manipulate them later + const fill = style.fill.slice(); + const fillGradientStops = style.fillGradientStops.slice(); + + // wanting to evenly distribute the fills. So an array of 4 colours should give fills of 0.25, 0.5 and 0.75 + if (!fillGradientStops.length) + { + const lengthPlus1 = fill.length + 1; + + for (let i = 1; i < lengthPlus1; ++i) + { + fillGradientStops.push(i / lengthPlus1); + } + } + + // stop the bleeding of the last gradient on the line above to the top gradient of the this line + // by hard defining the first gradient colour at point 0, and last gradient colour at point 1 + fill.unshift(style.fill[0]); + fillGradientStops.unshift(0); + + fill.push(style.fill[style.fill.length - 1]); + fillGradientStops.push(1); + if (style.fillGradientType === TEXT_GRADIENT.LINEAR_VERTICAL) { // start the gradient at the top center of the canvas, and end at the bottom middle of the canvas @@ -532,15 +584,22 @@ // we need to repeat the gradient so that each individual line of text has the same vertical gradient effect // ['#FF0000', '#00FF00', '#0000FF'] over 2 lines would create stops at 0.125, 0.25, 0.375, 0.625, 0.75, 0.875 - totalIterations = (style.fill.length + 1) * lines.length; + totalIterations = (fill.length + 1) * lines.length; currentIteration = 0; for (let i = 0; i < lines.length; i++) { currentIteration += 1; - for (let j = 0; j < style.fill.length; j++) + for (let j = 0; j < fill.length; j++) { - stop = (currentIteration / totalIterations); - gradient.addColorStop(stop, style.fill[j]); + if (fillGradientStops[j]) + { + stop = (fillGradientStops[j] / lines.length) + (i / lines.length); + } + else + { + stop = currentIteration / totalIterations; + } + gradient.addColorStop(stop, fill[j]); currentIteration++; } } @@ -552,13 +611,20 @@ // can just evenly space out the gradients in this case, as multiple lines makes no difference // to an even left to right gradient - totalIterations = style.fill.length + 1; + totalIterations = fill.length + 1; currentIteration = 1; - for (let i = 0; i < style.fill.length; i++) + for (let i = 0; i < fill.length; i++) { - stop = currentIteration / totalIterations; - gradient.addColorStop(stop, style.fill[i]); + if (fillGradientStops[i]) + { + stop = fillGradientStops[i]; + } + else + { + stop = currentIteration / totalIterations; + } + gradient.addColorStop(stop, fill[i]); currentIteration++; } } @@ -569,7 +635,7 @@ /** * Destroys this text object. * Note* Unlike a Sprite, a Text object will automatically destroy its baseTexture and texture as - * the majorety of the time the texture will not be shared with any other Sprites. + * the majority of the time the texture will not be shared with any other Sprites. * * @param {object|boolean} [options] - Options parameter. A boolean will act as if all options * have been set to that value @@ -680,7 +746,7 @@ set text(text) // eslint-disable-line require-jsdoc { - text = String(text || ' '); + text = String(text === '' || text === null || text === undefined ? ' ' : text); if (this._text === text) { diff --git a/src/core/text/TextStyle.js b/src/core/text/TextStyle.js index 3b25528..890950b 100644 --- a/src/core/text/TextStyle.js +++ b/src/core/text/TextStyle.js @@ -8,12 +8,14 @@ align: 'left', breakWords: false, dropShadow: false, + dropShadowAlpha: 1, dropShadowAngle: Math.PI / 6, dropShadowBlur: 0, dropShadowColor: '#000000', dropShadowDistance: 5, fill: 'black', fillGradientType: TEXT_GRADIENT.LINEAR_VERTICAL, + fillGradientStops: [], fontFamily: 'Arial', fontSize: 26, fontStyle: 'normal', @@ -27,6 +29,7 @@ stroke: 'black', strokeThickness: 0, textBaseline: 'alphabetic', + trim: false, wordWrap: false, wordWrapWidth: 100, }; @@ -47,6 +50,7 @@ * @param {boolean} [style.breakWords=false] - Indicates if lines can be wrapped within words, it * needs wordWrap to be set to true * @param {boolean} [style.dropShadow=false] - Set a drop shadow for the text + * @param {number} [style.dropShadowAlpha=1] - Set alpha for the drop shadow * @param {number} [style.dropShadowAngle=Math.PI/6] - Set a angle of the drop shadow * @param {number} [style.dropShadowBlur=0] - Set a shadow blur radius * @param {string} [style.dropShadowColor='#000000'] - A fill style to be used on the dropshadow e.g 'red', '#00FF00' @@ -55,8 +59,10 @@ * fillstyle that will be used on the text e.g 'red', '#00FF00'. Can be an array to create a gradient * eg ['#000000','#FFFFFF'] * {@link https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/fillStyle|MDN} - * @param {number} [style.fillGradientType=PIXI.TEXT_GRADIENT.LINEAR_VERTICAL] - If fills styles are - * supplied, this can change the type/direction of the gradient. See {@link PIXI.TEXT_GRADIENT} for possible values + * @param {number} [style.fillGradientType=PIXI.TEXT_GRADIENT.LINEAR_VERTICAL] - If fill is an array of colours + * to create a gradient, this can change the type/direction of the gradient. See {@link PIXI.TEXT_GRADIENT} + * @param {number[]} [style.fillGradientStops] - If fill is an array of colours to create a gradient, this array can set + * the stop points (numbers between 0 and 1) for the color, overriding the default behaviour of evenly spacing them. * @param {string|string[]} [style.fontFamily='Arial'] - The font family * @param {number|string} [style.fontSize=26] - The font size (as a number it converts to px, but as a string, * equivalents are '26px','20pt','160%' or '1.6em') @@ -76,6 +82,7 @@ * e.g 'blue', '#FCFF00' * @param {number} [style.strokeThickness=0] - A number that represents the thickness of the stroke. * Default is 0 (no stroke) + * @param {boolean} [style.trim=false] - Trim transparent borders * @param {string} [style.textBaseline='alphabetic'] - The baseline of the text that is rendered. * @param {boolean} [style.wordWrap=false] - Indicates if word wrap should be used * @param {number} [style.wordWrapWidth=100] - The width at which text will wrap, it needs wordWrap to be set to true @@ -152,6 +159,19 @@ } } + get dropShadowAlpha() + { + return this._dropShadowAlpha; + } + set dropShadowAlpha(dropShadowAlpha) + { + if (this._dropShadowAlpha !== dropShadowAlpha) + { + this._dropShadowAlpha = dropShadowAlpha; + this.styleID++; + } + } + get dropShadowAngle() { return this._dropShadowAngle; @@ -232,6 +252,19 @@ } } + get fillGradientStops() + { + return this._fillGradientStops; + } + set fillGradientStops(fillGradientStops) + { + if (!areArraysEqual(this._fillGradientStops,fillGradientStops)) + { + this._fillGradientStops = fillGradientStops; + this.styleID++; + } + } + get fontFamily() { return this._fontFamily; @@ -402,6 +435,19 @@ } } + get trim() + { + return this._trim; + } + set trim(trim) + { + if (this._trim !== trim) + { + this._trim = trim; + this.styleID++; + } + } + get wordWrap() { return this._wordWrap; @@ -475,3 +521,34 @@ return color; } } + +/** + * Utility function to convert hexadecimal colors to strings, and simply return the color if it's a string. + * This version can also convert array of colors + * + * @param {Array} array1 First array to compare + * @param {Array} array2 Second array to compare + * @return {boolean} Do the arrays contain the same values in the same order + */ +function areArraysEqual(array1, array2) +{ + if (!Array.isArray(array1) || !Array.isArray(array2)) + { + return false; + } + + if (array1.length !== array2.length) + { + return false; + } + + for (let i = 0; i < array1.length; ++i) + { + if (array1[i] !== array2[i]) + { + return false; + } + } + + return true; +} diff --git a/src/core/textures/BaseRenderTexture.js b/src/core/textures/BaseRenderTexture.js index 5d50146..aa727f0 100644 --- a/src/core/textures/BaseRenderTexture.js +++ b/src/core/textures/BaseRenderTexture.js @@ -1,8 +1,6 @@ import BaseTexture from './BaseTexture'; import settings from '../settings'; -const { RESOLUTION, SCALE_MODE } = settings; - /** * A BaseRenderTexture is a special texture that allows any Pixi display object to be rendered to it. * @@ -54,7 +52,7 @@ { super(null, scaleMode); - this.resolution = resolution || RESOLUTION; + this.resolution = resolution || settings.RESOLUTION; this.width = width; this.height = height; @@ -62,7 +60,7 @@ this.realWidth = this.width * this.resolution; this.realHeight = this.height * this.resolution; - this.scaleMode = scaleMode || SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; this.hasLoaded = true; /** diff --git a/src/core/textures/BaseTexture.js b/src/core/textures/BaseTexture.js index 676dca3..aeccf39 100644 --- a/src/core/textures/BaseTexture.js +++ b/src/core/textures/BaseTexture.js @@ -77,7 +77,7 @@ * @default PIXI.settings.SCALE_MODE * @see PIXI.SCALE_MODES */ - this.scaleMode = scaleMode || settings.SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; /** * Set to true once the base texture has successfully loaded. @@ -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 1271fef..71ade15 100644 --- a/package.json +++ b/package.json @@ -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/scripts/release.js b/scripts/release.js index c4c8e36..156620c 100644 --- a/scripts/release.js +++ b/scripts/release.js @@ -12,7 +12,8 @@ 'dist/**/*', 'lib/**/*', 'src/**/*', - 'scripts/**/*', + 'scripts/*', + 'scripts/renders/*', 'test/**/*', '*.json', '*.md', diff --git a/scripts/renders/client.js b/scripts/renders/client.js index 16c9565..6faee34 100755 --- a/scripts/renders/client.js +++ b/scripts/renders/client.js @@ -1,6 +1,6 @@ 'use strict'; -require('../../bin/pixi'); +require('../../dist/pixi'); PIXI.utils.skipHello(); const fs = require('fs'); 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 bc730db..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. @@ -33,8 +33,11 @@ * need to call toDataUrl on the webgl context * @param {number} [options.resolution=1] - The resolution / device pixel ratio of the renderer, retina would be 2 * @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 @@ -43,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. */ @@ -73,7 +99,7 @@ */ stop() { - this.ticker.stop(); + this._ticker.stop(); } /** @@ -81,7 +107,7 @@ */ start() { - this.ticker.start(); + this._ticker.start(); } /** @@ -95,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 index 81e076b..78b17e6 100644 --- a/src/core/Shader.js +++ b/src/core/Shader.js @@ -1,9 +1,7 @@ import { GLShader } from 'pixi-gl-core'; import settings from './settings'; -const { PRECISION } = settings; - -function checkPrecision(src) +function checkPrecision(src, def) { if (src instanceof Array) { @@ -11,14 +9,14 @@ { const copy = src.slice(0); - copy.unshift(`precision ${PRECISION} float;`); + copy.unshift(`precision ${def} float;`); return copy; } } else if (src.substring(0, 9) !== 'precision') { - return `precision ${PRECISION} float;\n${src}`; + return `precision ${def} float;\n${src}`; } return src; @@ -42,6 +40,7 @@ */ constructor(gl, vertexSrc, fragmentSrc) { - super(gl, checkPrecision(vertexSrc), checkPrecision(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 3425b41..f9235e4 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -181,7 +181,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 b3792ef..17fde8d 100644 --- a/src/core/display/Container.js +++ b/src/core/display/Container.js @@ -73,13 +73,14 @@ } child.parent = this; - - // ensure a transform will be recalculated.. - this.transform._parentID = -1; - this._boundsID++; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.push(child); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(this.children.length - 1); child.emit('added', this); @@ -108,9 +109,14 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.splice(index, 0, child); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(index); child.emit('added', this); @@ -174,6 +180,7 @@ removeItems(this.children, currentIndex, 1); // remove from old position this.children.splice(index, 0, child); // add at new position + this.onChildrenChange(index); } @@ -220,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! @@ -244,9 +252,14 @@ { const child = this.getChildAt(index); + // ensure child transform will be recalculated.. child.parent = null; + child.transform._parentID = -1; removeItems(this.children, index, 1); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(index); child.emit('removed', this); @@ -275,8 +288,14 @@ for (let i = 0; i < removed.length; ++i) { removed[i].parent = null; + if (removed[i].transform) + { + removed[i].transform._parentID = -1; + } } + this._boundsID++; + this.onChildrenChange(beginIndex); for (let i = 0; i < removed.length; ++i) diff --git a/src/core/display/DisplayObject.js b/src/core/display/DisplayObject.js index 3affe36..4e6c77d 100644 --- a/src/core/display/DisplayObject.js +++ b/src/core/display/DisplayObject.js @@ -109,7 +109,7 @@ /** * The original, cached mask of the object * - * @member {PIXI.Rectangle} + * @member {PIXI.Graphics|PIXI.Sprite} * @private */ this._mask = null; diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index 95f2af4..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 ); @@ -1065,10 +1061,15 @@ canvasRenderer = new CanvasRenderer(); } - tempMatrix.tx = -bounds.x; - tempMatrix.ty = -bounds.y; + this.transform.updateLocalTransform(); + this.transform.localTransform.copy(tempMatrix); - canvasRenderer.render(this, canvasBuffer, false, tempMatrix); + tempMatrix.invert(); + + tempMatrix.tx -= bounds.x; + tempMatrix.ty -= bounds.y; + + canvasRenderer.render(this, canvasBuffer, true, tempMatrix); const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode); 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 e67680f..867dde2 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/buildCircle.js b/src/core/graphics/webgl/utils/buildCircle.js index 0400d13..abf5d02 100644 --- a/src/core/graphics/webgl/utils/buildCircle.js +++ b/src/core/graphics/webgl/utils/buildCircle.js @@ -33,6 +33,11 @@ height = circleData.height; } + if (width === 0 || height === 0) + { + return; + } + const totalSegs = Math.floor(30 * Math.sqrt(circleData.radius)) || Math.floor(15 * Math.sqrt(circleData.width + circleData.height)); diff --git a/src/core/graphics/webgl/utils/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 90155f9..f03fe15 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -30,6 +30,7 @@ export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; export { default as CanvasGraphicsRenderer } from './graphics/canvas/CanvasGraphicsRenderer'; +export { default as Spritesheet } from './textures/Spritesheet'; export { default as Texture } from './textures/Texture'; export { default as BaseTexture } from './textures/BaseTexture'; export { default as RenderTexture } from './textures/RenderTexture'; diff --git a/src/core/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 ccce680..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); } /** @@ -162,6 +162,9 @@ if (transform) { transform.copy(tempWt); + + // lets not forget to flag the parent transform as dirty... + this._tempDisplayObjectParent.transform._worldID = -1; } else { @@ -169,6 +172,7 @@ } displayObject.parent = this._tempDisplayObjectParent; + displayObject.updateTransform(); displayObject.parent = cacheParent; // displayObject.hitArea = //TODO add a temp hit area @@ -279,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/canvas/utils/CanvasRenderTarget.js b/src/core/renderers/canvas/utils/CanvasRenderTarget.js index 3de0aef..b409891 100644 --- a/src/core/renderers/canvas/utils/CanvasRenderTarget.js +++ b/src/core/renderers/canvas/utils/CanvasRenderTarget.js @@ -1,5 +1,4 @@ import settings from '../../../settings'; -const { RESOLUTION } = settings; /** * Creates a Canvas element of the given size. @@ -30,7 +29,7 @@ */ this.context = this.canvas.getContext('2d'); - this.resolution = resolution || RESOLUTION; + this.resolution = resolution || settings.RESOLUTION; this.resize(width, height); } 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 ce0e1c4..788ed6c 100644 --- a/src/core/renderers/webgl/WebGLRenderer.js +++ b/src/core/renderers/webgl/WebGLRenderer.js @@ -30,8 +30,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 @@ -49,10 +49,19 @@ * enable this if you need to call toDataUrl on the webgl context. * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when * rendering, stopping pixel interpolation. + * @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; + + if (this.legacy) + { + glCore.VertexArrayObject.FORCE_NATIVE = true; + } /** * The type of this renderer as a standardised const @@ -229,7 +238,7 @@ this.emit('context', gl); // setup the width/height properties and gl viewport - this.resize(this.width, this.height); + this.resize(this.screen.width, this.screen.height); } /** @@ -323,16 +332,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) { @@ -376,6 +385,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 @@ -440,9 +469,10 @@ * Changes the current shader to the one given in parameter * * @param {PIXI.Shader} shader - the new shader + * @param {boolean} [autoProject=true] - Whether automatically set the projection matrix * @return {PIXI.WebGLRenderer} Returns itself. */ - bindShader(shader) + bindShader(shader, autoProject) { // TODO cache if (this._activeShader !== shader) @@ -450,8 +480,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 bbab3b3..944f41d 100644 --- a/src/core/renderers/webgl/filters/Filter.js +++ b/src/core/renderers/webgl/filters/Filter.js @@ -1,6 +1,7 @@ import extractUniformsFromSrc from './extractUniformsFromSrc'; import { uid } from '../../../utils'; import { BLEND_MODES } from '../../../const'; +import settings from '../../../settings'; const SOURCE_KEY_MAP = {}; @@ -10,7 +11,7 @@ * @memberof PIXI * @extends PIXI.Shader */ -class Filter +export default class Filter { /** * @param {string} [vertexSrc] - The source of the vertex shader. @@ -35,8 +36,6 @@ this.blendMode = BLEND_MODES.NORMAL; - // pull out the vertex and shader uniforms if they are not specified.. - // currently this does not extract structs only default types this.uniformData = uniforms || extractUniformsFromSrc(this.vertexSrc, this.fragmentSrc, 'projectionMatrix|uSampler'); /** @@ -80,7 +79,7 @@ * * @member {number} */ - this.resolution = 1; + this.resolution = settings.RESOLUTION; /** * If enabled is true the filter is applied, if false it will not. @@ -97,8 +96,11 @@ * @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 { // --- // // this.uniforms.filterMatrix = filterManager.calculateSpriteMatrix(tempMatrix, window.panda ); @@ -170,5 +172,3 @@ ].join('\n'); } } - -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 dc7072a..6c7422d 100644 --- a/src/core/renderers/webgl/managers/FilterManager.js +++ b/src/core/renderers/webgl/managers/FilterManager.js @@ -154,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 @@ -176,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; @@ -184,7 +184,7 @@ flop = t; } - filters[i].apply(this, flip, lastState.renderTarget, true); + filters[i].apply(this, flip, lastState.renderTarget, false, currentState); this.freePotRenderTarget(flip); this.freePotRenderTarget(flop); @@ -291,9 +291,13 @@ let textureCount = 1; let currentState; - if (shader.uniforms.data.filterArea) + // 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; @@ -306,9 +310,9 @@ // use this to clamp displaced texture coords so they belong to filterArea // see displacementFilter fragment shader for an example - if (shader.uniforms.data.filterClamp) + if (shader.uniforms.filterClamp) { - currentState = this.filterData.stack[this.filterData.index]; + currentState = currentState || this.filterData.stack[this.filterData.index]; const filterClamp = shader.uniforms.filterClamp; @@ -485,7 +489,7 @@ */ destroy() { - this.shaderCache = []; + this.shaderCache = {}; this.emptyPool(); } diff --git a/src/core/renderers/webgl/utils/RenderTarget.js b/src/core/renderers/webgl/utils/RenderTarget.js index afe51ab..6d2467f 100644 --- a/src/core/renderers/webgl/utils/RenderTarget.js +++ b/src/core/renderers/webgl/utils/RenderTarget.js @@ -124,7 +124,7 @@ * @default PIXI.settings.SCALE_MODE * @see PIXI.SCALE_MODES */ - this.scaleMode = scaleMode || settings.SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; /** * Whether this object is the root element or not 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 36ea5bb..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; @@ -346,8 +346,8 @@ /** * Gets the local bounds of the sprite object. * - * @param {Rectangle} rect - The output rectangle. - * @return {Rectangle} The bounds. + * @param {PIXI.Rectangle} rect - The output rectangle. + * @return {PIXI.Rectangle} The bounds. */ getLocalBounds(rect) { @@ -440,7 +440,7 @@ * * @static * @param {number|string|PIXI.BaseTexture|HTMLCanvasElement|HTMLVideoElement} source Source to create texture from - * @return {PIXI.Texture} 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 e77b39a..90b08c0 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++) @@ -103,11 +102,18 @@ { const gl = this.renderer.gl; - // step 1: first check max textures the GPU can handle. - this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); + if (this.renderer.legacy) + { + this.MAX_TEXTURES = 1; + } + else + { + // step 1: first check max textures the GPU can handle. + this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); - // step 2: check the maximum number of if statements the shader can have too.. - this.MAX_TEXTURES = checkMaxIfStatmentsInShader(this.MAX_TEXTURES, gl); + // step 2: check the maximum number of if statements the shader can have too.. + this.MAX_TEXTURES = checkMaxIfStatmentsInShader(this.MAX_TEXTURES, gl); + } const shader = this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); @@ -130,8 +136,12 @@ .addIndex(this.indexBuffer) .addAttribute(this.vertexBuffers[i], shader.attributes.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) .addAttribute(this.vertexBuffers[i], shader.attributes.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) - .addAttribute(this.vertexBuffers[i], shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4) - .addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + .addAttribute(this.vertexBuffers[i], shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); + + if (shader.attributes.aTextureId) + { + this.vaos[i].addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + } /* eslint-enable max-len */ } @@ -378,8 +388,12 @@ .addIndex(this.indexBuffer) .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) - .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4) - .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); + + if (this.shader.attributes.aTextureId) + { + this.vaos[this.vertexCount].addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + } /* eslint-enable max-len */ } diff --git a/src/core/text/Text.js b/src/core/text/Text.js index 013625a..cec8c32 100644 --- a/src/core/text/Text.js +++ b/src/core/text/Text.js @@ -6,6 +6,7 @@ import { TEXT_GRADIENT } from '../const'; import settings from '../settings'; import TextStyle from './TextStyle'; +import trimCanvas from '../utils/trimCanvas'; const defaultDestroyOptions = { texture: true, @@ -57,7 +58,7 @@ /** * The canvas 2d context that everything is drawn with - * @member {HTMLCanvasElement} + * @member {CanvasRenderingContext2D} */ this.context = this.canvas.getContext('2d'); @@ -189,10 +190,12 @@ if (style.dropShadow) { + this.context.shadowBlur = style.dropShadowBlur; + this.context.globalAlpha = style.dropShadowAlpha; + if (style.dropShadowBlur > 0) { this.context.shadowColor = style.dropShadowColor; - this.context.shadowBlur = style.dropShadowBlur; } else { @@ -237,6 +240,10 @@ } } + // reset the shadow blur and alpha that was set by the drop shadow, for the regular text + this.context.shadowBlur = 0; + this.context.globalAlpha = 1; + // set canvas text styles this.context.fillStyle = this._generateFillStyle(style, lines); @@ -326,6 +333,15 @@ */ updateTexture() { + if (this._style.trim) + { + const trimmed = trimCanvas(this.canvas); + + this.canvas.width = trimmed.width; + this.canvas.height = trimmed.height; + this.context.putImageData(trimmed.data, 0, 0); + } + const texture = this._texture; const style = this._style; @@ -475,6 +491,19 @@ } /** + * Gets the local bounds of the text object. + * + * @param {Rectangle} rect - The output rectangle. + * @return {Rectangle} The bounds. + */ + getLocalBounds(rect) + { + this.updateText(true); + + return super.getLocalBounds.call(this, rect); + } + + /** * calculates the bounds of the Text as a rectangle. The bounds calculation takes the worldTransform into account. */ _calculateBounds() @@ -525,6 +554,29 @@ const width = this.canvas.width / this.resolution; const height = this.canvas.height / this.resolution; + // make a copy of the style settings, so we can manipulate them later + const fill = style.fill.slice(); + const fillGradientStops = style.fillGradientStops.slice(); + + // wanting to evenly distribute the fills. So an array of 4 colours should give fills of 0.25, 0.5 and 0.75 + if (!fillGradientStops.length) + { + const lengthPlus1 = fill.length + 1; + + for (let i = 1; i < lengthPlus1; ++i) + { + fillGradientStops.push(i / lengthPlus1); + } + } + + // stop the bleeding of the last gradient on the line above to the top gradient of the this line + // by hard defining the first gradient colour at point 0, and last gradient colour at point 1 + fill.unshift(style.fill[0]); + fillGradientStops.unshift(0); + + fill.push(style.fill[style.fill.length - 1]); + fillGradientStops.push(1); + if (style.fillGradientType === TEXT_GRADIENT.LINEAR_VERTICAL) { // start the gradient at the top center of the canvas, and end at the bottom middle of the canvas @@ -532,15 +584,22 @@ // we need to repeat the gradient so that each individual line of text has the same vertical gradient effect // ['#FF0000', '#00FF00', '#0000FF'] over 2 lines would create stops at 0.125, 0.25, 0.375, 0.625, 0.75, 0.875 - totalIterations = (style.fill.length + 1) * lines.length; + totalIterations = (fill.length + 1) * lines.length; currentIteration = 0; for (let i = 0; i < lines.length; i++) { currentIteration += 1; - for (let j = 0; j < style.fill.length; j++) + for (let j = 0; j < fill.length; j++) { - stop = (currentIteration / totalIterations); - gradient.addColorStop(stop, style.fill[j]); + if (fillGradientStops[j]) + { + stop = (fillGradientStops[j] / lines.length) + (i / lines.length); + } + else + { + stop = currentIteration / totalIterations; + } + gradient.addColorStop(stop, fill[j]); currentIteration++; } } @@ -552,13 +611,20 @@ // can just evenly space out the gradients in this case, as multiple lines makes no difference // to an even left to right gradient - totalIterations = style.fill.length + 1; + totalIterations = fill.length + 1; currentIteration = 1; - for (let i = 0; i < style.fill.length; i++) + for (let i = 0; i < fill.length; i++) { - stop = currentIteration / totalIterations; - gradient.addColorStop(stop, style.fill[i]); + if (fillGradientStops[i]) + { + stop = fillGradientStops[i]; + } + else + { + stop = currentIteration / totalIterations; + } + gradient.addColorStop(stop, fill[i]); currentIteration++; } } @@ -569,7 +635,7 @@ /** * Destroys this text object. * Note* Unlike a Sprite, a Text object will automatically destroy its baseTexture and texture as - * the majorety of the time the texture will not be shared with any other Sprites. + * the majority of the time the texture will not be shared with any other Sprites. * * @param {object|boolean} [options] - Options parameter. A boolean will act as if all options * have been set to that value @@ -680,7 +746,7 @@ set text(text) // eslint-disable-line require-jsdoc { - text = String(text || ' '); + text = String(text === '' || text === null || text === undefined ? ' ' : text); if (this._text === text) { diff --git a/src/core/text/TextStyle.js b/src/core/text/TextStyle.js index 3b25528..890950b 100644 --- a/src/core/text/TextStyle.js +++ b/src/core/text/TextStyle.js @@ -8,12 +8,14 @@ align: 'left', breakWords: false, dropShadow: false, + dropShadowAlpha: 1, dropShadowAngle: Math.PI / 6, dropShadowBlur: 0, dropShadowColor: '#000000', dropShadowDistance: 5, fill: 'black', fillGradientType: TEXT_GRADIENT.LINEAR_VERTICAL, + fillGradientStops: [], fontFamily: 'Arial', fontSize: 26, fontStyle: 'normal', @@ -27,6 +29,7 @@ stroke: 'black', strokeThickness: 0, textBaseline: 'alphabetic', + trim: false, wordWrap: false, wordWrapWidth: 100, }; @@ -47,6 +50,7 @@ * @param {boolean} [style.breakWords=false] - Indicates if lines can be wrapped within words, it * needs wordWrap to be set to true * @param {boolean} [style.dropShadow=false] - Set a drop shadow for the text + * @param {number} [style.dropShadowAlpha=1] - Set alpha for the drop shadow * @param {number} [style.dropShadowAngle=Math.PI/6] - Set a angle of the drop shadow * @param {number} [style.dropShadowBlur=0] - Set a shadow blur radius * @param {string} [style.dropShadowColor='#000000'] - A fill style to be used on the dropshadow e.g 'red', '#00FF00' @@ -55,8 +59,10 @@ * fillstyle that will be used on the text e.g 'red', '#00FF00'. Can be an array to create a gradient * eg ['#000000','#FFFFFF'] * {@link https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/fillStyle|MDN} - * @param {number} [style.fillGradientType=PIXI.TEXT_GRADIENT.LINEAR_VERTICAL] - If fills styles are - * supplied, this can change the type/direction of the gradient. See {@link PIXI.TEXT_GRADIENT} for possible values + * @param {number} [style.fillGradientType=PIXI.TEXT_GRADIENT.LINEAR_VERTICAL] - If fill is an array of colours + * to create a gradient, this can change the type/direction of the gradient. See {@link PIXI.TEXT_GRADIENT} + * @param {number[]} [style.fillGradientStops] - If fill is an array of colours to create a gradient, this array can set + * the stop points (numbers between 0 and 1) for the color, overriding the default behaviour of evenly spacing them. * @param {string|string[]} [style.fontFamily='Arial'] - The font family * @param {number|string} [style.fontSize=26] - The font size (as a number it converts to px, but as a string, * equivalents are '26px','20pt','160%' or '1.6em') @@ -76,6 +82,7 @@ * e.g 'blue', '#FCFF00' * @param {number} [style.strokeThickness=0] - A number that represents the thickness of the stroke. * Default is 0 (no stroke) + * @param {boolean} [style.trim=false] - Trim transparent borders * @param {string} [style.textBaseline='alphabetic'] - The baseline of the text that is rendered. * @param {boolean} [style.wordWrap=false] - Indicates if word wrap should be used * @param {number} [style.wordWrapWidth=100] - The width at which text will wrap, it needs wordWrap to be set to true @@ -152,6 +159,19 @@ } } + get dropShadowAlpha() + { + return this._dropShadowAlpha; + } + set dropShadowAlpha(dropShadowAlpha) + { + if (this._dropShadowAlpha !== dropShadowAlpha) + { + this._dropShadowAlpha = dropShadowAlpha; + this.styleID++; + } + } + get dropShadowAngle() { return this._dropShadowAngle; @@ -232,6 +252,19 @@ } } + get fillGradientStops() + { + return this._fillGradientStops; + } + set fillGradientStops(fillGradientStops) + { + if (!areArraysEqual(this._fillGradientStops,fillGradientStops)) + { + this._fillGradientStops = fillGradientStops; + this.styleID++; + } + } + get fontFamily() { return this._fontFamily; @@ -402,6 +435,19 @@ } } + get trim() + { + return this._trim; + } + set trim(trim) + { + if (this._trim !== trim) + { + this._trim = trim; + this.styleID++; + } + } + get wordWrap() { return this._wordWrap; @@ -475,3 +521,34 @@ return color; } } + +/** + * Utility function to convert hexadecimal colors to strings, and simply return the color if it's a string. + * This version can also convert array of colors + * + * @param {Array} array1 First array to compare + * @param {Array} array2 Second array to compare + * @return {boolean} Do the arrays contain the same values in the same order + */ +function areArraysEqual(array1, array2) +{ + if (!Array.isArray(array1) || !Array.isArray(array2)) + { + return false; + } + + if (array1.length !== array2.length) + { + return false; + } + + for (let i = 0; i < array1.length; ++i) + { + if (array1[i] !== array2[i]) + { + return false; + } + } + + return true; +} diff --git a/src/core/textures/BaseRenderTexture.js b/src/core/textures/BaseRenderTexture.js index 5d50146..aa727f0 100644 --- a/src/core/textures/BaseRenderTexture.js +++ b/src/core/textures/BaseRenderTexture.js @@ -1,8 +1,6 @@ import BaseTexture from './BaseTexture'; import settings from '../settings'; -const { RESOLUTION, SCALE_MODE } = settings; - /** * A BaseRenderTexture is a special texture that allows any Pixi display object to be rendered to it. * @@ -54,7 +52,7 @@ { super(null, scaleMode); - this.resolution = resolution || RESOLUTION; + this.resolution = resolution || settings.RESOLUTION; this.width = width; this.height = height; @@ -62,7 +60,7 @@ this.realWidth = this.width * this.resolution; this.realHeight = this.height * this.resolution; - this.scaleMode = scaleMode || SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; this.hasLoaded = true; /** diff --git a/src/core/textures/BaseTexture.js b/src/core/textures/BaseTexture.js index 676dca3..aeccf39 100644 --- a/src/core/textures/BaseTexture.js +++ b/src/core/textures/BaseTexture.js @@ -77,7 +77,7 @@ * @default PIXI.settings.SCALE_MODE * @see PIXI.SCALE_MODES */ - this.scaleMode = scaleMode || settings.SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; /** * Set to true once the base texture has successfully loaded. @@ -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/RenderTexture.js b/src/core/textures/RenderTexture.js index 4c5034f..b3c6297 100644 --- a/src/core/textures/RenderTexture.js +++ b/src/core/textures/RenderTexture.js @@ -54,8 +54,8 @@ /* eslint-disable prefer-rest-params, no-console */ const width = arguments[1]; const height = arguments[2]; - const scaleMode = arguments[3] || 0; - const resolution = arguments[4] || 1; + const scaleMode = arguments[3]; + const resolution = arguments[4]; // we have an old render texture.. console.warn(`Please use RenderTexture.create(${width}, ${height}) instead of the ctor directly.`); diff --git a/package.json b/package.json index 1271fef..71ade15 100644 --- a/package.json +++ b/package.json @@ -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/scripts/release.js b/scripts/release.js index c4c8e36..156620c 100644 --- a/scripts/release.js +++ b/scripts/release.js @@ -12,7 +12,8 @@ 'dist/**/*', 'lib/**/*', 'src/**/*', - 'scripts/**/*', + 'scripts/*', + 'scripts/renders/*', 'test/**/*', '*.json', '*.md', diff --git a/scripts/renders/client.js b/scripts/renders/client.js index 16c9565..6faee34 100755 --- a/scripts/renders/client.js +++ b/scripts/renders/client.js @@ -1,6 +1,6 @@ 'use strict'; -require('../../bin/pixi'); +require('../../dist/pixi'); PIXI.utils.skipHello(); const fs = require('fs'); 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 bc730db..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. @@ -33,8 +33,11 @@ * need to call toDataUrl on the webgl context * @param {number} [options.resolution=1] - The resolution / device pixel ratio of the renderer, retina would be 2 * @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 @@ -43,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. */ @@ -73,7 +99,7 @@ */ stop() { - this.ticker.stop(); + this._ticker.stop(); } /** @@ -81,7 +107,7 @@ */ start() { - this.ticker.start(); + this._ticker.start(); } /** @@ -95,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 index 81e076b..78b17e6 100644 --- a/src/core/Shader.js +++ b/src/core/Shader.js @@ -1,9 +1,7 @@ import { GLShader } from 'pixi-gl-core'; import settings from './settings'; -const { PRECISION } = settings; - -function checkPrecision(src) +function checkPrecision(src, def) { if (src instanceof Array) { @@ -11,14 +9,14 @@ { const copy = src.slice(0); - copy.unshift(`precision ${PRECISION} float;`); + copy.unshift(`precision ${def} float;`); return copy; } } else if (src.substring(0, 9) !== 'precision') { - return `precision ${PRECISION} float;\n${src}`; + return `precision ${def} float;\n${src}`; } return src; @@ -42,6 +40,7 @@ */ constructor(gl, vertexSrc, fragmentSrc) { - super(gl, checkPrecision(vertexSrc), checkPrecision(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 3425b41..f9235e4 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -181,7 +181,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 b3792ef..17fde8d 100644 --- a/src/core/display/Container.js +++ b/src/core/display/Container.js @@ -73,13 +73,14 @@ } child.parent = this; - - // ensure a transform will be recalculated.. - this.transform._parentID = -1; - this._boundsID++; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.push(child); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(this.children.length - 1); child.emit('added', this); @@ -108,9 +109,14 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.splice(index, 0, child); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(index); child.emit('added', this); @@ -174,6 +180,7 @@ removeItems(this.children, currentIndex, 1); // remove from old position this.children.splice(index, 0, child); // add at new position + this.onChildrenChange(index); } @@ -220,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! @@ -244,9 +252,14 @@ { const child = this.getChildAt(index); + // ensure child transform will be recalculated.. child.parent = null; + child.transform._parentID = -1; removeItems(this.children, index, 1); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(index); child.emit('removed', this); @@ -275,8 +288,14 @@ for (let i = 0; i < removed.length; ++i) { removed[i].parent = null; + if (removed[i].transform) + { + removed[i].transform._parentID = -1; + } } + this._boundsID++; + this.onChildrenChange(beginIndex); for (let i = 0; i < removed.length; ++i) diff --git a/src/core/display/DisplayObject.js b/src/core/display/DisplayObject.js index 3affe36..4e6c77d 100644 --- a/src/core/display/DisplayObject.js +++ b/src/core/display/DisplayObject.js @@ -109,7 +109,7 @@ /** * The original, cached mask of the object * - * @member {PIXI.Rectangle} + * @member {PIXI.Graphics|PIXI.Sprite} * @private */ this._mask = null; diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index 95f2af4..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 ); @@ -1065,10 +1061,15 @@ canvasRenderer = new CanvasRenderer(); } - tempMatrix.tx = -bounds.x; - tempMatrix.ty = -bounds.y; + this.transform.updateLocalTransform(); + this.transform.localTransform.copy(tempMatrix); - canvasRenderer.render(this, canvasBuffer, false, tempMatrix); + tempMatrix.invert(); + + tempMatrix.tx -= bounds.x; + tempMatrix.ty -= bounds.y; + + canvasRenderer.render(this, canvasBuffer, true, tempMatrix); const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode); 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 e67680f..867dde2 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/buildCircle.js b/src/core/graphics/webgl/utils/buildCircle.js index 0400d13..abf5d02 100644 --- a/src/core/graphics/webgl/utils/buildCircle.js +++ b/src/core/graphics/webgl/utils/buildCircle.js @@ -33,6 +33,11 @@ height = circleData.height; } + if (width === 0 || height === 0) + { + return; + } + const totalSegs = Math.floor(30 * Math.sqrt(circleData.radius)) || Math.floor(15 * Math.sqrt(circleData.width + circleData.height)); diff --git a/src/core/graphics/webgl/utils/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 90155f9..f03fe15 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -30,6 +30,7 @@ export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; export { default as CanvasGraphicsRenderer } from './graphics/canvas/CanvasGraphicsRenderer'; +export { default as Spritesheet } from './textures/Spritesheet'; export { default as Texture } from './textures/Texture'; export { default as BaseTexture } from './textures/BaseTexture'; export { default as RenderTexture } from './textures/RenderTexture'; diff --git a/src/core/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 ccce680..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); } /** @@ -162,6 +162,9 @@ if (transform) { transform.copy(tempWt); + + // lets not forget to flag the parent transform as dirty... + this._tempDisplayObjectParent.transform._worldID = -1; } else { @@ -169,6 +172,7 @@ } displayObject.parent = this._tempDisplayObjectParent; + displayObject.updateTransform(); displayObject.parent = cacheParent; // displayObject.hitArea = //TODO add a temp hit area @@ -279,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/canvas/utils/CanvasRenderTarget.js b/src/core/renderers/canvas/utils/CanvasRenderTarget.js index 3de0aef..b409891 100644 --- a/src/core/renderers/canvas/utils/CanvasRenderTarget.js +++ b/src/core/renderers/canvas/utils/CanvasRenderTarget.js @@ -1,5 +1,4 @@ import settings from '../../../settings'; -const { RESOLUTION } = settings; /** * Creates a Canvas element of the given size. @@ -30,7 +29,7 @@ */ this.context = this.canvas.getContext('2d'); - this.resolution = resolution || RESOLUTION; + this.resolution = resolution || settings.RESOLUTION; this.resize(width, height); } 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 ce0e1c4..788ed6c 100644 --- a/src/core/renderers/webgl/WebGLRenderer.js +++ b/src/core/renderers/webgl/WebGLRenderer.js @@ -30,8 +30,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 @@ -49,10 +49,19 @@ * enable this if you need to call toDataUrl on the webgl context. * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when * rendering, stopping pixel interpolation. + * @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; + + if (this.legacy) + { + glCore.VertexArrayObject.FORCE_NATIVE = true; + } /** * The type of this renderer as a standardised const @@ -229,7 +238,7 @@ this.emit('context', gl); // setup the width/height properties and gl viewport - this.resize(this.width, this.height); + this.resize(this.screen.width, this.screen.height); } /** @@ -323,16 +332,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) { @@ -376,6 +385,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 @@ -440,9 +469,10 @@ * Changes the current shader to the one given in parameter * * @param {PIXI.Shader} shader - the new shader + * @param {boolean} [autoProject=true] - Whether automatically set the projection matrix * @return {PIXI.WebGLRenderer} Returns itself. */ - bindShader(shader) + bindShader(shader, autoProject) { // TODO cache if (this._activeShader !== shader) @@ -450,8 +480,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 bbab3b3..944f41d 100644 --- a/src/core/renderers/webgl/filters/Filter.js +++ b/src/core/renderers/webgl/filters/Filter.js @@ -1,6 +1,7 @@ import extractUniformsFromSrc from './extractUniformsFromSrc'; import { uid } from '../../../utils'; import { BLEND_MODES } from '../../../const'; +import settings from '../../../settings'; const SOURCE_KEY_MAP = {}; @@ -10,7 +11,7 @@ * @memberof PIXI * @extends PIXI.Shader */ -class Filter +export default class Filter { /** * @param {string} [vertexSrc] - The source of the vertex shader. @@ -35,8 +36,6 @@ this.blendMode = BLEND_MODES.NORMAL; - // pull out the vertex and shader uniforms if they are not specified.. - // currently this does not extract structs only default types this.uniformData = uniforms || extractUniformsFromSrc(this.vertexSrc, this.fragmentSrc, 'projectionMatrix|uSampler'); /** @@ -80,7 +79,7 @@ * * @member {number} */ - this.resolution = 1; + this.resolution = settings.RESOLUTION; /** * If enabled is true the filter is applied, if false it will not. @@ -97,8 +96,11 @@ * @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 { // --- // // this.uniforms.filterMatrix = filterManager.calculateSpriteMatrix(tempMatrix, window.panda ); @@ -170,5 +172,3 @@ ].join('\n'); } } - -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 dc7072a..6c7422d 100644 --- a/src/core/renderers/webgl/managers/FilterManager.js +++ b/src/core/renderers/webgl/managers/FilterManager.js @@ -154,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 @@ -176,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; @@ -184,7 +184,7 @@ flop = t; } - filters[i].apply(this, flip, lastState.renderTarget, true); + filters[i].apply(this, flip, lastState.renderTarget, false, currentState); this.freePotRenderTarget(flip); this.freePotRenderTarget(flop); @@ -291,9 +291,13 @@ let textureCount = 1; let currentState; - if (shader.uniforms.data.filterArea) + // 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; @@ -306,9 +310,9 @@ // use this to clamp displaced texture coords so they belong to filterArea // see displacementFilter fragment shader for an example - if (shader.uniforms.data.filterClamp) + if (shader.uniforms.filterClamp) { - currentState = this.filterData.stack[this.filterData.index]; + currentState = currentState || this.filterData.stack[this.filterData.index]; const filterClamp = shader.uniforms.filterClamp; @@ -485,7 +489,7 @@ */ destroy() { - this.shaderCache = []; + this.shaderCache = {}; this.emptyPool(); } diff --git a/src/core/renderers/webgl/utils/RenderTarget.js b/src/core/renderers/webgl/utils/RenderTarget.js index afe51ab..6d2467f 100644 --- a/src/core/renderers/webgl/utils/RenderTarget.js +++ b/src/core/renderers/webgl/utils/RenderTarget.js @@ -124,7 +124,7 @@ * @default PIXI.settings.SCALE_MODE * @see PIXI.SCALE_MODES */ - this.scaleMode = scaleMode || settings.SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; /** * Whether this object is the root element or not 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 36ea5bb..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; @@ -346,8 +346,8 @@ /** * Gets the local bounds of the sprite object. * - * @param {Rectangle} rect - The output rectangle. - * @return {Rectangle} The bounds. + * @param {PIXI.Rectangle} rect - The output rectangle. + * @return {PIXI.Rectangle} The bounds. */ getLocalBounds(rect) { @@ -440,7 +440,7 @@ * * @static * @param {number|string|PIXI.BaseTexture|HTMLCanvasElement|HTMLVideoElement} source Source to create texture from - * @return {PIXI.Texture} 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 e77b39a..90b08c0 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++) @@ -103,11 +102,18 @@ { const gl = this.renderer.gl; - // step 1: first check max textures the GPU can handle. - this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); + if (this.renderer.legacy) + { + this.MAX_TEXTURES = 1; + } + else + { + // step 1: first check max textures the GPU can handle. + this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); - // step 2: check the maximum number of if statements the shader can have too.. - this.MAX_TEXTURES = checkMaxIfStatmentsInShader(this.MAX_TEXTURES, gl); + // step 2: check the maximum number of if statements the shader can have too.. + this.MAX_TEXTURES = checkMaxIfStatmentsInShader(this.MAX_TEXTURES, gl); + } const shader = this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); @@ -130,8 +136,12 @@ .addIndex(this.indexBuffer) .addAttribute(this.vertexBuffers[i], shader.attributes.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) .addAttribute(this.vertexBuffers[i], shader.attributes.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) - .addAttribute(this.vertexBuffers[i], shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4) - .addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + .addAttribute(this.vertexBuffers[i], shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); + + if (shader.attributes.aTextureId) + { + this.vaos[i].addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + } /* eslint-enable max-len */ } @@ -378,8 +388,12 @@ .addIndex(this.indexBuffer) .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) - .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4) - .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); + + if (this.shader.attributes.aTextureId) + { + this.vaos[this.vertexCount].addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + } /* eslint-enable max-len */ } diff --git a/src/core/text/Text.js b/src/core/text/Text.js index 013625a..cec8c32 100644 --- a/src/core/text/Text.js +++ b/src/core/text/Text.js @@ -6,6 +6,7 @@ import { TEXT_GRADIENT } from '../const'; import settings from '../settings'; import TextStyle from './TextStyle'; +import trimCanvas from '../utils/trimCanvas'; const defaultDestroyOptions = { texture: true, @@ -57,7 +58,7 @@ /** * The canvas 2d context that everything is drawn with - * @member {HTMLCanvasElement} + * @member {CanvasRenderingContext2D} */ this.context = this.canvas.getContext('2d'); @@ -189,10 +190,12 @@ if (style.dropShadow) { + this.context.shadowBlur = style.dropShadowBlur; + this.context.globalAlpha = style.dropShadowAlpha; + if (style.dropShadowBlur > 0) { this.context.shadowColor = style.dropShadowColor; - this.context.shadowBlur = style.dropShadowBlur; } else { @@ -237,6 +240,10 @@ } } + // reset the shadow blur and alpha that was set by the drop shadow, for the regular text + this.context.shadowBlur = 0; + this.context.globalAlpha = 1; + // set canvas text styles this.context.fillStyle = this._generateFillStyle(style, lines); @@ -326,6 +333,15 @@ */ updateTexture() { + if (this._style.trim) + { + const trimmed = trimCanvas(this.canvas); + + this.canvas.width = trimmed.width; + this.canvas.height = trimmed.height; + this.context.putImageData(trimmed.data, 0, 0); + } + const texture = this._texture; const style = this._style; @@ -475,6 +491,19 @@ } /** + * Gets the local bounds of the text object. + * + * @param {Rectangle} rect - The output rectangle. + * @return {Rectangle} The bounds. + */ + getLocalBounds(rect) + { + this.updateText(true); + + return super.getLocalBounds.call(this, rect); + } + + /** * calculates the bounds of the Text as a rectangle. The bounds calculation takes the worldTransform into account. */ _calculateBounds() @@ -525,6 +554,29 @@ const width = this.canvas.width / this.resolution; const height = this.canvas.height / this.resolution; + // make a copy of the style settings, so we can manipulate them later + const fill = style.fill.slice(); + const fillGradientStops = style.fillGradientStops.slice(); + + // wanting to evenly distribute the fills. So an array of 4 colours should give fills of 0.25, 0.5 and 0.75 + if (!fillGradientStops.length) + { + const lengthPlus1 = fill.length + 1; + + for (let i = 1; i < lengthPlus1; ++i) + { + fillGradientStops.push(i / lengthPlus1); + } + } + + // stop the bleeding of the last gradient on the line above to the top gradient of the this line + // by hard defining the first gradient colour at point 0, and last gradient colour at point 1 + fill.unshift(style.fill[0]); + fillGradientStops.unshift(0); + + fill.push(style.fill[style.fill.length - 1]); + fillGradientStops.push(1); + if (style.fillGradientType === TEXT_GRADIENT.LINEAR_VERTICAL) { // start the gradient at the top center of the canvas, and end at the bottom middle of the canvas @@ -532,15 +584,22 @@ // we need to repeat the gradient so that each individual line of text has the same vertical gradient effect // ['#FF0000', '#00FF00', '#0000FF'] over 2 lines would create stops at 0.125, 0.25, 0.375, 0.625, 0.75, 0.875 - totalIterations = (style.fill.length + 1) * lines.length; + totalIterations = (fill.length + 1) * lines.length; currentIteration = 0; for (let i = 0; i < lines.length; i++) { currentIteration += 1; - for (let j = 0; j < style.fill.length; j++) + for (let j = 0; j < fill.length; j++) { - stop = (currentIteration / totalIterations); - gradient.addColorStop(stop, style.fill[j]); + if (fillGradientStops[j]) + { + stop = (fillGradientStops[j] / lines.length) + (i / lines.length); + } + else + { + stop = currentIteration / totalIterations; + } + gradient.addColorStop(stop, fill[j]); currentIteration++; } } @@ -552,13 +611,20 @@ // can just evenly space out the gradients in this case, as multiple lines makes no difference // to an even left to right gradient - totalIterations = style.fill.length + 1; + totalIterations = fill.length + 1; currentIteration = 1; - for (let i = 0; i < style.fill.length; i++) + for (let i = 0; i < fill.length; i++) { - stop = currentIteration / totalIterations; - gradient.addColorStop(stop, style.fill[i]); + if (fillGradientStops[i]) + { + stop = fillGradientStops[i]; + } + else + { + stop = currentIteration / totalIterations; + } + gradient.addColorStop(stop, fill[i]); currentIteration++; } } @@ -569,7 +635,7 @@ /** * Destroys this text object. * Note* Unlike a Sprite, a Text object will automatically destroy its baseTexture and texture as - * the majorety of the time the texture will not be shared with any other Sprites. + * the majority of the time the texture will not be shared with any other Sprites. * * @param {object|boolean} [options] - Options parameter. A boolean will act as if all options * have been set to that value @@ -680,7 +746,7 @@ set text(text) // eslint-disable-line require-jsdoc { - text = String(text || ' '); + text = String(text === '' || text === null || text === undefined ? ' ' : text); if (this._text === text) { diff --git a/src/core/text/TextStyle.js b/src/core/text/TextStyle.js index 3b25528..890950b 100644 --- a/src/core/text/TextStyle.js +++ b/src/core/text/TextStyle.js @@ -8,12 +8,14 @@ align: 'left', breakWords: false, dropShadow: false, + dropShadowAlpha: 1, dropShadowAngle: Math.PI / 6, dropShadowBlur: 0, dropShadowColor: '#000000', dropShadowDistance: 5, fill: 'black', fillGradientType: TEXT_GRADIENT.LINEAR_VERTICAL, + fillGradientStops: [], fontFamily: 'Arial', fontSize: 26, fontStyle: 'normal', @@ -27,6 +29,7 @@ stroke: 'black', strokeThickness: 0, textBaseline: 'alphabetic', + trim: false, wordWrap: false, wordWrapWidth: 100, }; @@ -47,6 +50,7 @@ * @param {boolean} [style.breakWords=false] - Indicates if lines can be wrapped within words, it * needs wordWrap to be set to true * @param {boolean} [style.dropShadow=false] - Set a drop shadow for the text + * @param {number} [style.dropShadowAlpha=1] - Set alpha for the drop shadow * @param {number} [style.dropShadowAngle=Math.PI/6] - Set a angle of the drop shadow * @param {number} [style.dropShadowBlur=0] - Set a shadow blur radius * @param {string} [style.dropShadowColor='#000000'] - A fill style to be used on the dropshadow e.g 'red', '#00FF00' @@ -55,8 +59,10 @@ * fillstyle that will be used on the text e.g 'red', '#00FF00'. Can be an array to create a gradient * eg ['#000000','#FFFFFF'] * {@link https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/fillStyle|MDN} - * @param {number} [style.fillGradientType=PIXI.TEXT_GRADIENT.LINEAR_VERTICAL] - If fills styles are - * supplied, this can change the type/direction of the gradient. See {@link PIXI.TEXT_GRADIENT} for possible values + * @param {number} [style.fillGradientType=PIXI.TEXT_GRADIENT.LINEAR_VERTICAL] - If fill is an array of colours + * to create a gradient, this can change the type/direction of the gradient. See {@link PIXI.TEXT_GRADIENT} + * @param {number[]} [style.fillGradientStops] - If fill is an array of colours to create a gradient, this array can set + * the stop points (numbers between 0 and 1) for the color, overriding the default behaviour of evenly spacing them. * @param {string|string[]} [style.fontFamily='Arial'] - The font family * @param {number|string} [style.fontSize=26] - The font size (as a number it converts to px, but as a string, * equivalents are '26px','20pt','160%' or '1.6em') @@ -76,6 +82,7 @@ * e.g 'blue', '#FCFF00' * @param {number} [style.strokeThickness=0] - A number that represents the thickness of the stroke. * Default is 0 (no stroke) + * @param {boolean} [style.trim=false] - Trim transparent borders * @param {string} [style.textBaseline='alphabetic'] - The baseline of the text that is rendered. * @param {boolean} [style.wordWrap=false] - Indicates if word wrap should be used * @param {number} [style.wordWrapWidth=100] - The width at which text will wrap, it needs wordWrap to be set to true @@ -152,6 +159,19 @@ } } + get dropShadowAlpha() + { + return this._dropShadowAlpha; + } + set dropShadowAlpha(dropShadowAlpha) + { + if (this._dropShadowAlpha !== dropShadowAlpha) + { + this._dropShadowAlpha = dropShadowAlpha; + this.styleID++; + } + } + get dropShadowAngle() { return this._dropShadowAngle; @@ -232,6 +252,19 @@ } } + get fillGradientStops() + { + return this._fillGradientStops; + } + set fillGradientStops(fillGradientStops) + { + if (!areArraysEqual(this._fillGradientStops,fillGradientStops)) + { + this._fillGradientStops = fillGradientStops; + this.styleID++; + } + } + get fontFamily() { return this._fontFamily; @@ -402,6 +435,19 @@ } } + get trim() + { + return this._trim; + } + set trim(trim) + { + if (this._trim !== trim) + { + this._trim = trim; + this.styleID++; + } + } + get wordWrap() { return this._wordWrap; @@ -475,3 +521,34 @@ return color; } } + +/** + * Utility function to convert hexadecimal colors to strings, and simply return the color if it's a string. + * This version can also convert array of colors + * + * @param {Array} array1 First array to compare + * @param {Array} array2 Second array to compare + * @return {boolean} Do the arrays contain the same values in the same order + */ +function areArraysEqual(array1, array2) +{ + if (!Array.isArray(array1) || !Array.isArray(array2)) + { + return false; + } + + if (array1.length !== array2.length) + { + return false; + } + + for (let i = 0; i < array1.length; ++i) + { + if (array1[i] !== array2[i]) + { + return false; + } + } + + return true; +} diff --git a/src/core/textures/BaseRenderTexture.js b/src/core/textures/BaseRenderTexture.js index 5d50146..aa727f0 100644 --- a/src/core/textures/BaseRenderTexture.js +++ b/src/core/textures/BaseRenderTexture.js @@ -1,8 +1,6 @@ import BaseTexture from './BaseTexture'; import settings from '../settings'; -const { RESOLUTION, SCALE_MODE } = settings; - /** * A BaseRenderTexture is a special texture that allows any Pixi display object to be rendered to it. * @@ -54,7 +52,7 @@ { super(null, scaleMode); - this.resolution = resolution || RESOLUTION; + this.resolution = resolution || settings.RESOLUTION; this.width = width; this.height = height; @@ -62,7 +60,7 @@ this.realWidth = this.width * this.resolution; this.realHeight = this.height * this.resolution; - this.scaleMode = scaleMode || SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; this.hasLoaded = true; /** diff --git a/src/core/textures/BaseTexture.js b/src/core/textures/BaseTexture.js index 676dca3..aeccf39 100644 --- a/src/core/textures/BaseTexture.js +++ b/src/core/textures/BaseTexture.js @@ -77,7 +77,7 @@ * @default PIXI.settings.SCALE_MODE * @see PIXI.SCALE_MODES */ - this.scaleMode = scaleMode || settings.SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; /** * Set to true once the base texture has successfully loaded. @@ -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/RenderTexture.js b/src/core/textures/RenderTexture.js index 4c5034f..b3c6297 100644 --- a/src/core/textures/RenderTexture.js +++ b/src/core/textures/RenderTexture.js @@ -54,8 +54,8 @@ /* eslint-disable prefer-rest-params, no-console */ const width = arguments[1]; const height = arguments[2]; - const scaleMode = arguments[3] || 0; - const resolution = arguments[4] || 1; + const scaleMode = arguments[3]; + const resolution = arguments[4]; // we have an old render texture.. console.warn(`Please use RenderTexture.create(${width}, ${height}) instead of the ctor directly.`); 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 1271fef..71ade15 100644 --- a/package.json +++ b/package.json @@ -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/scripts/release.js b/scripts/release.js index c4c8e36..156620c 100644 --- a/scripts/release.js +++ b/scripts/release.js @@ -12,7 +12,8 @@ 'dist/**/*', 'lib/**/*', 'src/**/*', - 'scripts/**/*', + 'scripts/*', + 'scripts/renders/*', 'test/**/*', '*.json', '*.md', diff --git a/scripts/renders/client.js b/scripts/renders/client.js index 16c9565..6faee34 100755 --- a/scripts/renders/client.js +++ b/scripts/renders/client.js @@ -1,6 +1,6 @@ 'use strict'; -require('../../bin/pixi'); +require('../../dist/pixi'); PIXI.utils.skipHello(); const fs = require('fs'); 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 bc730db..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. @@ -33,8 +33,11 @@ * need to call toDataUrl on the webgl context * @param {number} [options.resolution=1] - The resolution / device pixel ratio of the renderer, retina would be 2 * @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 @@ -43,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. */ @@ -73,7 +99,7 @@ */ stop() { - this.ticker.stop(); + this._ticker.stop(); } /** @@ -81,7 +107,7 @@ */ start() { - this.ticker.start(); + this._ticker.start(); } /** @@ -95,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 index 81e076b..78b17e6 100644 --- a/src/core/Shader.js +++ b/src/core/Shader.js @@ -1,9 +1,7 @@ import { GLShader } from 'pixi-gl-core'; import settings from './settings'; -const { PRECISION } = settings; - -function checkPrecision(src) +function checkPrecision(src, def) { if (src instanceof Array) { @@ -11,14 +9,14 @@ { const copy = src.slice(0); - copy.unshift(`precision ${PRECISION} float;`); + copy.unshift(`precision ${def} float;`); return copy; } } else if (src.substring(0, 9) !== 'precision') { - return `precision ${PRECISION} float;\n${src}`; + return `precision ${def} float;\n${src}`; } return src; @@ -42,6 +40,7 @@ */ constructor(gl, vertexSrc, fragmentSrc) { - super(gl, checkPrecision(vertexSrc), checkPrecision(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 3425b41..f9235e4 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -181,7 +181,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 b3792ef..17fde8d 100644 --- a/src/core/display/Container.js +++ b/src/core/display/Container.js @@ -73,13 +73,14 @@ } child.parent = this; - - // ensure a transform will be recalculated.. - this.transform._parentID = -1; - this._boundsID++; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.push(child); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(this.children.length - 1); child.emit('added', this); @@ -108,9 +109,14 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.splice(index, 0, child); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(index); child.emit('added', this); @@ -174,6 +180,7 @@ removeItems(this.children, currentIndex, 1); // remove from old position this.children.splice(index, 0, child); // add at new position + this.onChildrenChange(index); } @@ -220,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! @@ -244,9 +252,14 @@ { const child = this.getChildAt(index); + // ensure child transform will be recalculated.. child.parent = null; + child.transform._parentID = -1; removeItems(this.children, index, 1); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(index); child.emit('removed', this); @@ -275,8 +288,14 @@ for (let i = 0; i < removed.length; ++i) { removed[i].parent = null; + if (removed[i].transform) + { + removed[i].transform._parentID = -1; + } } + this._boundsID++; + this.onChildrenChange(beginIndex); for (let i = 0; i < removed.length; ++i) diff --git a/src/core/display/DisplayObject.js b/src/core/display/DisplayObject.js index 3affe36..4e6c77d 100644 --- a/src/core/display/DisplayObject.js +++ b/src/core/display/DisplayObject.js @@ -109,7 +109,7 @@ /** * The original, cached mask of the object * - * @member {PIXI.Rectangle} + * @member {PIXI.Graphics|PIXI.Sprite} * @private */ this._mask = null; diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index 95f2af4..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 ); @@ -1065,10 +1061,15 @@ canvasRenderer = new CanvasRenderer(); } - tempMatrix.tx = -bounds.x; - tempMatrix.ty = -bounds.y; + this.transform.updateLocalTransform(); + this.transform.localTransform.copy(tempMatrix); - canvasRenderer.render(this, canvasBuffer, false, tempMatrix); + tempMatrix.invert(); + + tempMatrix.tx -= bounds.x; + tempMatrix.ty -= bounds.y; + + canvasRenderer.render(this, canvasBuffer, true, tempMatrix); const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode); 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 e67680f..867dde2 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/buildCircle.js b/src/core/graphics/webgl/utils/buildCircle.js index 0400d13..abf5d02 100644 --- a/src/core/graphics/webgl/utils/buildCircle.js +++ b/src/core/graphics/webgl/utils/buildCircle.js @@ -33,6 +33,11 @@ height = circleData.height; } + if (width === 0 || height === 0) + { + return; + } + const totalSegs = Math.floor(30 * Math.sqrt(circleData.radius)) || Math.floor(15 * Math.sqrt(circleData.width + circleData.height)); diff --git a/src/core/graphics/webgl/utils/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 90155f9..f03fe15 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -30,6 +30,7 @@ export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; export { default as CanvasGraphicsRenderer } from './graphics/canvas/CanvasGraphicsRenderer'; +export { default as Spritesheet } from './textures/Spritesheet'; export { default as Texture } from './textures/Texture'; export { default as BaseTexture } from './textures/BaseTexture'; export { default as RenderTexture } from './textures/RenderTexture'; diff --git a/src/core/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 ccce680..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); } /** @@ -162,6 +162,9 @@ if (transform) { transform.copy(tempWt); + + // lets not forget to flag the parent transform as dirty... + this._tempDisplayObjectParent.transform._worldID = -1; } else { @@ -169,6 +172,7 @@ } displayObject.parent = this._tempDisplayObjectParent; + displayObject.updateTransform(); displayObject.parent = cacheParent; // displayObject.hitArea = //TODO add a temp hit area @@ -279,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/canvas/utils/CanvasRenderTarget.js b/src/core/renderers/canvas/utils/CanvasRenderTarget.js index 3de0aef..b409891 100644 --- a/src/core/renderers/canvas/utils/CanvasRenderTarget.js +++ b/src/core/renderers/canvas/utils/CanvasRenderTarget.js @@ -1,5 +1,4 @@ import settings from '../../../settings'; -const { RESOLUTION } = settings; /** * Creates a Canvas element of the given size. @@ -30,7 +29,7 @@ */ this.context = this.canvas.getContext('2d'); - this.resolution = resolution || RESOLUTION; + this.resolution = resolution || settings.RESOLUTION; this.resize(width, height); } 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 ce0e1c4..788ed6c 100644 --- a/src/core/renderers/webgl/WebGLRenderer.js +++ b/src/core/renderers/webgl/WebGLRenderer.js @@ -30,8 +30,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 @@ -49,10 +49,19 @@ * enable this if you need to call toDataUrl on the webgl context. * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when * rendering, stopping pixel interpolation. + * @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; + + if (this.legacy) + { + glCore.VertexArrayObject.FORCE_NATIVE = true; + } /** * The type of this renderer as a standardised const @@ -229,7 +238,7 @@ this.emit('context', gl); // setup the width/height properties and gl viewport - this.resize(this.width, this.height); + this.resize(this.screen.width, this.screen.height); } /** @@ -323,16 +332,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) { @@ -376,6 +385,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 @@ -440,9 +469,10 @@ * Changes the current shader to the one given in parameter * * @param {PIXI.Shader} shader - the new shader + * @param {boolean} [autoProject=true] - Whether automatically set the projection matrix * @return {PIXI.WebGLRenderer} Returns itself. */ - bindShader(shader) + bindShader(shader, autoProject) { // TODO cache if (this._activeShader !== shader) @@ -450,8 +480,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 bbab3b3..944f41d 100644 --- a/src/core/renderers/webgl/filters/Filter.js +++ b/src/core/renderers/webgl/filters/Filter.js @@ -1,6 +1,7 @@ import extractUniformsFromSrc from './extractUniformsFromSrc'; import { uid } from '../../../utils'; import { BLEND_MODES } from '../../../const'; +import settings from '../../../settings'; const SOURCE_KEY_MAP = {}; @@ -10,7 +11,7 @@ * @memberof PIXI * @extends PIXI.Shader */ -class Filter +export default class Filter { /** * @param {string} [vertexSrc] - The source of the vertex shader. @@ -35,8 +36,6 @@ this.blendMode = BLEND_MODES.NORMAL; - // pull out the vertex and shader uniforms if they are not specified.. - // currently this does not extract structs only default types this.uniformData = uniforms || extractUniformsFromSrc(this.vertexSrc, this.fragmentSrc, 'projectionMatrix|uSampler'); /** @@ -80,7 +79,7 @@ * * @member {number} */ - this.resolution = 1; + this.resolution = settings.RESOLUTION; /** * If enabled is true the filter is applied, if false it will not. @@ -97,8 +96,11 @@ * @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 { // --- // // this.uniforms.filterMatrix = filterManager.calculateSpriteMatrix(tempMatrix, window.panda ); @@ -170,5 +172,3 @@ ].join('\n'); } } - -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 dc7072a..6c7422d 100644 --- a/src/core/renderers/webgl/managers/FilterManager.js +++ b/src/core/renderers/webgl/managers/FilterManager.js @@ -154,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 @@ -176,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; @@ -184,7 +184,7 @@ flop = t; } - filters[i].apply(this, flip, lastState.renderTarget, true); + filters[i].apply(this, flip, lastState.renderTarget, false, currentState); this.freePotRenderTarget(flip); this.freePotRenderTarget(flop); @@ -291,9 +291,13 @@ let textureCount = 1; let currentState; - if (shader.uniforms.data.filterArea) + // 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; @@ -306,9 +310,9 @@ // use this to clamp displaced texture coords so they belong to filterArea // see displacementFilter fragment shader for an example - if (shader.uniforms.data.filterClamp) + if (shader.uniforms.filterClamp) { - currentState = this.filterData.stack[this.filterData.index]; + currentState = currentState || this.filterData.stack[this.filterData.index]; const filterClamp = shader.uniforms.filterClamp; @@ -485,7 +489,7 @@ */ destroy() { - this.shaderCache = []; + this.shaderCache = {}; this.emptyPool(); } diff --git a/src/core/renderers/webgl/utils/RenderTarget.js b/src/core/renderers/webgl/utils/RenderTarget.js index afe51ab..6d2467f 100644 --- a/src/core/renderers/webgl/utils/RenderTarget.js +++ b/src/core/renderers/webgl/utils/RenderTarget.js @@ -124,7 +124,7 @@ * @default PIXI.settings.SCALE_MODE * @see PIXI.SCALE_MODES */ - this.scaleMode = scaleMode || settings.SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; /** * Whether this object is the root element or not 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 36ea5bb..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; @@ -346,8 +346,8 @@ /** * Gets the local bounds of the sprite object. * - * @param {Rectangle} rect - The output rectangle. - * @return {Rectangle} The bounds. + * @param {PIXI.Rectangle} rect - The output rectangle. + * @return {PIXI.Rectangle} The bounds. */ getLocalBounds(rect) { @@ -440,7 +440,7 @@ * * @static * @param {number|string|PIXI.BaseTexture|HTMLCanvasElement|HTMLVideoElement} source Source to create texture from - * @return {PIXI.Texture} 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 e77b39a..90b08c0 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++) @@ -103,11 +102,18 @@ { const gl = this.renderer.gl; - // step 1: first check max textures the GPU can handle. - this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); + if (this.renderer.legacy) + { + this.MAX_TEXTURES = 1; + } + else + { + // step 1: first check max textures the GPU can handle. + this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); - // step 2: check the maximum number of if statements the shader can have too.. - this.MAX_TEXTURES = checkMaxIfStatmentsInShader(this.MAX_TEXTURES, gl); + // step 2: check the maximum number of if statements the shader can have too.. + this.MAX_TEXTURES = checkMaxIfStatmentsInShader(this.MAX_TEXTURES, gl); + } const shader = this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); @@ -130,8 +136,12 @@ .addIndex(this.indexBuffer) .addAttribute(this.vertexBuffers[i], shader.attributes.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) .addAttribute(this.vertexBuffers[i], shader.attributes.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) - .addAttribute(this.vertexBuffers[i], shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4) - .addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + .addAttribute(this.vertexBuffers[i], shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); + + if (shader.attributes.aTextureId) + { + this.vaos[i].addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + } /* eslint-enable max-len */ } @@ -378,8 +388,12 @@ .addIndex(this.indexBuffer) .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) - .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4) - .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); + + if (this.shader.attributes.aTextureId) + { + this.vaos[this.vertexCount].addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + } /* eslint-enable max-len */ } diff --git a/src/core/text/Text.js b/src/core/text/Text.js index 013625a..cec8c32 100644 --- a/src/core/text/Text.js +++ b/src/core/text/Text.js @@ -6,6 +6,7 @@ import { TEXT_GRADIENT } from '../const'; import settings from '../settings'; import TextStyle from './TextStyle'; +import trimCanvas from '../utils/trimCanvas'; const defaultDestroyOptions = { texture: true, @@ -57,7 +58,7 @@ /** * The canvas 2d context that everything is drawn with - * @member {HTMLCanvasElement} + * @member {CanvasRenderingContext2D} */ this.context = this.canvas.getContext('2d'); @@ -189,10 +190,12 @@ if (style.dropShadow) { + this.context.shadowBlur = style.dropShadowBlur; + this.context.globalAlpha = style.dropShadowAlpha; + if (style.dropShadowBlur > 0) { this.context.shadowColor = style.dropShadowColor; - this.context.shadowBlur = style.dropShadowBlur; } else { @@ -237,6 +240,10 @@ } } + // reset the shadow blur and alpha that was set by the drop shadow, for the regular text + this.context.shadowBlur = 0; + this.context.globalAlpha = 1; + // set canvas text styles this.context.fillStyle = this._generateFillStyle(style, lines); @@ -326,6 +333,15 @@ */ updateTexture() { + if (this._style.trim) + { + const trimmed = trimCanvas(this.canvas); + + this.canvas.width = trimmed.width; + this.canvas.height = trimmed.height; + this.context.putImageData(trimmed.data, 0, 0); + } + const texture = this._texture; const style = this._style; @@ -475,6 +491,19 @@ } /** + * Gets the local bounds of the text object. + * + * @param {Rectangle} rect - The output rectangle. + * @return {Rectangle} The bounds. + */ + getLocalBounds(rect) + { + this.updateText(true); + + return super.getLocalBounds.call(this, rect); + } + + /** * calculates the bounds of the Text as a rectangle. The bounds calculation takes the worldTransform into account. */ _calculateBounds() @@ -525,6 +554,29 @@ const width = this.canvas.width / this.resolution; const height = this.canvas.height / this.resolution; + // make a copy of the style settings, so we can manipulate them later + const fill = style.fill.slice(); + const fillGradientStops = style.fillGradientStops.slice(); + + // wanting to evenly distribute the fills. So an array of 4 colours should give fills of 0.25, 0.5 and 0.75 + if (!fillGradientStops.length) + { + const lengthPlus1 = fill.length + 1; + + for (let i = 1; i < lengthPlus1; ++i) + { + fillGradientStops.push(i / lengthPlus1); + } + } + + // stop the bleeding of the last gradient on the line above to the top gradient of the this line + // by hard defining the first gradient colour at point 0, and last gradient colour at point 1 + fill.unshift(style.fill[0]); + fillGradientStops.unshift(0); + + fill.push(style.fill[style.fill.length - 1]); + fillGradientStops.push(1); + if (style.fillGradientType === TEXT_GRADIENT.LINEAR_VERTICAL) { // start the gradient at the top center of the canvas, and end at the bottom middle of the canvas @@ -532,15 +584,22 @@ // we need to repeat the gradient so that each individual line of text has the same vertical gradient effect // ['#FF0000', '#00FF00', '#0000FF'] over 2 lines would create stops at 0.125, 0.25, 0.375, 0.625, 0.75, 0.875 - totalIterations = (style.fill.length + 1) * lines.length; + totalIterations = (fill.length + 1) * lines.length; currentIteration = 0; for (let i = 0; i < lines.length; i++) { currentIteration += 1; - for (let j = 0; j < style.fill.length; j++) + for (let j = 0; j < fill.length; j++) { - stop = (currentIteration / totalIterations); - gradient.addColorStop(stop, style.fill[j]); + if (fillGradientStops[j]) + { + stop = (fillGradientStops[j] / lines.length) + (i / lines.length); + } + else + { + stop = currentIteration / totalIterations; + } + gradient.addColorStop(stop, fill[j]); currentIteration++; } } @@ -552,13 +611,20 @@ // can just evenly space out the gradients in this case, as multiple lines makes no difference // to an even left to right gradient - totalIterations = style.fill.length + 1; + totalIterations = fill.length + 1; currentIteration = 1; - for (let i = 0; i < style.fill.length; i++) + for (let i = 0; i < fill.length; i++) { - stop = currentIteration / totalIterations; - gradient.addColorStop(stop, style.fill[i]); + if (fillGradientStops[i]) + { + stop = fillGradientStops[i]; + } + else + { + stop = currentIteration / totalIterations; + } + gradient.addColorStop(stop, fill[i]); currentIteration++; } } @@ -569,7 +635,7 @@ /** * Destroys this text object. * Note* Unlike a Sprite, a Text object will automatically destroy its baseTexture and texture as - * the majorety of the time the texture will not be shared with any other Sprites. + * the majority of the time the texture will not be shared with any other Sprites. * * @param {object|boolean} [options] - Options parameter. A boolean will act as if all options * have been set to that value @@ -680,7 +746,7 @@ set text(text) // eslint-disable-line require-jsdoc { - text = String(text || ' '); + text = String(text === '' || text === null || text === undefined ? ' ' : text); if (this._text === text) { diff --git a/src/core/text/TextStyle.js b/src/core/text/TextStyle.js index 3b25528..890950b 100644 --- a/src/core/text/TextStyle.js +++ b/src/core/text/TextStyle.js @@ -8,12 +8,14 @@ align: 'left', breakWords: false, dropShadow: false, + dropShadowAlpha: 1, dropShadowAngle: Math.PI / 6, dropShadowBlur: 0, dropShadowColor: '#000000', dropShadowDistance: 5, fill: 'black', fillGradientType: TEXT_GRADIENT.LINEAR_VERTICAL, + fillGradientStops: [], fontFamily: 'Arial', fontSize: 26, fontStyle: 'normal', @@ -27,6 +29,7 @@ stroke: 'black', strokeThickness: 0, textBaseline: 'alphabetic', + trim: false, wordWrap: false, wordWrapWidth: 100, }; @@ -47,6 +50,7 @@ * @param {boolean} [style.breakWords=false] - Indicates if lines can be wrapped within words, it * needs wordWrap to be set to true * @param {boolean} [style.dropShadow=false] - Set a drop shadow for the text + * @param {number} [style.dropShadowAlpha=1] - Set alpha for the drop shadow * @param {number} [style.dropShadowAngle=Math.PI/6] - Set a angle of the drop shadow * @param {number} [style.dropShadowBlur=0] - Set a shadow blur radius * @param {string} [style.dropShadowColor='#000000'] - A fill style to be used on the dropshadow e.g 'red', '#00FF00' @@ -55,8 +59,10 @@ * fillstyle that will be used on the text e.g 'red', '#00FF00'. Can be an array to create a gradient * eg ['#000000','#FFFFFF'] * {@link https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/fillStyle|MDN} - * @param {number} [style.fillGradientType=PIXI.TEXT_GRADIENT.LINEAR_VERTICAL] - If fills styles are - * supplied, this can change the type/direction of the gradient. See {@link PIXI.TEXT_GRADIENT} for possible values + * @param {number} [style.fillGradientType=PIXI.TEXT_GRADIENT.LINEAR_VERTICAL] - If fill is an array of colours + * to create a gradient, this can change the type/direction of the gradient. See {@link PIXI.TEXT_GRADIENT} + * @param {number[]} [style.fillGradientStops] - If fill is an array of colours to create a gradient, this array can set + * the stop points (numbers between 0 and 1) for the color, overriding the default behaviour of evenly spacing them. * @param {string|string[]} [style.fontFamily='Arial'] - The font family * @param {number|string} [style.fontSize=26] - The font size (as a number it converts to px, but as a string, * equivalents are '26px','20pt','160%' or '1.6em') @@ -76,6 +82,7 @@ * e.g 'blue', '#FCFF00' * @param {number} [style.strokeThickness=0] - A number that represents the thickness of the stroke. * Default is 0 (no stroke) + * @param {boolean} [style.trim=false] - Trim transparent borders * @param {string} [style.textBaseline='alphabetic'] - The baseline of the text that is rendered. * @param {boolean} [style.wordWrap=false] - Indicates if word wrap should be used * @param {number} [style.wordWrapWidth=100] - The width at which text will wrap, it needs wordWrap to be set to true @@ -152,6 +159,19 @@ } } + get dropShadowAlpha() + { + return this._dropShadowAlpha; + } + set dropShadowAlpha(dropShadowAlpha) + { + if (this._dropShadowAlpha !== dropShadowAlpha) + { + this._dropShadowAlpha = dropShadowAlpha; + this.styleID++; + } + } + get dropShadowAngle() { return this._dropShadowAngle; @@ -232,6 +252,19 @@ } } + get fillGradientStops() + { + return this._fillGradientStops; + } + set fillGradientStops(fillGradientStops) + { + if (!areArraysEqual(this._fillGradientStops,fillGradientStops)) + { + this._fillGradientStops = fillGradientStops; + this.styleID++; + } + } + get fontFamily() { return this._fontFamily; @@ -402,6 +435,19 @@ } } + get trim() + { + return this._trim; + } + set trim(trim) + { + if (this._trim !== trim) + { + this._trim = trim; + this.styleID++; + } + } + get wordWrap() { return this._wordWrap; @@ -475,3 +521,34 @@ return color; } } + +/** + * Utility function to convert hexadecimal colors to strings, and simply return the color if it's a string. + * This version can also convert array of colors + * + * @param {Array} array1 First array to compare + * @param {Array} array2 Second array to compare + * @return {boolean} Do the arrays contain the same values in the same order + */ +function areArraysEqual(array1, array2) +{ + if (!Array.isArray(array1) || !Array.isArray(array2)) + { + return false; + } + + if (array1.length !== array2.length) + { + return false; + } + + for (let i = 0; i < array1.length; ++i) + { + if (array1[i] !== array2[i]) + { + return false; + } + } + + return true; +} diff --git a/src/core/textures/BaseRenderTexture.js b/src/core/textures/BaseRenderTexture.js index 5d50146..aa727f0 100644 --- a/src/core/textures/BaseRenderTexture.js +++ b/src/core/textures/BaseRenderTexture.js @@ -1,8 +1,6 @@ import BaseTexture from './BaseTexture'; import settings from '../settings'; -const { RESOLUTION, SCALE_MODE } = settings; - /** * A BaseRenderTexture is a special texture that allows any Pixi display object to be rendered to it. * @@ -54,7 +52,7 @@ { super(null, scaleMode); - this.resolution = resolution || RESOLUTION; + this.resolution = resolution || settings.RESOLUTION; this.width = width; this.height = height; @@ -62,7 +60,7 @@ this.realWidth = this.width * this.resolution; this.realHeight = this.height * this.resolution; - this.scaleMode = scaleMode || SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; this.hasLoaded = true; /** diff --git a/src/core/textures/BaseTexture.js b/src/core/textures/BaseTexture.js index 676dca3..aeccf39 100644 --- a/src/core/textures/BaseTexture.js +++ b/src/core/textures/BaseTexture.js @@ -77,7 +77,7 @@ * @default PIXI.settings.SCALE_MODE * @see PIXI.SCALE_MODES */ - this.scaleMode = scaleMode || settings.SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; /** * Set to true once the base texture has successfully loaded. @@ -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/RenderTexture.js b/src/core/textures/RenderTexture.js index 4c5034f..b3c6297 100644 --- a/src/core/textures/RenderTexture.js +++ b/src/core/textures/RenderTexture.js @@ -54,8 +54,8 @@ /* eslint-disable prefer-rest-params, no-console */ const width = arguments[1]; const height = arguments[2]; - const scaleMode = arguments[3] || 0; - const resolution = arguments[4] || 1; + const scaleMode = arguments[3]; + const resolution = arguments[4]; // we have an old render texture.. console.warn(`Please use RenderTexture.create(${width}, ${height}) instead of the ctor directly.`); 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 ca31456..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 @@ -459,7 +497,9 @@ if (frame.x + frame.width > this.baseTexture.width || frame.y + frame.height > this.baseTexture.height) { - throw new Error(`Texture Error: frame does not fit inside the base Texture dimensions ${this}`); + throw new Error('Texture Error: frame does not fit inside the base Texture dimensions: ' + + `X: ${frame.x} + ${frame.width} > ${this.baseTexture.width} ` + + `Y: ${frame.y} + ${frame.height} > ${this.baseTexture.height}`); } // this.valid = frame && frame.width && frame.height && this.baseTexture.source && this.baseTexture.hasLoaded; @@ -520,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. @@ -528,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 1271fef..71ade15 100644 --- a/package.json +++ b/package.json @@ -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/scripts/release.js b/scripts/release.js index c4c8e36..156620c 100644 --- a/scripts/release.js +++ b/scripts/release.js @@ -12,7 +12,8 @@ 'dist/**/*', 'lib/**/*', 'src/**/*', - 'scripts/**/*', + 'scripts/*', + 'scripts/renders/*', 'test/**/*', '*.json', '*.md', diff --git a/scripts/renders/client.js b/scripts/renders/client.js index 16c9565..6faee34 100755 --- a/scripts/renders/client.js +++ b/scripts/renders/client.js @@ -1,6 +1,6 @@ 'use strict'; -require('../../bin/pixi'); +require('../../dist/pixi'); PIXI.utils.skipHello(); const fs = require('fs'); 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 bc730db..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. @@ -33,8 +33,11 @@ * need to call toDataUrl on the webgl context * @param {number} [options.resolution=1] - The resolution / device pixel ratio of the renderer, retina would be 2 * @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 @@ -43,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. */ @@ -73,7 +99,7 @@ */ stop() { - this.ticker.stop(); + this._ticker.stop(); } /** @@ -81,7 +107,7 @@ */ start() { - this.ticker.start(); + this._ticker.start(); } /** @@ -95,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 index 81e076b..78b17e6 100644 --- a/src/core/Shader.js +++ b/src/core/Shader.js @@ -1,9 +1,7 @@ import { GLShader } from 'pixi-gl-core'; import settings from './settings'; -const { PRECISION } = settings; - -function checkPrecision(src) +function checkPrecision(src, def) { if (src instanceof Array) { @@ -11,14 +9,14 @@ { const copy = src.slice(0); - copy.unshift(`precision ${PRECISION} float;`); + copy.unshift(`precision ${def} float;`); return copy; } } else if (src.substring(0, 9) !== 'precision') { - return `precision ${PRECISION} float;\n${src}`; + return `precision ${def} float;\n${src}`; } return src; @@ -42,6 +40,7 @@ */ constructor(gl, vertexSrc, fragmentSrc) { - super(gl, checkPrecision(vertexSrc), checkPrecision(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 3425b41..f9235e4 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -181,7 +181,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 b3792ef..17fde8d 100644 --- a/src/core/display/Container.js +++ b/src/core/display/Container.js @@ -73,13 +73,14 @@ } child.parent = this; - - // ensure a transform will be recalculated.. - this.transform._parentID = -1; - this._boundsID++; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.push(child); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(this.children.length - 1); child.emit('added', this); @@ -108,9 +109,14 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.splice(index, 0, child); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(index); child.emit('added', this); @@ -174,6 +180,7 @@ removeItems(this.children, currentIndex, 1); // remove from old position this.children.splice(index, 0, child); // add at new position + this.onChildrenChange(index); } @@ -220,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! @@ -244,9 +252,14 @@ { const child = this.getChildAt(index); + // ensure child transform will be recalculated.. child.parent = null; + child.transform._parentID = -1; removeItems(this.children, index, 1); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(index); child.emit('removed', this); @@ -275,8 +288,14 @@ for (let i = 0; i < removed.length; ++i) { removed[i].parent = null; + if (removed[i].transform) + { + removed[i].transform._parentID = -1; + } } + this._boundsID++; + this.onChildrenChange(beginIndex); for (let i = 0; i < removed.length; ++i) diff --git a/src/core/display/DisplayObject.js b/src/core/display/DisplayObject.js index 3affe36..4e6c77d 100644 --- a/src/core/display/DisplayObject.js +++ b/src/core/display/DisplayObject.js @@ -109,7 +109,7 @@ /** * The original, cached mask of the object * - * @member {PIXI.Rectangle} + * @member {PIXI.Graphics|PIXI.Sprite} * @private */ this._mask = null; diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index 95f2af4..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 ); @@ -1065,10 +1061,15 @@ canvasRenderer = new CanvasRenderer(); } - tempMatrix.tx = -bounds.x; - tempMatrix.ty = -bounds.y; + this.transform.updateLocalTransform(); + this.transform.localTransform.copy(tempMatrix); - canvasRenderer.render(this, canvasBuffer, false, tempMatrix); + tempMatrix.invert(); + + tempMatrix.tx -= bounds.x; + tempMatrix.ty -= bounds.y; + + canvasRenderer.render(this, canvasBuffer, true, tempMatrix); const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode); 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 e67680f..867dde2 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/buildCircle.js b/src/core/graphics/webgl/utils/buildCircle.js index 0400d13..abf5d02 100644 --- a/src/core/graphics/webgl/utils/buildCircle.js +++ b/src/core/graphics/webgl/utils/buildCircle.js @@ -33,6 +33,11 @@ height = circleData.height; } + if (width === 0 || height === 0) + { + return; + } + const totalSegs = Math.floor(30 * Math.sqrt(circleData.radius)) || Math.floor(15 * Math.sqrt(circleData.width + circleData.height)); diff --git a/src/core/graphics/webgl/utils/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 90155f9..f03fe15 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -30,6 +30,7 @@ export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; export { default as CanvasGraphicsRenderer } from './graphics/canvas/CanvasGraphicsRenderer'; +export { default as Spritesheet } from './textures/Spritesheet'; export { default as Texture } from './textures/Texture'; export { default as BaseTexture } from './textures/BaseTexture'; export { default as RenderTexture } from './textures/RenderTexture'; diff --git a/src/core/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 ccce680..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); } /** @@ -162,6 +162,9 @@ if (transform) { transform.copy(tempWt); + + // lets not forget to flag the parent transform as dirty... + this._tempDisplayObjectParent.transform._worldID = -1; } else { @@ -169,6 +172,7 @@ } displayObject.parent = this._tempDisplayObjectParent; + displayObject.updateTransform(); displayObject.parent = cacheParent; // displayObject.hitArea = //TODO add a temp hit area @@ -279,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/canvas/utils/CanvasRenderTarget.js b/src/core/renderers/canvas/utils/CanvasRenderTarget.js index 3de0aef..b409891 100644 --- a/src/core/renderers/canvas/utils/CanvasRenderTarget.js +++ b/src/core/renderers/canvas/utils/CanvasRenderTarget.js @@ -1,5 +1,4 @@ import settings from '../../../settings'; -const { RESOLUTION } = settings; /** * Creates a Canvas element of the given size. @@ -30,7 +29,7 @@ */ this.context = this.canvas.getContext('2d'); - this.resolution = resolution || RESOLUTION; + this.resolution = resolution || settings.RESOLUTION; this.resize(width, height); } 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 ce0e1c4..788ed6c 100644 --- a/src/core/renderers/webgl/WebGLRenderer.js +++ b/src/core/renderers/webgl/WebGLRenderer.js @@ -30,8 +30,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 @@ -49,10 +49,19 @@ * enable this if you need to call toDataUrl on the webgl context. * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when * rendering, stopping pixel interpolation. + * @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; + + if (this.legacy) + { + glCore.VertexArrayObject.FORCE_NATIVE = true; + } /** * The type of this renderer as a standardised const @@ -229,7 +238,7 @@ this.emit('context', gl); // setup the width/height properties and gl viewport - this.resize(this.width, this.height); + this.resize(this.screen.width, this.screen.height); } /** @@ -323,16 +332,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) { @@ -376,6 +385,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 @@ -440,9 +469,10 @@ * Changes the current shader to the one given in parameter * * @param {PIXI.Shader} shader - the new shader + * @param {boolean} [autoProject=true] - Whether automatically set the projection matrix * @return {PIXI.WebGLRenderer} Returns itself. */ - bindShader(shader) + bindShader(shader, autoProject) { // TODO cache if (this._activeShader !== shader) @@ -450,8 +480,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 bbab3b3..944f41d 100644 --- a/src/core/renderers/webgl/filters/Filter.js +++ b/src/core/renderers/webgl/filters/Filter.js @@ -1,6 +1,7 @@ import extractUniformsFromSrc from './extractUniformsFromSrc'; import { uid } from '../../../utils'; import { BLEND_MODES } from '../../../const'; +import settings from '../../../settings'; const SOURCE_KEY_MAP = {}; @@ -10,7 +11,7 @@ * @memberof PIXI * @extends PIXI.Shader */ -class Filter +export default class Filter { /** * @param {string} [vertexSrc] - The source of the vertex shader. @@ -35,8 +36,6 @@ this.blendMode = BLEND_MODES.NORMAL; - // pull out the vertex and shader uniforms if they are not specified.. - // currently this does not extract structs only default types this.uniformData = uniforms || extractUniformsFromSrc(this.vertexSrc, this.fragmentSrc, 'projectionMatrix|uSampler'); /** @@ -80,7 +79,7 @@ * * @member {number} */ - this.resolution = 1; + this.resolution = settings.RESOLUTION; /** * If enabled is true the filter is applied, if false it will not. @@ -97,8 +96,11 @@ * @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 { // --- // // this.uniforms.filterMatrix = filterManager.calculateSpriteMatrix(tempMatrix, window.panda ); @@ -170,5 +172,3 @@ ].join('\n'); } } - -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 dc7072a..6c7422d 100644 --- a/src/core/renderers/webgl/managers/FilterManager.js +++ b/src/core/renderers/webgl/managers/FilterManager.js @@ -154,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 @@ -176,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; @@ -184,7 +184,7 @@ flop = t; } - filters[i].apply(this, flip, lastState.renderTarget, true); + filters[i].apply(this, flip, lastState.renderTarget, false, currentState); this.freePotRenderTarget(flip); this.freePotRenderTarget(flop); @@ -291,9 +291,13 @@ let textureCount = 1; let currentState; - if (shader.uniforms.data.filterArea) + // 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; @@ -306,9 +310,9 @@ // use this to clamp displaced texture coords so they belong to filterArea // see displacementFilter fragment shader for an example - if (shader.uniforms.data.filterClamp) + if (shader.uniforms.filterClamp) { - currentState = this.filterData.stack[this.filterData.index]; + currentState = currentState || this.filterData.stack[this.filterData.index]; const filterClamp = shader.uniforms.filterClamp; @@ -485,7 +489,7 @@ */ destroy() { - this.shaderCache = []; + this.shaderCache = {}; this.emptyPool(); } diff --git a/src/core/renderers/webgl/utils/RenderTarget.js b/src/core/renderers/webgl/utils/RenderTarget.js index afe51ab..6d2467f 100644 --- a/src/core/renderers/webgl/utils/RenderTarget.js +++ b/src/core/renderers/webgl/utils/RenderTarget.js @@ -124,7 +124,7 @@ * @default PIXI.settings.SCALE_MODE * @see PIXI.SCALE_MODES */ - this.scaleMode = scaleMode || settings.SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; /** * Whether this object is the root element or not 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 36ea5bb..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; @@ -346,8 +346,8 @@ /** * Gets the local bounds of the sprite object. * - * @param {Rectangle} rect - The output rectangle. - * @return {Rectangle} The bounds. + * @param {PIXI.Rectangle} rect - The output rectangle. + * @return {PIXI.Rectangle} The bounds. */ getLocalBounds(rect) { @@ -440,7 +440,7 @@ * * @static * @param {number|string|PIXI.BaseTexture|HTMLCanvasElement|HTMLVideoElement} source Source to create texture from - * @return {PIXI.Texture} 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 e77b39a..90b08c0 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++) @@ -103,11 +102,18 @@ { const gl = this.renderer.gl; - // step 1: first check max textures the GPU can handle. - this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); + if (this.renderer.legacy) + { + this.MAX_TEXTURES = 1; + } + else + { + // step 1: first check max textures the GPU can handle. + this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); - // step 2: check the maximum number of if statements the shader can have too.. - this.MAX_TEXTURES = checkMaxIfStatmentsInShader(this.MAX_TEXTURES, gl); + // step 2: check the maximum number of if statements the shader can have too.. + this.MAX_TEXTURES = checkMaxIfStatmentsInShader(this.MAX_TEXTURES, gl); + } const shader = this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); @@ -130,8 +136,12 @@ .addIndex(this.indexBuffer) .addAttribute(this.vertexBuffers[i], shader.attributes.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) .addAttribute(this.vertexBuffers[i], shader.attributes.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) - .addAttribute(this.vertexBuffers[i], shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4) - .addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + .addAttribute(this.vertexBuffers[i], shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); + + if (shader.attributes.aTextureId) + { + this.vaos[i].addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + } /* eslint-enable max-len */ } @@ -378,8 +388,12 @@ .addIndex(this.indexBuffer) .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) - .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4) - .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); + + if (this.shader.attributes.aTextureId) + { + this.vaos[this.vertexCount].addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + } /* eslint-enable max-len */ } diff --git a/src/core/text/Text.js b/src/core/text/Text.js index 013625a..cec8c32 100644 --- a/src/core/text/Text.js +++ b/src/core/text/Text.js @@ -6,6 +6,7 @@ import { TEXT_GRADIENT } from '../const'; import settings from '../settings'; import TextStyle from './TextStyle'; +import trimCanvas from '../utils/trimCanvas'; const defaultDestroyOptions = { texture: true, @@ -57,7 +58,7 @@ /** * The canvas 2d context that everything is drawn with - * @member {HTMLCanvasElement} + * @member {CanvasRenderingContext2D} */ this.context = this.canvas.getContext('2d'); @@ -189,10 +190,12 @@ if (style.dropShadow) { + this.context.shadowBlur = style.dropShadowBlur; + this.context.globalAlpha = style.dropShadowAlpha; + if (style.dropShadowBlur > 0) { this.context.shadowColor = style.dropShadowColor; - this.context.shadowBlur = style.dropShadowBlur; } else { @@ -237,6 +240,10 @@ } } + // reset the shadow blur and alpha that was set by the drop shadow, for the regular text + this.context.shadowBlur = 0; + this.context.globalAlpha = 1; + // set canvas text styles this.context.fillStyle = this._generateFillStyle(style, lines); @@ -326,6 +333,15 @@ */ updateTexture() { + if (this._style.trim) + { + const trimmed = trimCanvas(this.canvas); + + this.canvas.width = trimmed.width; + this.canvas.height = trimmed.height; + this.context.putImageData(trimmed.data, 0, 0); + } + const texture = this._texture; const style = this._style; @@ -475,6 +491,19 @@ } /** + * Gets the local bounds of the text object. + * + * @param {Rectangle} rect - The output rectangle. + * @return {Rectangle} The bounds. + */ + getLocalBounds(rect) + { + this.updateText(true); + + return super.getLocalBounds.call(this, rect); + } + + /** * calculates the bounds of the Text as a rectangle. The bounds calculation takes the worldTransform into account. */ _calculateBounds() @@ -525,6 +554,29 @@ const width = this.canvas.width / this.resolution; const height = this.canvas.height / this.resolution; + // make a copy of the style settings, so we can manipulate them later + const fill = style.fill.slice(); + const fillGradientStops = style.fillGradientStops.slice(); + + // wanting to evenly distribute the fills. So an array of 4 colours should give fills of 0.25, 0.5 and 0.75 + if (!fillGradientStops.length) + { + const lengthPlus1 = fill.length + 1; + + for (let i = 1; i < lengthPlus1; ++i) + { + fillGradientStops.push(i / lengthPlus1); + } + } + + // stop the bleeding of the last gradient on the line above to the top gradient of the this line + // by hard defining the first gradient colour at point 0, and last gradient colour at point 1 + fill.unshift(style.fill[0]); + fillGradientStops.unshift(0); + + fill.push(style.fill[style.fill.length - 1]); + fillGradientStops.push(1); + if (style.fillGradientType === TEXT_GRADIENT.LINEAR_VERTICAL) { // start the gradient at the top center of the canvas, and end at the bottom middle of the canvas @@ -532,15 +584,22 @@ // we need to repeat the gradient so that each individual line of text has the same vertical gradient effect // ['#FF0000', '#00FF00', '#0000FF'] over 2 lines would create stops at 0.125, 0.25, 0.375, 0.625, 0.75, 0.875 - totalIterations = (style.fill.length + 1) * lines.length; + totalIterations = (fill.length + 1) * lines.length; currentIteration = 0; for (let i = 0; i < lines.length; i++) { currentIteration += 1; - for (let j = 0; j < style.fill.length; j++) + for (let j = 0; j < fill.length; j++) { - stop = (currentIteration / totalIterations); - gradient.addColorStop(stop, style.fill[j]); + if (fillGradientStops[j]) + { + stop = (fillGradientStops[j] / lines.length) + (i / lines.length); + } + else + { + stop = currentIteration / totalIterations; + } + gradient.addColorStop(stop, fill[j]); currentIteration++; } } @@ -552,13 +611,20 @@ // can just evenly space out the gradients in this case, as multiple lines makes no difference // to an even left to right gradient - totalIterations = style.fill.length + 1; + totalIterations = fill.length + 1; currentIteration = 1; - for (let i = 0; i < style.fill.length; i++) + for (let i = 0; i < fill.length; i++) { - stop = currentIteration / totalIterations; - gradient.addColorStop(stop, style.fill[i]); + if (fillGradientStops[i]) + { + stop = fillGradientStops[i]; + } + else + { + stop = currentIteration / totalIterations; + } + gradient.addColorStop(stop, fill[i]); currentIteration++; } } @@ -569,7 +635,7 @@ /** * Destroys this text object. * Note* Unlike a Sprite, a Text object will automatically destroy its baseTexture and texture as - * the majorety of the time the texture will not be shared with any other Sprites. + * the majority of the time the texture will not be shared with any other Sprites. * * @param {object|boolean} [options] - Options parameter. A boolean will act as if all options * have been set to that value @@ -680,7 +746,7 @@ set text(text) // eslint-disable-line require-jsdoc { - text = String(text || ' '); + text = String(text === '' || text === null || text === undefined ? ' ' : text); if (this._text === text) { diff --git a/src/core/text/TextStyle.js b/src/core/text/TextStyle.js index 3b25528..890950b 100644 --- a/src/core/text/TextStyle.js +++ b/src/core/text/TextStyle.js @@ -8,12 +8,14 @@ align: 'left', breakWords: false, dropShadow: false, + dropShadowAlpha: 1, dropShadowAngle: Math.PI / 6, dropShadowBlur: 0, dropShadowColor: '#000000', dropShadowDistance: 5, fill: 'black', fillGradientType: TEXT_GRADIENT.LINEAR_VERTICAL, + fillGradientStops: [], fontFamily: 'Arial', fontSize: 26, fontStyle: 'normal', @@ -27,6 +29,7 @@ stroke: 'black', strokeThickness: 0, textBaseline: 'alphabetic', + trim: false, wordWrap: false, wordWrapWidth: 100, }; @@ -47,6 +50,7 @@ * @param {boolean} [style.breakWords=false] - Indicates if lines can be wrapped within words, it * needs wordWrap to be set to true * @param {boolean} [style.dropShadow=false] - Set a drop shadow for the text + * @param {number} [style.dropShadowAlpha=1] - Set alpha for the drop shadow * @param {number} [style.dropShadowAngle=Math.PI/6] - Set a angle of the drop shadow * @param {number} [style.dropShadowBlur=0] - Set a shadow blur radius * @param {string} [style.dropShadowColor='#000000'] - A fill style to be used on the dropshadow e.g 'red', '#00FF00' @@ -55,8 +59,10 @@ * fillstyle that will be used on the text e.g 'red', '#00FF00'. Can be an array to create a gradient * eg ['#000000','#FFFFFF'] * {@link https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/fillStyle|MDN} - * @param {number} [style.fillGradientType=PIXI.TEXT_GRADIENT.LINEAR_VERTICAL] - If fills styles are - * supplied, this can change the type/direction of the gradient. See {@link PIXI.TEXT_GRADIENT} for possible values + * @param {number} [style.fillGradientType=PIXI.TEXT_GRADIENT.LINEAR_VERTICAL] - If fill is an array of colours + * to create a gradient, this can change the type/direction of the gradient. See {@link PIXI.TEXT_GRADIENT} + * @param {number[]} [style.fillGradientStops] - If fill is an array of colours to create a gradient, this array can set + * the stop points (numbers between 0 and 1) for the color, overriding the default behaviour of evenly spacing them. * @param {string|string[]} [style.fontFamily='Arial'] - The font family * @param {number|string} [style.fontSize=26] - The font size (as a number it converts to px, but as a string, * equivalents are '26px','20pt','160%' or '1.6em') @@ -76,6 +82,7 @@ * e.g 'blue', '#FCFF00' * @param {number} [style.strokeThickness=0] - A number that represents the thickness of the stroke. * Default is 0 (no stroke) + * @param {boolean} [style.trim=false] - Trim transparent borders * @param {string} [style.textBaseline='alphabetic'] - The baseline of the text that is rendered. * @param {boolean} [style.wordWrap=false] - Indicates if word wrap should be used * @param {number} [style.wordWrapWidth=100] - The width at which text will wrap, it needs wordWrap to be set to true @@ -152,6 +159,19 @@ } } + get dropShadowAlpha() + { + return this._dropShadowAlpha; + } + set dropShadowAlpha(dropShadowAlpha) + { + if (this._dropShadowAlpha !== dropShadowAlpha) + { + this._dropShadowAlpha = dropShadowAlpha; + this.styleID++; + } + } + get dropShadowAngle() { return this._dropShadowAngle; @@ -232,6 +252,19 @@ } } + get fillGradientStops() + { + return this._fillGradientStops; + } + set fillGradientStops(fillGradientStops) + { + if (!areArraysEqual(this._fillGradientStops,fillGradientStops)) + { + this._fillGradientStops = fillGradientStops; + this.styleID++; + } + } + get fontFamily() { return this._fontFamily; @@ -402,6 +435,19 @@ } } + get trim() + { + return this._trim; + } + set trim(trim) + { + if (this._trim !== trim) + { + this._trim = trim; + this.styleID++; + } + } + get wordWrap() { return this._wordWrap; @@ -475,3 +521,34 @@ return color; } } + +/** + * Utility function to convert hexadecimal colors to strings, and simply return the color if it's a string. + * This version can also convert array of colors + * + * @param {Array} array1 First array to compare + * @param {Array} array2 Second array to compare + * @return {boolean} Do the arrays contain the same values in the same order + */ +function areArraysEqual(array1, array2) +{ + if (!Array.isArray(array1) || !Array.isArray(array2)) + { + return false; + } + + if (array1.length !== array2.length) + { + return false; + } + + for (let i = 0; i < array1.length; ++i) + { + if (array1[i] !== array2[i]) + { + return false; + } + } + + return true; +} diff --git a/src/core/textures/BaseRenderTexture.js b/src/core/textures/BaseRenderTexture.js index 5d50146..aa727f0 100644 --- a/src/core/textures/BaseRenderTexture.js +++ b/src/core/textures/BaseRenderTexture.js @@ -1,8 +1,6 @@ import BaseTexture from './BaseTexture'; import settings from '../settings'; -const { RESOLUTION, SCALE_MODE } = settings; - /** * A BaseRenderTexture is a special texture that allows any Pixi display object to be rendered to it. * @@ -54,7 +52,7 @@ { super(null, scaleMode); - this.resolution = resolution || RESOLUTION; + this.resolution = resolution || settings.RESOLUTION; this.width = width; this.height = height; @@ -62,7 +60,7 @@ this.realWidth = this.width * this.resolution; this.realHeight = this.height * this.resolution; - this.scaleMode = scaleMode || SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; this.hasLoaded = true; /** diff --git a/src/core/textures/BaseTexture.js b/src/core/textures/BaseTexture.js index 676dca3..aeccf39 100644 --- a/src/core/textures/BaseTexture.js +++ b/src/core/textures/BaseTexture.js @@ -77,7 +77,7 @@ * @default PIXI.settings.SCALE_MODE * @see PIXI.SCALE_MODES */ - this.scaleMode = scaleMode || settings.SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; /** * Set to true once the base texture has successfully loaded. @@ -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/RenderTexture.js b/src/core/textures/RenderTexture.js index 4c5034f..b3c6297 100644 --- a/src/core/textures/RenderTexture.js +++ b/src/core/textures/RenderTexture.js @@ -54,8 +54,8 @@ /* eslint-disable prefer-rest-params, no-console */ const width = arguments[1]; const height = arguments[2]; - const scaleMode = arguments[3] || 0; - const resolution = arguments[4] || 1; + const scaleMode = arguments[3]; + const resolution = arguments[4]; // we have an old render texture.. console.warn(`Please use RenderTexture.create(${width}, ${height}) instead of the ctor directly.`); 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 ca31456..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 @@ -459,7 +497,9 @@ if (frame.x + frame.width > this.baseTexture.width || frame.y + frame.height > this.baseTexture.height) { - throw new Error(`Texture Error: frame does not fit inside the base Texture dimensions ${this}`); + throw new Error('Texture Error: frame does not fit inside the base Texture dimensions: ' + + `X: ${frame.x} + ${frame.width} > ${this.baseTexture.width} ` + + `Y: ${frame.y} + ${frame.height} > ${this.baseTexture.height}`); } // this.valid = frame && frame.width && frame.height && this.baseTexture.source && this.baseTexture.hasLoaded; @@ -520,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. @@ -528,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 1271fef..71ade15 100644 --- a/package.json +++ b/package.json @@ -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/scripts/release.js b/scripts/release.js index c4c8e36..156620c 100644 --- a/scripts/release.js +++ b/scripts/release.js @@ -12,7 +12,8 @@ 'dist/**/*', 'lib/**/*', 'src/**/*', - 'scripts/**/*', + 'scripts/*', + 'scripts/renders/*', 'test/**/*', '*.json', '*.md', diff --git a/scripts/renders/client.js b/scripts/renders/client.js index 16c9565..6faee34 100755 --- a/scripts/renders/client.js +++ b/scripts/renders/client.js @@ -1,6 +1,6 @@ 'use strict'; -require('../../bin/pixi'); +require('../../dist/pixi'); PIXI.utils.skipHello(); const fs = require('fs'); 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 bc730db..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. @@ -33,8 +33,11 @@ * need to call toDataUrl on the webgl context * @param {number} [options.resolution=1] - The resolution / device pixel ratio of the renderer, retina would be 2 * @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 @@ -43,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. */ @@ -73,7 +99,7 @@ */ stop() { - this.ticker.stop(); + this._ticker.stop(); } /** @@ -81,7 +107,7 @@ */ start() { - this.ticker.start(); + this._ticker.start(); } /** @@ -95,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 index 81e076b..78b17e6 100644 --- a/src/core/Shader.js +++ b/src/core/Shader.js @@ -1,9 +1,7 @@ import { GLShader } from 'pixi-gl-core'; import settings from './settings'; -const { PRECISION } = settings; - -function checkPrecision(src) +function checkPrecision(src, def) { if (src instanceof Array) { @@ -11,14 +9,14 @@ { const copy = src.slice(0); - copy.unshift(`precision ${PRECISION} float;`); + copy.unshift(`precision ${def} float;`); return copy; } } else if (src.substring(0, 9) !== 'precision') { - return `precision ${PRECISION} float;\n${src}`; + return `precision ${def} float;\n${src}`; } return src; @@ -42,6 +40,7 @@ */ constructor(gl, vertexSrc, fragmentSrc) { - super(gl, checkPrecision(vertexSrc), checkPrecision(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 3425b41..f9235e4 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -181,7 +181,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 b3792ef..17fde8d 100644 --- a/src/core/display/Container.js +++ b/src/core/display/Container.js @@ -73,13 +73,14 @@ } child.parent = this; - - // ensure a transform will be recalculated.. - this.transform._parentID = -1; - this._boundsID++; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.push(child); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(this.children.length - 1); child.emit('added', this); @@ -108,9 +109,14 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.splice(index, 0, child); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(index); child.emit('added', this); @@ -174,6 +180,7 @@ removeItems(this.children, currentIndex, 1); // remove from old position this.children.splice(index, 0, child); // add at new position + this.onChildrenChange(index); } @@ -220,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! @@ -244,9 +252,14 @@ { const child = this.getChildAt(index); + // ensure child transform will be recalculated.. child.parent = null; + child.transform._parentID = -1; removeItems(this.children, index, 1); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(index); child.emit('removed', this); @@ -275,8 +288,14 @@ for (let i = 0; i < removed.length; ++i) { removed[i].parent = null; + if (removed[i].transform) + { + removed[i].transform._parentID = -1; + } } + this._boundsID++; + this.onChildrenChange(beginIndex); for (let i = 0; i < removed.length; ++i) diff --git a/src/core/display/DisplayObject.js b/src/core/display/DisplayObject.js index 3affe36..4e6c77d 100644 --- a/src/core/display/DisplayObject.js +++ b/src/core/display/DisplayObject.js @@ -109,7 +109,7 @@ /** * The original, cached mask of the object * - * @member {PIXI.Rectangle} + * @member {PIXI.Graphics|PIXI.Sprite} * @private */ this._mask = null; diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index 95f2af4..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 ); @@ -1065,10 +1061,15 @@ canvasRenderer = new CanvasRenderer(); } - tempMatrix.tx = -bounds.x; - tempMatrix.ty = -bounds.y; + this.transform.updateLocalTransform(); + this.transform.localTransform.copy(tempMatrix); - canvasRenderer.render(this, canvasBuffer, false, tempMatrix); + tempMatrix.invert(); + + tempMatrix.tx -= bounds.x; + tempMatrix.ty -= bounds.y; + + canvasRenderer.render(this, canvasBuffer, true, tempMatrix); const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode); 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 e67680f..867dde2 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/buildCircle.js b/src/core/graphics/webgl/utils/buildCircle.js index 0400d13..abf5d02 100644 --- a/src/core/graphics/webgl/utils/buildCircle.js +++ b/src/core/graphics/webgl/utils/buildCircle.js @@ -33,6 +33,11 @@ height = circleData.height; } + if (width === 0 || height === 0) + { + return; + } + const totalSegs = Math.floor(30 * Math.sqrt(circleData.radius)) || Math.floor(15 * Math.sqrt(circleData.width + circleData.height)); diff --git a/src/core/graphics/webgl/utils/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 90155f9..f03fe15 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -30,6 +30,7 @@ export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; export { default as CanvasGraphicsRenderer } from './graphics/canvas/CanvasGraphicsRenderer'; +export { default as Spritesheet } from './textures/Spritesheet'; export { default as Texture } from './textures/Texture'; export { default as BaseTexture } from './textures/BaseTexture'; export { default as RenderTexture } from './textures/RenderTexture'; diff --git a/src/core/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 ccce680..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); } /** @@ -162,6 +162,9 @@ if (transform) { transform.copy(tempWt); + + // lets not forget to flag the parent transform as dirty... + this._tempDisplayObjectParent.transform._worldID = -1; } else { @@ -169,6 +172,7 @@ } displayObject.parent = this._tempDisplayObjectParent; + displayObject.updateTransform(); displayObject.parent = cacheParent; // displayObject.hitArea = //TODO add a temp hit area @@ -279,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/canvas/utils/CanvasRenderTarget.js b/src/core/renderers/canvas/utils/CanvasRenderTarget.js index 3de0aef..b409891 100644 --- a/src/core/renderers/canvas/utils/CanvasRenderTarget.js +++ b/src/core/renderers/canvas/utils/CanvasRenderTarget.js @@ -1,5 +1,4 @@ import settings from '../../../settings'; -const { RESOLUTION } = settings; /** * Creates a Canvas element of the given size. @@ -30,7 +29,7 @@ */ this.context = this.canvas.getContext('2d'); - this.resolution = resolution || RESOLUTION; + this.resolution = resolution || settings.RESOLUTION; this.resize(width, height); } 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 ce0e1c4..788ed6c 100644 --- a/src/core/renderers/webgl/WebGLRenderer.js +++ b/src/core/renderers/webgl/WebGLRenderer.js @@ -30,8 +30,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 @@ -49,10 +49,19 @@ * enable this if you need to call toDataUrl on the webgl context. * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when * rendering, stopping pixel interpolation. + * @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; + + if (this.legacy) + { + glCore.VertexArrayObject.FORCE_NATIVE = true; + } /** * The type of this renderer as a standardised const @@ -229,7 +238,7 @@ this.emit('context', gl); // setup the width/height properties and gl viewport - this.resize(this.width, this.height); + this.resize(this.screen.width, this.screen.height); } /** @@ -323,16 +332,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) { @@ -376,6 +385,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 @@ -440,9 +469,10 @@ * Changes the current shader to the one given in parameter * * @param {PIXI.Shader} shader - the new shader + * @param {boolean} [autoProject=true] - Whether automatically set the projection matrix * @return {PIXI.WebGLRenderer} Returns itself. */ - bindShader(shader) + bindShader(shader, autoProject) { // TODO cache if (this._activeShader !== shader) @@ -450,8 +480,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 bbab3b3..944f41d 100644 --- a/src/core/renderers/webgl/filters/Filter.js +++ b/src/core/renderers/webgl/filters/Filter.js @@ -1,6 +1,7 @@ import extractUniformsFromSrc from './extractUniformsFromSrc'; import { uid } from '../../../utils'; import { BLEND_MODES } from '../../../const'; +import settings from '../../../settings'; const SOURCE_KEY_MAP = {}; @@ -10,7 +11,7 @@ * @memberof PIXI * @extends PIXI.Shader */ -class Filter +export default class Filter { /** * @param {string} [vertexSrc] - The source of the vertex shader. @@ -35,8 +36,6 @@ this.blendMode = BLEND_MODES.NORMAL; - // pull out the vertex and shader uniforms if they are not specified.. - // currently this does not extract structs only default types this.uniformData = uniforms || extractUniformsFromSrc(this.vertexSrc, this.fragmentSrc, 'projectionMatrix|uSampler'); /** @@ -80,7 +79,7 @@ * * @member {number} */ - this.resolution = 1; + this.resolution = settings.RESOLUTION; /** * If enabled is true the filter is applied, if false it will not. @@ -97,8 +96,11 @@ * @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 { // --- // // this.uniforms.filterMatrix = filterManager.calculateSpriteMatrix(tempMatrix, window.panda ); @@ -170,5 +172,3 @@ ].join('\n'); } } - -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 dc7072a..6c7422d 100644 --- a/src/core/renderers/webgl/managers/FilterManager.js +++ b/src/core/renderers/webgl/managers/FilterManager.js @@ -154,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 @@ -176,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; @@ -184,7 +184,7 @@ flop = t; } - filters[i].apply(this, flip, lastState.renderTarget, true); + filters[i].apply(this, flip, lastState.renderTarget, false, currentState); this.freePotRenderTarget(flip); this.freePotRenderTarget(flop); @@ -291,9 +291,13 @@ let textureCount = 1; let currentState; - if (shader.uniforms.data.filterArea) + // 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; @@ -306,9 +310,9 @@ // use this to clamp displaced texture coords so they belong to filterArea // see displacementFilter fragment shader for an example - if (shader.uniforms.data.filterClamp) + if (shader.uniforms.filterClamp) { - currentState = this.filterData.stack[this.filterData.index]; + currentState = currentState || this.filterData.stack[this.filterData.index]; const filterClamp = shader.uniforms.filterClamp; @@ -485,7 +489,7 @@ */ destroy() { - this.shaderCache = []; + this.shaderCache = {}; this.emptyPool(); } diff --git a/src/core/renderers/webgl/utils/RenderTarget.js b/src/core/renderers/webgl/utils/RenderTarget.js index afe51ab..6d2467f 100644 --- a/src/core/renderers/webgl/utils/RenderTarget.js +++ b/src/core/renderers/webgl/utils/RenderTarget.js @@ -124,7 +124,7 @@ * @default PIXI.settings.SCALE_MODE * @see PIXI.SCALE_MODES */ - this.scaleMode = scaleMode || settings.SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; /** * Whether this object is the root element or not 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 36ea5bb..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; @@ -346,8 +346,8 @@ /** * Gets the local bounds of the sprite object. * - * @param {Rectangle} rect - The output rectangle. - * @return {Rectangle} The bounds. + * @param {PIXI.Rectangle} rect - The output rectangle. + * @return {PIXI.Rectangle} The bounds. */ getLocalBounds(rect) { @@ -440,7 +440,7 @@ * * @static * @param {number|string|PIXI.BaseTexture|HTMLCanvasElement|HTMLVideoElement} source Source to create texture from - * @return {PIXI.Texture} 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 e77b39a..90b08c0 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++) @@ -103,11 +102,18 @@ { const gl = this.renderer.gl; - // step 1: first check max textures the GPU can handle. - this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); + if (this.renderer.legacy) + { + this.MAX_TEXTURES = 1; + } + else + { + // step 1: first check max textures the GPU can handle. + this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); - // step 2: check the maximum number of if statements the shader can have too.. - this.MAX_TEXTURES = checkMaxIfStatmentsInShader(this.MAX_TEXTURES, gl); + // step 2: check the maximum number of if statements the shader can have too.. + this.MAX_TEXTURES = checkMaxIfStatmentsInShader(this.MAX_TEXTURES, gl); + } const shader = this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); @@ -130,8 +136,12 @@ .addIndex(this.indexBuffer) .addAttribute(this.vertexBuffers[i], shader.attributes.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) .addAttribute(this.vertexBuffers[i], shader.attributes.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) - .addAttribute(this.vertexBuffers[i], shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4) - .addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + .addAttribute(this.vertexBuffers[i], shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); + + if (shader.attributes.aTextureId) + { + this.vaos[i].addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + } /* eslint-enable max-len */ } @@ -378,8 +388,12 @@ .addIndex(this.indexBuffer) .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) - .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4) - .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); + + if (this.shader.attributes.aTextureId) + { + this.vaos[this.vertexCount].addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + } /* eslint-enable max-len */ } diff --git a/src/core/text/Text.js b/src/core/text/Text.js index 013625a..cec8c32 100644 --- a/src/core/text/Text.js +++ b/src/core/text/Text.js @@ -6,6 +6,7 @@ import { TEXT_GRADIENT } from '../const'; import settings from '../settings'; import TextStyle from './TextStyle'; +import trimCanvas from '../utils/trimCanvas'; const defaultDestroyOptions = { texture: true, @@ -57,7 +58,7 @@ /** * The canvas 2d context that everything is drawn with - * @member {HTMLCanvasElement} + * @member {CanvasRenderingContext2D} */ this.context = this.canvas.getContext('2d'); @@ -189,10 +190,12 @@ if (style.dropShadow) { + this.context.shadowBlur = style.dropShadowBlur; + this.context.globalAlpha = style.dropShadowAlpha; + if (style.dropShadowBlur > 0) { this.context.shadowColor = style.dropShadowColor; - this.context.shadowBlur = style.dropShadowBlur; } else { @@ -237,6 +240,10 @@ } } + // reset the shadow blur and alpha that was set by the drop shadow, for the regular text + this.context.shadowBlur = 0; + this.context.globalAlpha = 1; + // set canvas text styles this.context.fillStyle = this._generateFillStyle(style, lines); @@ -326,6 +333,15 @@ */ updateTexture() { + if (this._style.trim) + { + const trimmed = trimCanvas(this.canvas); + + this.canvas.width = trimmed.width; + this.canvas.height = trimmed.height; + this.context.putImageData(trimmed.data, 0, 0); + } + const texture = this._texture; const style = this._style; @@ -475,6 +491,19 @@ } /** + * Gets the local bounds of the text object. + * + * @param {Rectangle} rect - The output rectangle. + * @return {Rectangle} The bounds. + */ + getLocalBounds(rect) + { + this.updateText(true); + + return super.getLocalBounds.call(this, rect); + } + + /** * calculates the bounds of the Text as a rectangle. The bounds calculation takes the worldTransform into account. */ _calculateBounds() @@ -525,6 +554,29 @@ const width = this.canvas.width / this.resolution; const height = this.canvas.height / this.resolution; + // make a copy of the style settings, so we can manipulate them later + const fill = style.fill.slice(); + const fillGradientStops = style.fillGradientStops.slice(); + + // wanting to evenly distribute the fills. So an array of 4 colours should give fills of 0.25, 0.5 and 0.75 + if (!fillGradientStops.length) + { + const lengthPlus1 = fill.length + 1; + + for (let i = 1; i < lengthPlus1; ++i) + { + fillGradientStops.push(i / lengthPlus1); + } + } + + // stop the bleeding of the last gradient on the line above to the top gradient of the this line + // by hard defining the first gradient colour at point 0, and last gradient colour at point 1 + fill.unshift(style.fill[0]); + fillGradientStops.unshift(0); + + fill.push(style.fill[style.fill.length - 1]); + fillGradientStops.push(1); + if (style.fillGradientType === TEXT_GRADIENT.LINEAR_VERTICAL) { // start the gradient at the top center of the canvas, and end at the bottom middle of the canvas @@ -532,15 +584,22 @@ // we need to repeat the gradient so that each individual line of text has the same vertical gradient effect // ['#FF0000', '#00FF00', '#0000FF'] over 2 lines would create stops at 0.125, 0.25, 0.375, 0.625, 0.75, 0.875 - totalIterations = (style.fill.length + 1) * lines.length; + totalIterations = (fill.length + 1) * lines.length; currentIteration = 0; for (let i = 0; i < lines.length; i++) { currentIteration += 1; - for (let j = 0; j < style.fill.length; j++) + for (let j = 0; j < fill.length; j++) { - stop = (currentIteration / totalIterations); - gradient.addColorStop(stop, style.fill[j]); + if (fillGradientStops[j]) + { + stop = (fillGradientStops[j] / lines.length) + (i / lines.length); + } + else + { + stop = currentIteration / totalIterations; + } + gradient.addColorStop(stop, fill[j]); currentIteration++; } } @@ -552,13 +611,20 @@ // can just evenly space out the gradients in this case, as multiple lines makes no difference // to an even left to right gradient - totalIterations = style.fill.length + 1; + totalIterations = fill.length + 1; currentIteration = 1; - for (let i = 0; i < style.fill.length; i++) + for (let i = 0; i < fill.length; i++) { - stop = currentIteration / totalIterations; - gradient.addColorStop(stop, style.fill[i]); + if (fillGradientStops[i]) + { + stop = fillGradientStops[i]; + } + else + { + stop = currentIteration / totalIterations; + } + gradient.addColorStop(stop, fill[i]); currentIteration++; } } @@ -569,7 +635,7 @@ /** * Destroys this text object. * Note* Unlike a Sprite, a Text object will automatically destroy its baseTexture and texture as - * the majorety of the time the texture will not be shared with any other Sprites. + * the majority of the time the texture will not be shared with any other Sprites. * * @param {object|boolean} [options] - Options parameter. A boolean will act as if all options * have been set to that value @@ -680,7 +746,7 @@ set text(text) // eslint-disable-line require-jsdoc { - text = String(text || ' '); + text = String(text === '' || text === null || text === undefined ? ' ' : text); if (this._text === text) { diff --git a/src/core/text/TextStyle.js b/src/core/text/TextStyle.js index 3b25528..890950b 100644 --- a/src/core/text/TextStyle.js +++ b/src/core/text/TextStyle.js @@ -8,12 +8,14 @@ align: 'left', breakWords: false, dropShadow: false, + dropShadowAlpha: 1, dropShadowAngle: Math.PI / 6, dropShadowBlur: 0, dropShadowColor: '#000000', dropShadowDistance: 5, fill: 'black', fillGradientType: TEXT_GRADIENT.LINEAR_VERTICAL, + fillGradientStops: [], fontFamily: 'Arial', fontSize: 26, fontStyle: 'normal', @@ -27,6 +29,7 @@ stroke: 'black', strokeThickness: 0, textBaseline: 'alphabetic', + trim: false, wordWrap: false, wordWrapWidth: 100, }; @@ -47,6 +50,7 @@ * @param {boolean} [style.breakWords=false] - Indicates if lines can be wrapped within words, it * needs wordWrap to be set to true * @param {boolean} [style.dropShadow=false] - Set a drop shadow for the text + * @param {number} [style.dropShadowAlpha=1] - Set alpha for the drop shadow * @param {number} [style.dropShadowAngle=Math.PI/6] - Set a angle of the drop shadow * @param {number} [style.dropShadowBlur=0] - Set a shadow blur radius * @param {string} [style.dropShadowColor='#000000'] - A fill style to be used on the dropshadow e.g 'red', '#00FF00' @@ -55,8 +59,10 @@ * fillstyle that will be used on the text e.g 'red', '#00FF00'. Can be an array to create a gradient * eg ['#000000','#FFFFFF'] * {@link https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/fillStyle|MDN} - * @param {number} [style.fillGradientType=PIXI.TEXT_GRADIENT.LINEAR_VERTICAL] - If fills styles are - * supplied, this can change the type/direction of the gradient. See {@link PIXI.TEXT_GRADIENT} for possible values + * @param {number} [style.fillGradientType=PIXI.TEXT_GRADIENT.LINEAR_VERTICAL] - If fill is an array of colours + * to create a gradient, this can change the type/direction of the gradient. See {@link PIXI.TEXT_GRADIENT} + * @param {number[]} [style.fillGradientStops] - If fill is an array of colours to create a gradient, this array can set + * the stop points (numbers between 0 and 1) for the color, overriding the default behaviour of evenly spacing them. * @param {string|string[]} [style.fontFamily='Arial'] - The font family * @param {number|string} [style.fontSize=26] - The font size (as a number it converts to px, but as a string, * equivalents are '26px','20pt','160%' or '1.6em') @@ -76,6 +82,7 @@ * e.g 'blue', '#FCFF00' * @param {number} [style.strokeThickness=0] - A number that represents the thickness of the stroke. * Default is 0 (no stroke) + * @param {boolean} [style.trim=false] - Trim transparent borders * @param {string} [style.textBaseline='alphabetic'] - The baseline of the text that is rendered. * @param {boolean} [style.wordWrap=false] - Indicates if word wrap should be used * @param {number} [style.wordWrapWidth=100] - The width at which text will wrap, it needs wordWrap to be set to true @@ -152,6 +159,19 @@ } } + get dropShadowAlpha() + { + return this._dropShadowAlpha; + } + set dropShadowAlpha(dropShadowAlpha) + { + if (this._dropShadowAlpha !== dropShadowAlpha) + { + this._dropShadowAlpha = dropShadowAlpha; + this.styleID++; + } + } + get dropShadowAngle() { return this._dropShadowAngle; @@ -232,6 +252,19 @@ } } + get fillGradientStops() + { + return this._fillGradientStops; + } + set fillGradientStops(fillGradientStops) + { + if (!areArraysEqual(this._fillGradientStops,fillGradientStops)) + { + this._fillGradientStops = fillGradientStops; + this.styleID++; + } + } + get fontFamily() { return this._fontFamily; @@ -402,6 +435,19 @@ } } + get trim() + { + return this._trim; + } + set trim(trim) + { + if (this._trim !== trim) + { + this._trim = trim; + this.styleID++; + } + } + get wordWrap() { return this._wordWrap; @@ -475,3 +521,34 @@ return color; } } + +/** + * Utility function to convert hexadecimal colors to strings, and simply return the color if it's a string. + * This version can also convert array of colors + * + * @param {Array} array1 First array to compare + * @param {Array} array2 Second array to compare + * @return {boolean} Do the arrays contain the same values in the same order + */ +function areArraysEqual(array1, array2) +{ + if (!Array.isArray(array1) || !Array.isArray(array2)) + { + return false; + } + + if (array1.length !== array2.length) + { + return false; + } + + for (let i = 0; i < array1.length; ++i) + { + if (array1[i] !== array2[i]) + { + return false; + } + } + + return true; +} diff --git a/src/core/textures/BaseRenderTexture.js b/src/core/textures/BaseRenderTexture.js index 5d50146..aa727f0 100644 --- a/src/core/textures/BaseRenderTexture.js +++ b/src/core/textures/BaseRenderTexture.js @@ -1,8 +1,6 @@ import BaseTexture from './BaseTexture'; import settings from '../settings'; -const { RESOLUTION, SCALE_MODE } = settings; - /** * A BaseRenderTexture is a special texture that allows any Pixi display object to be rendered to it. * @@ -54,7 +52,7 @@ { super(null, scaleMode); - this.resolution = resolution || RESOLUTION; + this.resolution = resolution || settings.RESOLUTION; this.width = width; this.height = height; @@ -62,7 +60,7 @@ this.realWidth = this.width * this.resolution; this.realHeight = this.height * this.resolution; - this.scaleMode = scaleMode || SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; this.hasLoaded = true; /** diff --git a/src/core/textures/BaseTexture.js b/src/core/textures/BaseTexture.js index 676dca3..aeccf39 100644 --- a/src/core/textures/BaseTexture.js +++ b/src/core/textures/BaseTexture.js @@ -77,7 +77,7 @@ * @default PIXI.settings.SCALE_MODE * @see PIXI.SCALE_MODES */ - this.scaleMode = scaleMode || settings.SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; /** * Set to true once the base texture has successfully loaded. @@ -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/RenderTexture.js b/src/core/textures/RenderTexture.js index 4c5034f..b3c6297 100644 --- a/src/core/textures/RenderTexture.js +++ b/src/core/textures/RenderTexture.js @@ -54,8 +54,8 @@ /* eslint-disable prefer-rest-params, no-console */ const width = arguments[1]; const height = arguments[2]; - const scaleMode = arguments[3] || 0; - const resolution = arguments[4] || 1; + const scaleMode = arguments[3]; + const resolution = arguments[4]; // we have an old render texture.. console.warn(`Please use RenderTexture.create(${width}, ${height}) instead of the ctor directly.`); 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 ca31456..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 @@ -459,7 +497,9 @@ if (frame.x + frame.width > this.baseTexture.width || frame.y + frame.height > this.baseTexture.height) { - throw new Error(`Texture Error: frame does not fit inside the base Texture dimensions ${this}`); + throw new Error('Texture Error: frame does not fit inside the base Texture dimensions: ' + + `X: ${frame.x} + ${frame.width} > ${this.baseTexture.width} ` + + `Y: ${frame.y} + ${frame.height} > ${this.baseTexture.height}`); } // this.valid = frame && frame.width && frame.height && this.baseTexture.source && this.baseTexture.hasLoaded; @@ -520,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. @@ -528,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 2a7497b..99e585a 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)); } /** @@ -360,3 +362,43 @@ * @private */ export const BaseTextureCache = {}; + +/** + * 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 1271fef..71ade15 100644 --- a/package.json +++ b/package.json @@ -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/scripts/release.js b/scripts/release.js index c4c8e36..156620c 100644 --- a/scripts/release.js +++ b/scripts/release.js @@ -12,7 +12,8 @@ 'dist/**/*', 'lib/**/*', 'src/**/*', - 'scripts/**/*', + 'scripts/*', + 'scripts/renders/*', 'test/**/*', '*.json', '*.md', diff --git a/scripts/renders/client.js b/scripts/renders/client.js index 16c9565..6faee34 100755 --- a/scripts/renders/client.js +++ b/scripts/renders/client.js @@ -1,6 +1,6 @@ 'use strict'; -require('../../bin/pixi'); +require('../../dist/pixi'); PIXI.utils.skipHello(); const fs = require('fs'); 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 bc730db..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. @@ -33,8 +33,11 @@ * need to call toDataUrl on the webgl context * @param {number} [options.resolution=1] - The resolution / device pixel ratio of the renderer, retina would be 2 * @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 @@ -43,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. */ @@ -73,7 +99,7 @@ */ stop() { - this.ticker.stop(); + this._ticker.stop(); } /** @@ -81,7 +107,7 @@ */ start() { - this.ticker.start(); + this._ticker.start(); } /** @@ -95,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 index 81e076b..78b17e6 100644 --- a/src/core/Shader.js +++ b/src/core/Shader.js @@ -1,9 +1,7 @@ import { GLShader } from 'pixi-gl-core'; import settings from './settings'; -const { PRECISION } = settings; - -function checkPrecision(src) +function checkPrecision(src, def) { if (src instanceof Array) { @@ -11,14 +9,14 @@ { const copy = src.slice(0); - copy.unshift(`precision ${PRECISION} float;`); + copy.unshift(`precision ${def} float;`); return copy; } } else if (src.substring(0, 9) !== 'precision') { - return `precision ${PRECISION} float;\n${src}`; + return `precision ${def} float;\n${src}`; } return src; @@ -42,6 +40,7 @@ */ constructor(gl, vertexSrc, fragmentSrc) { - super(gl, checkPrecision(vertexSrc), checkPrecision(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 3425b41..f9235e4 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -181,7 +181,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 b3792ef..17fde8d 100644 --- a/src/core/display/Container.js +++ b/src/core/display/Container.js @@ -73,13 +73,14 @@ } child.parent = this; - - // ensure a transform will be recalculated.. - this.transform._parentID = -1; - this._boundsID++; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.push(child); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(this.children.length - 1); child.emit('added', this); @@ -108,9 +109,14 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.splice(index, 0, child); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(index); child.emit('added', this); @@ -174,6 +180,7 @@ removeItems(this.children, currentIndex, 1); // remove from old position this.children.splice(index, 0, child); // add at new position + this.onChildrenChange(index); } @@ -220,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! @@ -244,9 +252,14 @@ { const child = this.getChildAt(index); + // ensure child transform will be recalculated.. child.parent = null; + child.transform._parentID = -1; removeItems(this.children, index, 1); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(index); child.emit('removed', this); @@ -275,8 +288,14 @@ for (let i = 0; i < removed.length; ++i) { removed[i].parent = null; + if (removed[i].transform) + { + removed[i].transform._parentID = -1; + } } + this._boundsID++; + this.onChildrenChange(beginIndex); for (let i = 0; i < removed.length; ++i) diff --git a/src/core/display/DisplayObject.js b/src/core/display/DisplayObject.js index 3affe36..4e6c77d 100644 --- a/src/core/display/DisplayObject.js +++ b/src/core/display/DisplayObject.js @@ -109,7 +109,7 @@ /** * The original, cached mask of the object * - * @member {PIXI.Rectangle} + * @member {PIXI.Graphics|PIXI.Sprite} * @private */ this._mask = null; diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index 95f2af4..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 ); @@ -1065,10 +1061,15 @@ canvasRenderer = new CanvasRenderer(); } - tempMatrix.tx = -bounds.x; - tempMatrix.ty = -bounds.y; + this.transform.updateLocalTransform(); + this.transform.localTransform.copy(tempMatrix); - canvasRenderer.render(this, canvasBuffer, false, tempMatrix); + tempMatrix.invert(); + + tempMatrix.tx -= bounds.x; + tempMatrix.ty -= bounds.y; + + canvasRenderer.render(this, canvasBuffer, true, tempMatrix); const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode); 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 e67680f..867dde2 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/buildCircle.js b/src/core/graphics/webgl/utils/buildCircle.js index 0400d13..abf5d02 100644 --- a/src/core/graphics/webgl/utils/buildCircle.js +++ b/src/core/graphics/webgl/utils/buildCircle.js @@ -33,6 +33,11 @@ height = circleData.height; } + if (width === 0 || height === 0) + { + return; + } + const totalSegs = Math.floor(30 * Math.sqrt(circleData.radius)) || Math.floor(15 * Math.sqrt(circleData.width + circleData.height)); diff --git a/src/core/graphics/webgl/utils/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 90155f9..f03fe15 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -30,6 +30,7 @@ export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; export { default as CanvasGraphicsRenderer } from './graphics/canvas/CanvasGraphicsRenderer'; +export { default as Spritesheet } from './textures/Spritesheet'; export { default as Texture } from './textures/Texture'; export { default as BaseTexture } from './textures/BaseTexture'; export { default as RenderTexture } from './textures/RenderTexture'; diff --git a/src/core/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 ccce680..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); } /** @@ -162,6 +162,9 @@ if (transform) { transform.copy(tempWt); + + // lets not forget to flag the parent transform as dirty... + this._tempDisplayObjectParent.transform._worldID = -1; } else { @@ -169,6 +172,7 @@ } displayObject.parent = this._tempDisplayObjectParent; + displayObject.updateTransform(); displayObject.parent = cacheParent; // displayObject.hitArea = //TODO add a temp hit area @@ -279,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/canvas/utils/CanvasRenderTarget.js b/src/core/renderers/canvas/utils/CanvasRenderTarget.js index 3de0aef..b409891 100644 --- a/src/core/renderers/canvas/utils/CanvasRenderTarget.js +++ b/src/core/renderers/canvas/utils/CanvasRenderTarget.js @@ -1,5 +1,4 @@ import settings from '../../../settings'; -const { RESOLUTION } = settings; /** * Creates a Canvas element of the given size. @@ -30,7 +29,7 @@ */ this.context = this.canvas.getContext('2d'); - this.resolution = resolution || RESOLUTION; + this.resolution = resolution || settings.RESOLUTION; this.resize(width, height); } 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 ce0e1c4..788ed6c 100644 --- a/src/core/renderers/webgl/WebGLRenderer.js +++ b/src/core/renderers/webgl/WebGLRenderer.js @@ -30,8 +30,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 @@ -49,10 +49,19 @@ * enable this if you need to call toDataUrl on the webgl context. * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when * rendering, stopping pixel interpolation. + * @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; + + if (this.legacy) + { + glCore.VertexArrayObject.FORCE_NATIVE = true; + } /** * The type of this renderer as a standardised const @@ -229,7 +238,7 @@ this.emit('context', gl); // setup the width/height properties and gl viewport - this.resize(this.width, this.height); + this.resize(this.screen.width, this.screen.height); } /** @@ -323,16 +332,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) { @@ -376,6 +385,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 @@ -440,9 +469,10 @@ * Changes the current shader to the one given in parameter * * @param {PIXI.Shader} shader - the new shader + * @param {boolean} [autoProject=true] - Whether automatically set the projection matrix * @return {PIXI.WebGLRenderer} Returns itself. */ - bindShader(shader) + bindShader(shader, autoProject) { // TODO cache if (this._activeShader !== shader) @@ -450,8 +480,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 bbab3b3..944f41d 100644 --- a/src/core/renderers/webgl/filters/Filter.js +++ b/src/core/renderers/webgl/filters/Filter.js @@ -1,6 +1,7 @@ import extractUniformsFromSrc from './extractUniformsFromSrc'; import { uid } from '../../../utils'; import { BLEND_MODES } from '../../../const'; +import settings from '../../../settings'; const SOURCE_KEY_MAP = {}; @@ -10,7 +11,7 @@ * @memberof PIXI * @extends PIXI.Shader */ -class Filter +export default class Filter { /** * @param {string} [vertexSrc] - The source of the vertex shader. @@ -35,8 +36,6 @@ this.blendMode = BLEND_MODES.NORMAL; - // pull out the vertex and shader uniforms if they are not specified.. - // currently this does not extract structs only default types this.uniformData = uniforms || extractUniformsFromSrc(this.vertexSrc, this.fragmentSrc, 'projectionMatrix|uSampler'); /** @@ -80,7 +79,7 @@ * * @member {number} */ - this.resolution = 1; + this.resolution = settings.RESOLUTION; /** * If enabled is true the filter is applied, if false it will not. @@ -97,8 +96,11 @@ * @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 { // --- // // this.uniforms.filterMatrix = filterManager.calculateSpriteMatrix(tempMatrix, window.panda ); @@ -170,5 +172,3 @@ ].join('\n'); } } - -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 dc7072a..6c7422d 100644 --- a/src/core/renderers/webgl/managers/FilterManager.js +++ b/src/core/renderers/webgl/managers/FilterManager.js @@ -154,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 @@ -176,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; @@ -184,7 +184,7 @@ flop = t; } - filters[i].apply(this, flip, lastState.renderTarget, true); + filters[i].apply(this, flip, lastState.renderTarget, false, currentState); this.freePotRenderTarget(flip); this.freePotRenderTarget(flop); @@ -291,9 +291,13 @@ let textureCount = 1; let currentState; - if (shader.uniforms.data.filterArea) + // 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; @@ -306,9 +310,9 @@ // use this to clamp displaced texture coords so they belong to filterArea // see displacementFilter fragment shader for an example - if (shader.uniforms.data.filterClamp) + if (shader.uniforms.filterClamp) { - currentState = this.filterData.stack[this.filterData.index]; + currentState = currentState || this.filterData.stack[this.filterData.index]; const filterClamp = shader.uniforms.filterClamp; @@ -485,7 +489,7 @@ */ destroy() { - this.shaderCache = []; + this.shaderCache = {}; this.emptyPool(); } diff --git a/src/core/renderers/webgl/utils/RenderTarget.js b/src/core/renderers/webgl/utils/RenderTarget.js index afe51ab..6d2467f 100644 --- a/src/core/renderers/webgl/utils/RenderTarget.js +++ b/src/core/renderers/webgl/utils/RenderTarget.js @@ -124,7 +124,7 @@ * @default PIXI.settings.SCALE_MODE * @see PIXI.SCALE_MODES */ - this.scaleMode = scaleMode || settings.SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; /** * Whether this object is the root element or not 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 36ea5bb..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; @@ -346,8 +346,8 @@ /** * Gets the local bounds of the sprite object. * - * @param {Rectangle} rect - The output rectangle. - * @return {Rectangle} The bounds. + * @param {PIXI.Rectangle} rect - The output rectangle. + * @return {PIXI.Rectangle} The bounds. */ getLocalBounds(rect) { @@ -440,7 +440,7 @@ * * @static * @param {number|string|PIXI.BaseTexture|HTMLCanvasElement|HTMLVideoElement} source Source to create texture from - * @return {PIXI.Texture} 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 e77b39a..90b08c0 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++) @@ -103,11 +102,18 @@ { const gl = this.renderer.gl; - // step 1: first check max textures the GPU can handle. - this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); + if (this.renderer.legacy) + { + this.MAX_TEXTURES = 1; + } + else + { + // step 1: first check max textures the GPU can handle. + this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); - // step 2: check the maximum number of if statements the shader can have too.. - this.MAX_TEXTURES = checkMaxIfStatmentsInShader(this.MAX_TEXTURES, gl); + // step 2: check the maximum number of if statements the shader can have too.. + this.MAX_TEXTURES = checkMaxIfStatmentsInShader(this.MAX_TEXTURES, gl); + } const shader = this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); @@ -130,8 +136,12 @@ .addIndex(this.indexBuffer) .addAttribute(this.vertexBuffers[i], shader.attributes.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) .addAttribute(this.vertexBuffers[i], shader.attributes.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) - .addAttribute(this.vertexBuffers[i], shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4) - .addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + .addAttribute(this.vertexBuffers[i], shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); + + if (shader.attributes.aTextureId) + { + this.vaos[i].addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + } /* eslint-enable max-len */ } @@ -378,8 +388,12 @@ .addIndex(this.indexBuffer) .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) - .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4) - .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); + + if (this.shader.attributes.aTextureId) + { + this.vaos[this.vertexCount].addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + } /* eslint-enable max-len */ } diff --git a/src/core/text/Text.js b/src/core/text/Text.js index 013625a..cec8c32 100644 --- a/src/core/text/Text.js +++ b/src/core/text/Text.js @@ -6,6 +6,7 @@ import { TEXT_GRADIENT } from '../const'; import settings from '../settings'; import TextStyle from './TextStyle'; +import trimCanvas from '../utils/trimCanvas'; const defaultDestroyOptions = { texture: true, @@ -57,7 +58,7 @@ /** * The canvas 2d context that everything is drawn with - * @member {HTMLCanvasElement} + * @member {CanvasRenderingContext2D} */ this.context = this.canvas.getContext('2d'); @@ -189,10 +190,12 @@ if (style.dropShadow) { + this.context.shadowBlur = style.dropShadowBlur; + this.context.globalAlpha = style.dropShadowAlpha; + if (style.dropShadowBlur > 0) { this.context.shadowColor = style.dropShadowColor; - this.context.shadowBlur = style.dropShadowBlur; } else { @@ -237,6 +240,10 @@ } } + // reset the shadow blur and alpha that was set by the drop shadow, for the regular text + this.context.shadowBlur = 0; + this.context.globalAlpha = 1; + // set canvas text styles this.context.fillStyle = this._generateFillStyle(style, lines); @@ -326,6 +333,15 @@ */ updateTexture() { + if (this._style.trim) + { + const trimmed = trimCanvas(this.canvas); + + this.canvas.width = trimmed.width; + this.canvas.height = trimmed.height; + this.context.putImageData(trimmed.data, 0, 0); + } + const texture = this._texture; const style = this._style; @@ -475,6 +491,19 @@ } /** + * Gets the local bounds of the text object. + * + * @param {Rectangle} rect - The output rectangle. + * @return {Rectangle} The bounds. + */ + getLocalBounds(rect) + { + this.updateText(true); + + return super.getLocalBounds.call(this, rect); + } + + /** * calculates the bounds of the Text as a rectangle. The bounds calculation takes the worldTransform into account. */ _calculateBounds() @@ -525,6 +554,29 @@ const width = this.canvas.width / this.resolution; const height = this.canvas.height / this.resolution; + // make a copy of the style settings, so we can manipulate them later + const fill = style.fill.slice(); + const fillGradientStops = style.fillGradientStops.slice(); + + // wanting to evenly distribute the fills. So an array of 4 colours should give fills of 0.25, 0.5 and 0.75 + if (!fillGradientStops.length) + { + const lengthPlus1 = fill.length + 1; + + for (let i = 1; i < lengthPlus1; ++i) + { + fillGradientStops.push(i / lengthPlus1); + } + } + + // stop the bleeding of the last gradient on the line above to the top gradient of the this line + // by hard defining the first gradient colour at point 0, and last gradient colour at point 1 + fill.unshift(style.fill[0]); + fillGradientStops.unshift(0); + + fill.push(style.fill[style.fill.length - 1]); + fillGradientStops.push(1); + if (style.fillGradientType === TEXT_GRADIENT.LINEAR_VERTICAL) { // start the gradient at the top center of the canvas, and end at the bottom middle of the canvas @@ -532,15 +584,22 @@ // we need to repeat the gradient so that each individual line of text has the same vertical gradient effect // ['#FF0000', '#00FF00', '#0000FF'] over 2 lines would create stops at 0.125, 0.25, 0.375, 0.625, 0.75, 0.875 - totalIterations = (style.fill.length + 1) * lines.length; + totalIterations = (fill.length + 1) * lines.length; currentIteration = 0; for (let i = 0; i < lines.length; i++) { currentIteration += 1; - for (let j = 0; j < style.fill.length; j++) + for (let j = 0; j < fill.length; j++) { - stop = (currentIteration / totalIterations); - gradient.addColorStop(stop, style.fill[j]); + if (fillGradientStops[j]) + { + stop = (fillGradientStops[j] / lines.length) + (i / lines.length); + } + else + { + stop = currentIteration / totalIterations; + } + gradient.addColorStop(stop, fill[j]); currentIteration++; } } @@ -552,13 +611,20 @@ // can just evenly space out the gradients in this case, as multiple lines makes no difference // to an even left to right gradient - totalIterations = style.fill.length + 1; + totalIterations = fill.length + 1; currentIteration = 1; - for (let i = 0; i < style.fill.length; i++) + for (let i = 0; i < fill.length; i++) { - stop = currentIteration / totalIterations; - gradient.addColorStop(stop, style.fill[i]); + if (fillGradientStops[i]) + { + stop = fillGradientStops[i]; + } + else + { + stop = currentIteration / totalIterations; + } + gradient.addColorStop(stop, fill[i]); currentIteration++; } } @@ -569,7 +635,7 @@ /** * Destroys this text object. * Note* Unlike a Sprite, a Text object will automatically destroy its baseTexture and texture as - * the majorety of the time the texture will not be shared with any other Sprites. + * the majority of the time the texture will not be shared with any other Sprites. * * @param {object|boolean} [options] - Options parameter. A boolean will act as if all options * have been set to that value @@ -680,7 +746,7 @@ set text(text) // eslint-disable-line require-jsdoc { - text = String(text || ' '); + text = String(text === '' || text === null || text === undefined ? ' ' : text); if (this._text === text) { diff --git a/src/core/text/TextStyle.js b/src/core/text/TextStyle.js index 3b25528..890950b 100644 --- a/src/core/text/TextStyle.js +++ b/src/core/text/TextStyle.js @@ -8,12 +8,14 @@ align: 'left', breakWords: false, dropShadow: false, + dropShadowAlpha: 1, dropShadowAngle: Math.PI / 6, dropShadowBlur: 0, dropShadowColor: '#000000', dropShadowDistance: 5, fill: 'black', fillGradientType: TEXT_GRADIENT.LINEAR_VERTICAL, + fillGradientStops: [], fontFamily: 'Arial', fontSize: 26, fontStyle: 'normal', @@ -27,6 +29,7 @@ stroke: 'black', strokeThickness: 0, textBaseline: 'alphabetic', + trim: false, wordWrap: false, wordWrapWidth: 100, }; @@ -47,6 +50,7 @@ * @param {boolean} [style.breakWords=false] - Indicates if lines can be wrapped within words, it * needs wordWrap to be set to true * @param {boolean} [style.dropShadow=false] - Set a drop shadow for the text + * @param {number} [style.dropShadowAlpha=1] - Set alpha for the drop shadow * @param {number} [style.dropShadowAngle=Math.PI/6] - Set a angle of the drop shadow * @param {number} [style.dropShadowBlur=0] - Set a shadow blur radius * @param {string} [style.dropShadowColor='#000000'] - A fill style to be used on the dropshadow e.g 'red', '#00FF00' @@ -55,8 +59,10 @@ * fillstyle that will be used on the text e.g 'red', '#00FF00'. Can be an array to create a gradient * eg ['#000000','#FFFFFF'] * {@link https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/fillStyle|MDN} - * @param {number} [style.fillGradientType=PIXI.TEXT_GRADIENT.LINEAR_VERTICAL] - If fills styles are - * supplied, this can change the type/direction of the gradient. See {@link PIXI.TEXT_GRADIENT} for possible values + * @param {number} [style.fillGradientType=PIXI.TEXT_GRADIENT.LINEAR_VERTICAL] - If fill is an array of colours + * to create a gradient, this can change the type/direction of the gradient. See {@link PIXI.TEXT_GRADIENT} + * @param {number[]} [style.fillGradientStops] - If fill is an array of colours to create a gradient, this array can set + * the stop points (numbers between 0 and 1) for the color, overriding the default behaviour of evenly spacing them. * @param {string|string[]} [style.fontFamily='Arial'] - The font family * @param {number|string} [style.fontSize=26] - The font size (as a number it converts to px, but as a string, * equivalents are '26px','20pt','160%' or '1.6em') @@ -76,6 +82,7 @@ * e.g 'blue', '#FCFF00' * @param {number} [style.strokeThickness=0] - A number that represents the thickness of the stroke. * Default is 0 (no stroke) + * @param {boolean} [style.trim=false] - Trim transparent borders * @param {string} [style.textBaseline='alphabetic'] - The baseline of the text that is rendered. * @param {boolean} [style.wordWrap=false] - Indicates if word wrap should be used * @param {number} [style.wordWrapWidth=100] - The width at which text will wrap, it needs wordWrap to be set to true @@ -152,6 +159,19 @@ } } + get dropShadowAlpha() + { + return this._dropShadowAlpha; + } + set dropShadowAlpha(dropShadowAlpha) + { + if (this._dropShadowAlpha !== dropShadowAlpha) + { + this._dropShadowAlpha = dropShadowAlpha; + this.styleID++; + } + } + get dropShadowAngle() { return this._dropShadowAngle; @@ -232,6 +252,19 @@ } } + get fillGradientStops() + { + return this._fillGradientStops; + } + set fillGradientStops(fillGradientStops) + { + if (!areArraysEqual(this._fillGradientStops,fillGradientStops)) + { + this._fillGradientStops = fillGradientStops; + this.styleID++; + } + } + get fontFamily() { return this._fontFamily; @@ -402,6 +435,19 @@ } } + get trim() + { + return this._trim; + } + set trim(trim) + { + if (this._trim !== trim) + { + this._trim = trim; + this.styleID++; + } + } + get wordWrap() { return this._wordWrap; @@ -475,3 +521,34 @@ return color; } } + +/** + * Utility function to convert hexadecimal colors to strings, and simply return the color if it's a string. + * This version can also convert array of colors + * + * @param {Array} array1 First array to compare + * @param {Array} array2 Second array to compare + * @return {boolean} Do the arrays contain the same values in the same order + */ +function areArraysEqual(array1, array2) +{ + if (!Array.isArray(array1) || !Array.isArray(array2)) + { + return false; + } + + if (array1.length !== array2.length) + { + return false; + } + + for (let i = 0; i < array1.length; ++i) + { + if (array1[i] !== array2[i]) + { + return false; + } + } + + return true; +} diff --git a/src/core/textures/BaseRenderTexture.js b/src/core/textures/BaseRenderTexture.js index 5d50146..aa727f0 100644 --- a/src/core/textures/BaseRenderTexture.js +++ b/src/core/textures/BaseRenderTexture.js @@ -1,8 +1,6 @@ import BaseTexture from './BaseTexture'; import settings from '../settings'; -const { RESOLUTION, SCALE_MODE } = settings; - /** * A BaseRenderTexture is a special texture that allows any Pixi display object to be rendered to it. * @@ -54,7 +52,7 @@ { super(null, scaleMode); - this.resolution = resolution || RESOLUTION; + this.resolution = resolution || settings.RESOLUTION; this.width = width; this.height = height; @@ -62,7 +60,7 @@ this.realWidth = this.width * this.resolution; this.realHeight = this.height * this.resolution; - this.scaleMode = scaleMode || SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; this.hasLoaded = true; /** diff --git a/src/core/textures/BaseTexture.js b/src/core/textures/BaseTexture.js index 676dca3..aeccf39 100644 --- a/src/core/textures/BaseTexture.js +++ b/src/core/textures/BaseTexture.js @@ -77,7 +77,7 @@ * @default PIXI.settings.SCALE_MODE * @see PIXI.SCALE_MODES */ - this.scaleMode = scaleMode || settings.SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; /** * Set to true once the base texture has successfully loaded. @@ -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/RenderTexture.js b/src/core/textures/RenderTexture.js index 4c5034f..b3c6297 100644 --- a/src/core/textures/RenderTexture.js +++ b/src/core/textures/RenderTexture.js @@ -54,8 +54,8 @@ /* eslint-disable prefer-rest-params, no-console */ const width = arguments[1]; const height = arguments[2]; - const scaleMode = arguments[3] || 0; - const resolution = arguments[4] || 1; + const scaleMode = arguments[3]; + const resolution = arguments[4]; // we have an old render texture.. console.warn(`Please use RenderTexture.create(${width}, ${height}) instead of the ctor directly.`); 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 ca31456..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 @@ -459,7 +497,9 @@ if (frame.x + frame.width > this.baseTexture.width || frame.y + frame.height > this.baseTexture.height) { - throw new Error(`Texture Error: frame does not fit inside the base Texture dimensions ${this}`); + throw new Error('Texture Error: frame does not fit inside the base Texture dimensions: ' + + `X: ${frame.x} + ${frame.width} > ${this.baseTexture.width} ` + + `Y: ${frame.y} + ${frame.height} > ${this.baseTexture.height}`); } // this.valid = frame && frame.width && frame.height && this.baseTexture.source && this.baseTexture.hasLoaded; @@ -520,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. @@ -528,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 2a7497b..99e585a 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)); } /** @@ -360,3 +362,43 @@ * @private */ export const BaseTextureCache = {}; + +/** + * 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 1271fef..71ade15 100644 --- a/package.json +++ b/package.json @@ -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/scripts/release.js b/scripts/release.js index c4c8e36..156620c 100644 --- a/scripts/release.js +++ b/scripts/release.js @@ -12,7 +12,8 @@ 'dist/**/*', 'lib/**/*', 'src/**/*', - 'scripts/**/*', + 'scripts/*', + 'scripts/renders/*', 'test/**/*', '*.json', '*.md', diff --git a/scripts/renders/client.js b/scripts/renders/client.js index 16c9565..6faee34 100755 --- a/scripts/renders/client.js +++ b/scripts/renders/client.js @@ -1,6 +1,6 @@ 'use strict'; -require('../../bin/pixi'); +require('../../dist/pixi'); PIXI.utils.skipHello(); const fs = require('fs'); 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 bc730db..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. @@ -33,8 +33,11 @@ * need to call toDataUrl on the webgl context * @param {number} [options.resolution=1] - The resolution / device pixel ratio of the renderer, retina would be 2 * @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 @@ -43,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. */ @@ -73,7 +99,7 @@ */ stop() { - this.ticker.stop(); + this._ticker.stop(); } /** @@ -81,7 +107,7 @@ */ start() { - this.ticker.start(); + this._ticker.start(); } /** @@ -95,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 index 81e076b..78b17e6 100644 --- a/src/core/Shader.js +++ b/src/core/Shader.js @@ -1,9 +1,7 @@ import { GLShader } from 'pixi-gl-core'; import settings from './settings'; -const { PRECISION } = settings; - -function checkPrecision(src) +function checkPrecision(src, def) { if (src instanceof Array) { @@ -11,14 +9,14 @@ { const copy = src.slice(0); - copy.unshift(`precision ${PRECISION} float;`); + copy.unshift(`precision ${def} float;`); return copy; } } else if (src.substring(0, 9) !== 'precision') { - return `precision ${PRECISION} float;\n${src}`; + return `precision ${def} float;\n${src}`; } return src; @@ -42,6 +40,7 @@ */ constructor(gl, vertexSrc, fragmentSrc) { - super(gl, checkPrecision(vertexSrc), checkPrecision(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 3425b41..f9235e4 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -181,7 +181,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 b3792ef..17fde8d 100644 --- a/src/core/display/Container.js +++ b/src/core/display/Container.js @@ -73,13 +73,14 @@ } child.parent = this; - - // ensure a transform will be recalculated.. - this.transform._parentID = -1; - this._boundsID++; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.push(child); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(this.children.length - 1); child.emit('added', this); @@ -108,9 +109,14 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.splice(index, 0, child); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(index); child.emit('added', this); @@ -174,6 +180,7 @@ removeItems(this.children, currentIndex, 1); // remove from old position this.children.splice(index, 0, child); // add at new position + this.onChildrenChange(index); } @@ -220,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! @@ -244,9 +252,14 @@ { const child = this.getChildAt(index); + // ensure child transform will be recalculated.. child.parent = null; + child.transform._parentID = -1; removeItems(this.children, index, 1); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(index); child.emit('removed', this); @@ -275,8 +288,14 @@ for (let i = 0; i < removed.length; ++i) { removed[i].parent = null; + if (removed[i].transform) + { + removed[i].transform._parentID = -1; + } } + this._boundsID++; + this.onChildrenChange(beginIndex); for (let i = 0; i < removed.length; ++i) diff --git a/src/core/display/DisplayObject.js b/src/core/display/DisplayObject.js index 3affe36..4e6c77d 100644 --- a/src/core/display/DisplayObject.js +++ b/src/core/display/DisplayObject.js @@ -109,7 +109,7 @@ /** * The original, cached mask of the object * - * @member {PIXI.Rectangle} + * @member {PIXI.Graphics|PIXI.Sprite} * @private */ this._mask = null; diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index 95f2af4..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 ); @@ -1065,10 +1061,15 @@ canvasRenderer = new CanvasRenderer(); } - tempMatrix.tx = -bounds.x; - tempMatrix.ty = -bounds.y; + this.transform.updateLocalTransform(); + this.transform.localTransform.copy(tempMatrix); - canvasRenderer.render(this, canvasBuffer, false, tempMatrix); + tempMatrix.invert(); + + tempMatrix.tx -= bounds.x; + tempMatrix.ty -= bounds.y; + + canvasRenderer.render(this, canvasBuffer, true, tempMatrix); const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode); 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 e67680f..867dde2 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/buildCircle.js b/src/core/graphics/webgl/utils/buildCircle.js index 0400d13..abf5d02 100644 --- a/src/core/graphics/webgl/utils/buildCircle.js +++ b/src/core/graphics/webgl/utils/buildCircle.js @@ -33,6 +33,11 @@ height = circleData.height; } + if (width === 0 || height === 0) + { + return; + } + const totalSegs = Math.floor(30 * Math.sqrt(circleData.radius)) || Math.floor(15 * Math.sqrt(circleData.width + circleData.height)); diff --git a/src/core/graphics/webgl/utils/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 90155f9..f03fe15 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -30,6 +30,7 @@ export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; export { default as CanvasGraphicsRenderer } from './graphics/canvas/CanvasGraphicsRenderer'; +export { default as Spritesheet } from './textures/Spritesheet'; export { default as Texture } from './textures/Texture'; export { default as BaseTexture } from './textures/BaseTexture'; export { default as RenderTexture } from './textures/RenderTexture'; diff --git a/src/core/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 ccce680..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); } /** @@ -162,6 +162,9 @@ if (transform) { transform.copy(tempWt); + + // lets not forget to flag the parent transform as dirty... + this._tempDisplayObjectParent.transform._worldID = -1; } else { @@ -169,6 +172,7 @@ } displayObject.parent = this._tempDisplayObjectParent; + displayObject.updateTransform(); displayObject.parent = cacheParent; // displayObject.hitArea = //TODO add a temp hit area @@ -279,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/canvas/utils/CanvasRenderTarget.js b/src/core/renderers/canvas/utils/CanvasRenderTarget.js index 3de0aef..b409891 100644 --- a/src/core/renderers/canvas/utils/CanvasRenderTarget.js +++ b/src/core/renderers/canvas/utils/CanvasRenderTarget.js @@ -1,5 +1,4 @@ import settings from '../../../settings'; -const { RESOLUTION } = settings; /** * Creates a Canvas element of the given size. @@ -30,7 +29,7 @@ */ this.context = this.canvas.getContext('2d'); - this.resolution = resolution || RESOLUTION; + this.resolution = resolution || settings.RESOLUTION; this.resize(width, height); } 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 ce0e1c4..788ed6c 100644 --- a/src/core/renderers/webgl/WebGLRenderer.js +++ b/src/core/renderers/webgl/WebGLRenderer.js @@ -30,8 +30,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 @@ -49,10 +49,19 @@ * enable this if you need to call toDataUrl on the webgl context. * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when * rendering, stopping pixel interpolation. + * @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; + + if (this.legacy) + { + glCore.VertexArrayObject.FORCE_NATIVE = true; + } /** * The type of this renderer as a standardised const @@ -229,7 +238,7 @@ this.emit('context', gl); // setup the width/height properties and gl viewport - this.resize(this.width, this.height); + this.resize(this.screen.width, this.screen.height); } /** @@ -323,16 +332,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) { @@ -376,6 +385,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 @@ -440,9 +469,10 @@ * Changes the current shader to the one given in parameter * * @param {PIXI.Shader} shader - the new shader + * @param {boolean} [autoProject=true] - Whether automatically set the projection matrix * @return {PIXI.WebGLRenderer} Returns itself. */ - bindShader(shader) + bindShader(shader, autoProject) { // TODO cache if (this._activeShader !== shader) @@ -450,8 +480,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 bbab3b3..944f41d 100644 --- a/src/core/renderers/webgl/filters/Filter.js +++ b/src/core/renderers/webgl/filters/Filter.js @@ -1,6 +1,7 @@ import extractUniformsFromSrc from './extractUniformsFromSrc'; import { uid } from '../../../utils'; import { BLEND_MODES } from '../../../const'; +import settings from '../../../settings'; const SOURCE_KEY_MAP = {}; @@ -10,7 +11,7 @@ * @memberof PIXI * @extends PIXI.Shader */ -class Filter +export default class Filter { /** * @param {string} [vertexSrc] - The source of the vertex shader. @@ -35,8 +36,6 @@ this.blendMode = BLEND_MODES.NORMAL; - // pull out the vertex and shader uniforms if they are not specified.. - // currently this does not extract structs only default types this.uniformData = uniforms || extractUniformsFromSrc(this.vertexSrc, this.fragmentSrc, 'projectionMatrix|uSampler'); /** @@ -80,7 +79,7 @@ * * @member {number} */ - this.resolution = 1; + this.resolution = settings.RESOLUTION; /** * If enabled is true the filter is applied, if false it will not. @@ -97,8 +96,11 @@ * @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 { // --- // // this.uniforms.filterMatrix = filterManager.calculateSpriteMatrix(tempMatrix, window.panda ); @@ -170,5 +172,3 @@ ].join('\n'); } } - -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 dc7072a..6c7422d 100644 --- a/src/core/renderers/webgl/managers/FilterManager.js +++ b/src/core/renderers/webgl/managers/FilterManager.js @@ -154,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 @@ -176,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; @@ -184,7 +184,7 @@ flop = t; } - filters[i].apply(this, flip, lastState.renderTarget, true); + filters[i].apply(this, flip, lastState.renderTarget, false, currentState); this.freePotRenderTarget(flip); this.freePotRenderTarget(flop); @@ -291,9 +291,13 @@ let textureCount = 1; let currentState; - if (shader.uniforms.data.filterArea) + // 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; @@ -306,9 +310,9 @@ // use this to clamp displaced texture coords so they belong to filterArea // see displacementFilter fragment shader for an example - if (shader.uniforms.data.filterClamp) + if (shader.uniforms.filterClamp) { - currentState = this.filterData.stack[this.filterData.index]; + currentState = currentState || this.filterData.stack[this.filterData.index]; const filterClamp = shader.uniforms.filterClamp; @@ -485,7 +489,7 @@ */ destroy() { - this.shaderCache = []; + this.shaderCache = {}; this.emptyPool(); } diff --git a/src/core/renderers/webgl/utils/RenderTarget.js b/src/core/renderers/webgl/utils/RenderTarget.js index afe51ab..6d2467f 100644 --- a/src/core/renderers/webgl/utils/RenderTarget.js +++ b/src/core/renderers/webgl/utils/RenderTarget.js @@ -124,7 +124,7 @@ * @default PIXI.settings.SCALE_MODE * @see PIXI.SCALE_MODES */ - this.scaleMode = scaleMode || settings.SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; /** * Whether this object is the root element or not 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 36ea5bb..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; @@ -346,8 +346,8 @@ /** * Gets the local bounds of the sprite object. * - * @param {Rectangle} rect - The output rectangle. - * @return {Rectangle} The bounds. + * @param {PIXI.Rectangle} rect - The output rectangle. + * @return {PIXI.Rectangle} The bounds. */ getLocalBounds(rect) { @@ -440,7 +440,7 @@ * * @static * @param {number|string|PIXI.BaseTexture|HTMLCanvasElement|HTMLVideoElement} source Source to create texture from - * @return {PIXI.Texture} 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 e77b39a..90b08c0 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++) @@ -103,11 +102,18 @@ { const gl = this.renderer.gl; - // step 1: first check max textures the GPU can handle. - this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); + if (this.renderer.legacy) + { + this.MAX_TEXTURES = 1; + } + else + { + // step 1: first check max textures the GPU can handle. + this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); - // step 2: check the maximum number of if statements the shader can have too.. - this.MAX_TEXTURES = checkMaxIfStatmentsInShader(this.MAX_TEXTURES, gl); + // step 2: check the maximum number of if statements the shader can have too.. + this.MAX_TEXTURES = checkMaxIfStatmentsInShader(this.MAX_TEXTURES, gl); + } const shader = this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); @@ -130,8 +136,12 @@ .addIndex(this.indexBuffer) .addAttribute(this.vertexBuffers[i], shader.attributes.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) .addAttribute(this.vertexBuffers[i], shader.attributes.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) - .addAttribute(this.vertexBuffers[i], shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4) - .addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + .addAttribute(this.vertexBuffers[i], shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); + + if (shader.attributes.aTextureId) + { + this.vaos[i].addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + } /* eslint-enable max-len */ } @@ -378,8 +388,12 @@ .addIndex(this.indexBuffer) .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) - .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4) - .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); + + if (this.shader.attributes.aTextureId) + { + this.vaos[this.vertexCount].addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + } /* eslint-enable max-len */ } diff --git a/src/core/text/Text.js b/src/core/text/Text.js index 013625a..cec8c32 100644 --- a/src/core/text/Text.js +++ b/src/core/text/Text.js @@ -6,6 +6,7 @@ import { TEXT_GRADIENT } from '../const'; import settings from '../settings'; import TextStyle from './TextStyle'; +import trimCanvas from '../utils/trimCanvas'; const defaultDestroyOptions = { texture: true, @@ -57,7 +58,7 @@ /** * The canvas 2d context that everything is drawn with - * @member {HTMLCanvasElement} + * @member {CanvasRenderingContext2D} */ this.context = this.canvas.getContext('2d'); @@ -189,10 +190,12 @@ if (style.dropShadow) { + this.context.shadowBlur = style.dropShadowBlur; + this.context.globalAlpha = style.dropShadowAlpha; + if (style.dropShadowBlur > 0) { this.context.shadowColor = style.dropShadowColor; - this.context.shadowBlur = style.dropShadowBlur; } else { @@ -237,6 +240,10 @@ } } + // reset the shadow blur and alpha that was set by the drop shadow, for the regular text + this.context.shadowBlur = 0; + this.context.globalAlpha = 1; + // set canvas text styles this.context.fillStyle = this._generateFillStyle(style, lines); @@ -326,6 +333,15 @@ */ updateTexture() { + if (this._style.trim) + { + const trimmed = trimCanvas(this.canvas); + + this.canvas.width = trimmed.width; + this.canvas.height = trimmed.height; + this.context.putImageData(trimmed.data, 0, 0); + } + const texture = this._texture; const style = this._style; @@ -475,6 +491,19 @@ } /** + * Gets the local bounds of the text object. + * + * @param {Rectangle} rect - The output rectangle. + * @return {Rectangle} The bounds. + */ + getLocalBounds(rect) + { + this.updateText(true); + + return super.getLocalBounds.call(this, rect); + } + + /** * calculates the bounds of the Text as a rectangle. The bounds calculation takes the worldTransform into account. */ _calculateBounds() @@ -525,6 +554,29 @@ const width = this.canvas.width / this.resolution; const height = this.canvas.height / this.resolution; + // make a copy of the style settings, so we can manipulate them later + const fill = style.fill.slice(); + const fillGradientStops = style.fillGradientStops.slice(); + + // wanting to evenly distribute the fills. So an array of 4 colours should give fills of 0.25, 0.5 and 0.75 + if (!fillGradientStops.length) + { + const lengthPlus1 = fill.length + 1; + + for (let i = 1; i < lengthPlus1; ++i) + { + fillGradientStops.push(i / lengthPlus1); + } + } + + // stop the bleeding of the last gradient on the line above to the top gradient of the this line + // by hard defining the first gradient colour at point 0, and last gradient colour at point 1 + fill.unshift(style.fill[0]); + fillGradientStops.unshift(0); + + fill.push(style.fill[style.fill.length - 1]); + fillGradientStops.push(1); + if (style.fillGradientType === TEXT_GRADIENT.LINEAR_VERTICAL) { // start the gradient at the top center of the canvas, and end at the bottom middle of the canvas @@ -532,15 +584,22 @@ // we need to repeat the gradient so that each individual line of text has the same vertical gradient effect // ['#FF0000', '#00FF00', '#0000FF'] over 2 lines would create stops at 0.125, 0.25, 0.375, 0.625, 0.75, 0.875 - totalIterations = (style.fill.length + 1) * lines.length; + totalIterations = (fill.length + 1) * lines.length; currentIteration = 0; for (let i = 0; i < lines.length; i++) { currentIteration += 1; - for (let j = 0; j < style.fill.length; j++) + for (let j = 0; j < fill.length; j++) { - stop = (currentIteration / totalIterations); - gradient.addColorStop(stop, style.fill[j]); + if (fillGradientStops[j]) + { + stop = (fillGradientStops[j] / lines.length) + (i / lines.length); + } + else + { + stop = currentIteration / totalIterations; + } + gradient.addColorStop(stop, fill[j]); currentIteration++; } } @@ -552,13 +611,20 @@ // can just evenly space out the gradients in this case, as multiple lines makes no difference // to an even left to right gradient - totalIterations = style.fill.length + 1; + totalIterations = fill.length + 1; currentIteration = 1; - for (let i = 0; i < style.fill.length; i++) + for (let i = 0; i < fill.length; i++) { - stop = currentIteration / totalIterations; - gradient.addColorStop(stop, style.fill[i]); + if (fillGradientStops[i]) + { + stop = fillGradientStops[i]; + } + else + { + stop = currentIteration / totalIterations; + } + gradient.addColorStop(stop, fill[i]); currentIteration++; } } @@ -569,7 +635,7 @@ /** * Destroys this text object. * Note* Unlike a Sprite, a Text object will automatically destroy its baseTexture and texture as - * the majorety of the time the texture will not be shared with any other Sprites. + * the majority of the time the texture will not be shared with any other Sprites. * * @param {object|boolean} [options] - Options parameter. A boolean will act as if all options * have been set to that value @@ -680,7 +746,7 @@ set text(text) // eslint-disable-line require-jsdoc { - text = String(text || ' '); + text = String(text === '' || text === null || text === undefined ? ' ' : text); if (this._text === text) { diff --git a/src/core/text/TextStyle.js b/src/core/text/TextStyle.js index 3b25528..890950b 100644 --- a/src/core/text/TextStyle.js +++ b/src/core/text/TextStyle.js @@ -8,12 +8,14 @@ align: 'left', breakWords: false, dropShadow: false, + dropShadowAlpha: 1, dropShadowAngle: Math.PI / 6, dropShadowBlur: 0, dropShadowColor: '#000000', dropShadowDistance: 5, fill: 'black', fillGradientType: TEXT_GRADIENT.LINEAR_VERTICAL, + fillGradientStops: [], fontFamily: 'Arial', fontSize: 26, fontStyle: 'normal', @@ -27,6 +29,7 @@ stroke: 'black', strokeThickness: 0, textBaseline: 'alphabetic', + trim: false, wordWrap: false, wordWrapWidth: 100, }; @@ -47,6 +50,7 @@ * @param {boolean} [style.breakWords=false] - Indicates if lines can be wrapped within words, it * needs wordWrap to be set to true * @param {boolean} [style.dropShadow=false] - Set a drop shadow for the text + * @param {number} [style.dropShadowAlpha=1] - Set alpha for the drop shadow * @param {number} [style.dropShadowAngle=Math.PI/6] - Set a angle of the drop shadow * @param {number} [style.dropShadowBlur=0] - Set a shadow blur radius * @param {string} [style.dropShadowColor='#000000'] - A fill style to be used on the dropshadow e.g 'red', '#00FF00' @@ -55,8 +59,10 @@ * fillstyle that will be used on the text e.g 'red', '#00FF00'. Can be an array to create a gradient * eg ['#000000','#FFFFFF'] * {@link https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/fillStyle|MDN} - * @param {number} [style.fillGradientType=PIXI.TEXT_GRADIENT.LINEAR_VERTICAL] - If fills styles are - * supplied, this can change the type/direction of the gradient. See {@link PIXI.TEXT_GRADIENT} for possible values + * @param {number} [style.fillGradientType=PIXI.TEXT_GRADIENT.LINEAR_VERTICAL] - If fill is an array of colours + * to create a gradient, this can change the type/direction of the gradient. See {@link PIXI.TEXT_GRADIENT} + * @param {number[]} [style.fillGradientStops] - If fill is an array of colours to create a gradient, this array can set + * the stop points (numbers between 0 and 1) for the color, overriding the default behaviour of evenly spacing them. * @param {string|string[]} [style.fontFamily='Arial'] - The font family * @param {number|string} [style.fontSize=26] - The font size (as a number it converts to px, but as a string, * equivalents are '26px','20pt','160%' or '1.6em') @@ -76,6 +82,7 @@ * e.g 'blue', '#FCFF00' * @param {number} [style.strokeThickness=0] - A number that represents the thickness of the stroke. * Default is 0 (no stroke) + * @param {boolean} [style.trim=false] - Trim transparent borders * @param {string} [style.textBaseline='alphabetic'] - The baseline of the text that is rendered. * @param {boolean} [style.wordWrap=false] - Indicates if word wrap should be used * @param {number} [style.wordWrapWidth=100] - The width at which text will wrap, it needs wordWrap to be set to true @@ -152,6 +159,19 @@ } } + get dropShadowAlpha() + { + return this._dropShadowAlpha; + } + set dropShadowAlpha(dropShadowAlpha) + { + if (this._dropShadowAlpha !== dropShadowAlpha) + { + this._dropShadowAlpha = dropShadowAlpha; + this.styleID++; + } + } + get dropShadowAngle() { return this._dropShadowAngle; @@ -232,6 +252,19 @@ } } + get fillGradientStops() + { + return this._fillGradientStops; + } + set fillGradientStops(fillGradientStops) + { + if (!areArraysEqual(this._fillGradientStops,fillGradientStops)) + { + this._fillGradientStops = fillGradientStops; + this.styleID++; + } + } + get fontFamily() { return this._fontFamily; @@ -402,6 +435,19 @@ } } + get trim() + { + return this._trim; + } + set trim(trim) + { + if (this._trim !== trim) + { + this._trim = trim; + this.styleID++; + } + } + get wordWrap() { return this._wordWrap; @@ -475,3 +521,34 @@ return color; } } + +/** + * Utility function to convert hexadecimal colors to strings, and simply return the color if it's a string. + * This version can also convert array of colors + * + * @param {Array} array1 First array to compare + * @param {Array} array2 Second array to compare + * @return {boolean} Do the arrays contain the same values in the same order + */ +function areArraysEqual(array1, array2) +{ + if (!Array.isArray(array1) || !Array.isArray(array2)) + { + return false; + } + + if (array1.length !== array2.length) + { + return false; + } + + for (let i = 0; i < array1.length; ++i) + { + if (array1[i] !== array2[i]) + { + return false; + } + } + + return true; +} diff --git a/src/core/textures/BaseRenderTexture.js b/src/core/textures/BaseRenderTexture.js index 5d50146..aa727f0 100644 --- a/src/core/textures/BaseRenderTexture.js +++ b/src/core/textures/BaseRenderTexture.js @@ -1,8 +1,6 @@ import BaseTexture from './BaseTexture'; import settings from '../settings'; -const { RESOLUTION, SCALE_MODE } = settings; - /** * A BaseRenderTexture is a special texture that allows any Pixi display object to be rendered to it. * @@ -54,7 +52,7 @@ { super(null, scaleMode); - this.resolution = resolution || RESOLUTION; + this.resolution = resolution || settings.RESOLUTION; this.width = width; this.height = height; @@ -62,7 +60,7 @@ this.realWidth = this.width * this.resolution; this.realHeight = this.height * this.resolution; - this.scaleMode = scaleMode || SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; this.hasLoaded = true; /** diff --git a/src/core/textures/BaseTexture.js b/src/core/textures/BaseTexture.js index 676dca3..aeccf39 100644 --- a/src/core/textures/BaseTexture.js +++ b/src/core/textures/BaseTexture.js @@ -77,7 +77,7 @@ * @default PIXI.settings.SCALE_MODE * @see PIXI.SCALE_MODES */ - this.scaleMode = scaleMode || settings.SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; /** * Set to true once the base texture has successfully loaded. @@ -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/RenderTexture.js b/src/core/textures/RenderTexture.js index 4c5034f..b3c6297 100644 --- a/src/core/textures/RenderTexture.js +++ b/src/core/textures/RenderTexture.js @@ -54,8 +54,8 @@ /* eslint-disable prefer-rest-params, no-console */ const width = arguments[1]; const height = arguments[2]; - const scaleMode = arguments[3] || 0; - const resolution = arguments[4] || 1; + const scaleMode = arguments[3]; + const resolution = arguments[4]; // we have an old render texture.. console.warn(`Please use RenderTexture.create(${width}, ${height}) instead of the ctor directly.`); 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 ca31456..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 @@ -459,7 +497,9 @@ if (frame.x + frame.width > this.baseTexture.width || frame.y + frame.height > this.baseTexture.height) { - throw new Error(`Texture Error: frame does not fit inside the base Texture dimensions ${this}`); + throw new Error('Texture Error: frame does not fit inside the base Texture dimensions: ' + + `X: ${frame.x} + ${frame.width} > ${this.baseTexture.width} ` + + `Y: ${frame.y} + ${frame.height} > ${this.baseTexture.height}`); } // this.valid = frame && frame.width && frame.height && this.baseTexture.source && this.baseTexture.hasLoaded; @@ -520,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. @@ -528,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 2a7497b..99e585a 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)); } /** @@ -360,3 +362,43 @@ * @private */ export const BaseTextureCache = {}; + +/** + * 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/core/utils/trimCanvas.js b/src/core/utils/trimCanvas.js new file mode 100644 index 0000000..5af6de0 --- /dev/null +++ b/src/core/utils/trimCanvas.js @@ -0,0 +1,83 @@ +/** + * Trim transparent borders from a canvas + * + * @memberof PIXI + * @function trimCanvas + * @private + * @param {HTMLCanvasElement} canvas - the canvas to trim + * @returns {object} Trim data + */ +export default function trimCanvas(canvas) +{ + // https://gist.github.com/remy/784508 + + let width = canvas.width; + let height = canvas.height; + + const context = canvas.getContext('2d'); + const imageData = context.getImageData(0, 0, width, height); + const pixels = imageData.data; + const len = pixels.length; + + const bound = { + top: null, + left: null, + right: null, + bottom: null, + }; + let i; + let x; + let y; + + for (i = 0; i < len; i += 4) + { + if (pixels[i + 3] !== 0) + { + x = (i / 4) % width; + y = ~~((i / 4) / width); + + if (bound.top === null) + { + bound.top = y; + } + + if (bound.left === null) + { + bound.left = x; + } + else if (x < bound.left) + { + bound.left = x; + } + + if (bound.right === null) + { + bound.right = x + 1; + } + else if (bound.right < x) + { + bound.right = x + 1; + } + + if (bound.bottom === null) + { + bound.bottom = y; + } + else if (bound.bottom < y) + { + bound.bottom = y; + } + } + } + + width = bound.right - bound.left; + height = bound.bottom - bound.top + 1; + + const data = context.getImageData(bound.left, bound.top, width, height); + + return { + height, + width, + data, + }; +} diff --git a/package.json b/package.json index 1271fef..71ade15 100644 --- a/package.json +++ b/package.json @@ -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/scripts/release.js b/scripts/release.js index c4c8e36..156620c 100644 --- a/scripts/release.js +++ b/scripts/release.js @@ -12,7 +12,8 @@ 'dist/**/*', 'lib/**/*', 'src/**/*', - 'scripts/**/*', + 'scripts/*', + 'scripts/renders/*', 'test/**/*', '*.json', '*.md', diff --git a/scripts/renders/client.js b/scripts/renders/client.js index 16c9565..6faee34 100755 --- a/scripts/renders/client.js +++ b/scripts/renders/client.js @@ -1,6 +1,6 @@ 'use strict'; -require('../../bin/pixi'); +require('../../dist/pixi'); PIXI.utils.skipHello(); const fs = require('fs'); 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 bc730db..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. @@ -33,8 +33,11 @@ * need to call toDataUrl on the webgl context * @param {number} [options.resolution=1] - The resolution / device pixel ratio of the renderer, retina would be 2 * @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 @@ -43,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. */ @@ -73,7 +99,7 @@ */ stop() { - this.ticker.stop(); + this._ticker.stop(); } /** @@ -81,7 +107,7 @@ */ start() { - this.ticker.start(); + this._ticker.start(); } /** @@ -95,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 index 81e076b..78b17e6 100644 --- a/src/core/Shader.js +++ b/src/core/Shader.js @@ -1,9 +1,7 @@ import { GLShader } from 'pixi-gl-core'; import settings from './settings'; -const { PRECISION } = settings; - -function checkPrecision(src) +function checkPrecision(src, def) { if (src instanceof Array) { @@ -11,14 +9,14 @@ { const copy = src.slice(0); - copy.unshift(`precision ${PRECISION} float;`); + copy.unshift(`precision ${def} float;`); return copy; } } else if (src.substring(0, 9) !== 'precision') { - return `precision ${PRECISION} float;\n${src}`; + return `precision ${def} float;\n${src}`; } return src; @@ -42,6 +40,7 @@ */ constructor(gl, vertexSrc, fragmentSrc) { - super(gl, checkPrecision(vertexSrc), checkPrecision(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 3425b41..f9235e4 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -181,7 +181,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 b3792ef..17fde8d 100644 --- a/src/core/display/Container.js +++ b/src/core/display/Container.js @@ -73,13 +73,14 @@ } child.parent = this; - - // ensure a transform will be recalculated.. - this.transform._parentID = -1; - this._boundsID++; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.push(child); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(this.children.length - 1); child.emit('added', this); @@ -108,9 +109,14 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.splice(index, 0, child); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(index); child.emit('added', this); @@ -174,6 +180,7 @@ removeItems(this.children, currentIndex, 1); // remove from old position this.children.splice(index, 0, child); // add at new position + this.onChildrenChange(index); } @@ -220,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! @@ -244,9 +252,14 @@ { const child = this.getChildAt(index); + // ensure child transform will be recalculated.. child.parent = null; + child.transform._parentID = -1; removeItems(this.children, index, 1); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(index); child.emit('removed', this); @@ -275,8 +288,14 @@ for (let i = 0; i < removed.length; ++i) { removed[i].parent = null; + if (removed[i].transform) + { + removed[i].transform._parentID = -1; + } } + this._boundsID++; + this.onChildrenChange(beginIndex); for (let i = 0; i < removed.length; ++i) diff --git a/src/core/display/DisplayObject.js b/src/core/display/DisplayObject.js index 3affe36..4e6c77d 100644 --- a/src/core/display/DisplayObject.js +++ b/src/core/display/DisplayObject.js @@ -109,7 +109,7 @@ /** * The original, cached mask of the object * - * @member {PIXI.Rectangle} + * @member {PIXI.Graphics|PIXI.Sprite} * @private */ this._mask = null; diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index 95f2af4..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 ); @@ -1065,10 +1061,15 @@ canvasRenderer = new CanvasRenderer(); } - tempMatrix.tx = -bounds.x; - tempMatrix.ty = -bounds.y; + this.transform.updateLocalTransform(); + this.transform.localTransform.copy(tempMatrix); - canvasRenderer.render(this, canvasBuffer, false, tempMatrix); + tempMatrix.invert(); + + tempMatrix.tx -= bounds.x; + tempMatrix.ty -= bounds.y; + + canvasRenderer.render(this, canvasBuffer, true, tempMatrix); const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode); 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 e67680f..867dde2 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/buildCircle.js b/src/core/graphics/webgl/utils/buildCircle.js index 0400d13..abf5d02 100644 --- a/src/core/graphics/webgl/utils/buildCircle.js +++ b/src/core/graphics/webgl/utils/buildCircle.js @@ -33,6 +33,11 @@ height = circleData.height; } + if (width === 0 || height === 0) + { + return; + } + const totalSegs = Math.floor(30 * Math.sqrt(circleData.radius)) || Math.floor(15 * Math.sqrt(circleData.width + circleData.height)); diff --git a/src/core/graphics/webgl/utils/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 90155f9..f03fe15 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -30,6 +30,7 @@ export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; export { default as CanvasGraphicsRenderer } from './graphics/canvas/CanvasGraphicsRenderer'; +export { default as Spritesheet } from './textures/Spritesheet'; export { default as Texture } from './textures/Texture'; export { default as BaseTexture } from './textures/BaseTexture'; export { default as RenderTexture } from './textures/RenderTexture'; diff --git a/src/core/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 ccce680..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); } /** @@ -162,6 +162,9 @@ if (transform) { transform.copy(tempWt); + + // lets not forget to flag the parent transform as dirty... + this._tempDisplayObjectParent.transform._worldID = -1; } else { @@ -169,6 +172,7 @@ } displayObject.parent = this._tempDisplayObjectParent; + displayObject.updateTransform(); displayObject.parent = cacheParent; // displayObject.hitArea = //TODO add a temp hit area @@ -279,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/canvas/utils/CanvasRenderTarget.js b/src/core/renderers/canvas/utils/CanvasRenderTarget.js index 3de0aef..b409891 100644 --- a/src/core/renderers/canvas/utils/CanvasRenderTarget.js +++ b/src/core/renderers/canvas/utils/CanvasRenderTarget.js @@ -1,5 +1,4 @@ import settings from '../../../settings'; -const { RESOLUTION } = settings; /** * Creates a Canvas element of the given size. @@ -30,7 +29,7 @@ */ this.context = this.canvas.getContext('2d'); - this.resolution = resolution || RESOLUTION; + this.resolution = resolution || settings.RESOLUTION; this.resize(width, height); } 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 ce0e1c4..788ed6c 100644 --- a/src/core/renderers/webgl/WebGLRenderer.js +++ b/src/core/renderers/webgl/WebGLRenderer.js @@ -30,8 +30,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 @@ -49,10 +49,19 @@ * enable this if you need to call toDataUrl on the webgl context. * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when * rendering, stopping pixel interpolation. + * @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; + + if (this.legacy) + { + glCore.VertexArrayObject.FORCE_NATIVE = true; + } /** * The type of this renderer as a standardised const @@ -229,7 +238,7 @@ this.emit('context', gl); // setup the width/height properties and gl viewport - this.resize(this.width, this.height); + this.resize(this.screen.width, this.screen.height); } /** @@ -323,16 +332,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) { @@ -376,6 +385,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 @@ -440,9 +469,10 @@ * Changes the current shader to the one given in parameter * * @param {PIXI.Shader} shader - the new shader + * @param {boolean} [autoProject=true] - Whether automatically set the projection matrix * @return {PIXI.WebGLRenderer} Returns itself. */ - bindShader(shader) + bindShader(shader, autoProject) { // TODO cache if (this._activeShader !== shader) @@ -450,8 +480,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 bbab3b3..944f41d 100644 --- a/src/core/renderers/webgl/filters/Filter.js +++ b/src/core/renderers/webgl/filters/Filter.js @@ -1,6 +1,7 @@ import extractUniformsFromSrc from './extractUniformsFromSrc'; import { uid } from '../../../utils'; import { BLEND_MODES } from '../../../const'; +import settings from '../../../settings'; const SOURCE_KEY_MAP = {}; @@ -10,7 +11,7 @@ * @memberof PIXI * @extends PIXI.Shader */ -class Filter +export default class Filter { /** * @param {string} [vertexSrc] - The source of the vertex shader. @@ -35,8 +36,6 @@ this.blendMode = BLEND_MODES.NORMAL; - // pull out the vertex and shader uniforms if they are not specified.. - // currently this does not extract structs only default types this.uniformData = uniforms || extractUniformsFromSrc(this.vertexSrc, this.fragmentSrc, 'projectionMatrix|uSampler'); /** @@ -80,7 +79,7 @@ * * @member {number} */ - this.resolution = 1; + this.resolution = settings.RESOLUTION; /** * If enabled is true the filter is applied, if false it will not. @@ -97,8 +96,11 @@ * @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 { // --- // // this.uniforms.filterMatrix = filterManager.calculateSpriteMatrix(tempMatrix, window.panda ); @@ -170,5 +172,3 @@ ].join('\n'); } } - -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 dc7072a..6c7422d 100644 --- a/src/core/renderers/webgl/managers/FilterManager.js +++ b/src/core/renderers/webgl/managers/FilterManager.js @@ -154,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 @@ -176,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; @@ -184,7 +184,7 @@ flop = t; } - filters[i].apply(this, flip, lastState.renderTarget, true); + filters[i].apply(this, flip, lastState.renderTarget, false, currentState); this.freePotRenderTarget(flip); this.freePotRenderTarget(flop); @@ -291,9 +291,13 @@ let textureCount = 1; let currentState; - if (shader.uniforms.data.filterArea) + // 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; @@ -306,9 +310,9 @@ // use this to clamp displaced texture coords so they belong to filterArea // see displacementFilter fragment shader for an example - if (shader.uniforms.data.filterClamp) + if (shader.uniforms.filterClamp) { - currentState = this.filterData.stack[this.filterData.index]; + currentState = currentState || this.filterData.stack[this.filterData.index]; const filterClamp = shader.uniforms.filterClamp; @@ -485,7 +489,7 @@ */ destroy() { - this.shaderCache = []; + this.shaderCache = {}; this.emptyPool(); } diff --git a/src/core/renderers/webgl/utils/RenderTarget.js b/src/core/renderers/webgl/utils/RenderTarget.js index afe51ab..6d2467f 100644 --- a/src/core/renderers/webgl/utils/RenderTarget.js +++ b/src/core/renderers/webgl/utils/RenderTarget.js @@ -124,7 +124,7 @@ * @default PIXI.settings.SCALE_MODE * @see PIXI.SCALE_MODES */ - this.scaleMode = scaleMode || settings.SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; /** * Whether this object is the root element or not 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 36ea5bb..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; @@ -346,8 +346,8 @@ /** * Gets the local bounds of the sprite object. * - * @param {Rectangle} rect - The output rectangle. - * @return {Rectangle} The bounds. + * @param {PIXI.Rectangle} rect - The output rectangle. + * @return {PIXI.Rectangle} The bounds. */ getLocalBounds(rect) { @@ -440,7 +440,7 @@ * * @static * @param {number|string|PIXI.BaseTexture|HTMLCanvasElement|HTMLVideoElement} source Source to create texture from - * @return {PIXI.Texture} 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 e77b39a..90b08c0 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++) @@ -103,11 +102,18 @@ { const gl = this.renderer.gl; - // step 1: first check max textures the GPU can handle. - this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); + if (this.renderer.legacy) + { + this.MAX_TEXTURES = 1; + } + else + { + // step 1: first check max textures the GPU can handle. + this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); - // step 2: check the maximum number of if statements the shader can have too.. - this.MAX_TEXTURES = checkMaxIfStatmentsInShader(this.MAX_TEXTURES, gl); + // step 2: check the maximum number of if statements the shader can have too.. + this.MAX_TEXTURES = checkMaxIfStatmentsInShader(this.MAX_TEXTURES, gl); + } const shader = this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); @@ -130,8 +136,12 @@ .addIndex(this.indexBuffer) .addAttribute(this.vertexBuffers[i], shader.attributes.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) .addAttribute(this.vertexBuffers[i], shader.attributes.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) - .addAttribute(this.vertexBuffers[i], shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4) - .addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + .addAttribute(this.vertexBuffers[i], shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); + + if (shader.attributes.aTextureId) + { + this.vaos[i].addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + } /* eslint-enable max-len */ } @@ -378,8 +388,12 @@ .addIndex(this.indexBuffer) .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) - .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4) - .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); + + if (this.shader.attributes.aTextureId) + { + this.vaos[this.vertexCount].addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + } /* eslint-enable max-len */ } diff --git a/src/core/text/Text.js b/src/core/text/Text.js index 013625a..cec8c32 100644 --- a/src/core/text/Text.js +++ b/src/core/text/Text.js @@ -6,6 +6,7 @@ import { TEXT_GRADIENT } from '../const'; import settings from '../settings'; import TextStyle from './TextStyle'; +import trimCanvas from '../utils/trimCanvas'; const defaultDestroyOptions = { texture: true, @@ -57,7 +58,7 @@ /** * The canvas 2d context that everything is drawn with - * @member {HTMLCanvasElement} + * @member {CanvasRenderingContext2D} */ this.context = this.canvas.getContext('2d'); @@ -189,10 +190,12 @@ if (style.dropShadow) { + this.context.shadowBlur = style.dropShadowBlur; + this.context.globalAlpha = style.dropShadowAlpha; + if (style.dropShadowBlur > 0) { this.context.shadowColor = style.dropShadowColor; - this.context.shadowBlur = style.dropShadowBlur; } else { @@ -237,6 +240,10 @@ } } + // reset the shadow blur and alpha that was set by the drop shadow, for the regular text + this.context.shadowBlur = 0; + this.context.globalAlpha = 1; + // set canvas text styles this.context.fillStyle = this._generateFillStyle(style, lines); @@ -326,6 +333,15 @@ */ updateTexture() { + if (this._style.trim) + { + const trimmed = trimCanvas(this.canvas); + + this.canvas.width = trimmed.width; + this.canvas.height = trimmed.height; + this.context.putImageData(trimmed.data, 0, 0); + } + const texture = this._texture; const style = this._style; @@ -475,6 +491,19 @@ } /** + * Gets the local bounds of the text object. + * + * @param {Rectangle} rect - The output rectangle. + * @return {Rectangle} The bounds. + */ + getLocalBounds(rect) + { + this.updateText(true); + + return super.getLocalBounds.call(this, rect); + } + + /** * calculates the bounds of the Text as a rectangle. The bounds calculation takes the worldTransform into account. */ _calculateBounds() @@ -525,6 +554,29 @@ const width = this.canvas.width / this.resolution; const height = this.canvas.height / this.resolution; + // make a copy of the style settings, so we can manipulate them later + const fill = style.fill.slice(); + const fillGradientStops = style.fillGradientStops.slice(); + + // wanting to evenly distribute the fills. So an array of 4 colours should give fills of 0.25, 0.5 and 0.75 + if (!fillGradientStops.length) + { + const lengthPlus1 = fill.length + 1; + + for (let i = 1; i < lengthPlus1; ++i) + { + fillGradientStops.push(i / lengthPlus1); + } + } + + // stop the bleeding of the last gradient on the line above to the top gradient of the this line + // by hard defining the first gradient colour at point 0, and last gradient colour at point 1 + fill.unshift(style.fill[0]); + fillGradientStops.unshift(0); + + fill.push(style.fill[style.fill.length - 1]); + fillGradientStops.push(1); + if (style.fillGradientType === TEXT_GRADIENT.LINEAR_VERTICAL) { // start the gradient at the top center of the canvas, and end at the bottom middle of the canvas @@ -532,15 +584,22 @@ // we need to repeat the gradient so that each individual line of text has the same vertical gradient effect // ['#FF0000', '#00FF00', '#0000FF'] over 2 lines would create stops at 0.125, 0.25, 0.375, 0.625, 0.75, 0.875 - totalIterations = (style.fill.length + 1) * lines.length; + totalIterations = (fill.length + 1) * lines.length; currentIteration = 0; for (let i = 0; i < lines.length; i++) { currentIteration += 1; - for (let j = 0; j < style.fill.length; j++) + for (let j = 0; j < fill.length; j++) { - stop = (currentIteration / totalIterations); - gradient.addColorStop(stop, style.fill[j]); + if (fillGradientStops[j]) + { + stop = (fillGradientStops[j] / lines.length) + (i / lines.length); + } + else + { + stop = currentIteration / totalIterations; + } + gradient.addColorStop(stop, fill[j]); currentIteration++; } } @@ -552,13 +611,20 @@ // can just evenly space out the gradients in this case, as multiple lines makes no difference // to an even left to right gradient - totalIterations = style.fill.length + 1; + totalIterations = fill.length + 1; currentIteration = 1; - for (let i = 0; i < style.fill.length; i++) + for (let i = 0; i < fill.length; i++) { - stop = currentIteration / totalIterations; - gradient.addColorStop(stop, style.fill[i]); + if (fillGradientStops[i]) + { + stop = fillGradientStops[i]; + } + else + { + stop = currentIteration / totalIterations; + } + gradient.addColorStop(stop, fill[i]); currentIteration++; } } @@ -569,7 +635,7 @@ /** * Destroys this text object. * Note* Unlike a Sprite, a Text object will automatically destroy its baseTexture and texture as - * the majorety of the time the texture will not be shared with any other Sprites. + * the majority of the time the texture will not be shared with any other Sprites. * * @param {object|boolean} [options] - Options parameter. A boolean will act as if all options * have been set to that value @@ -680,7 +746,7 @@ set text(text) // eslint-disable-line require-jsdoc { - text = String(text || ' '); + text = String(text === '' || text === null || text === undefined ? ' ' : text); if (this._text === text) { diff --git a/src/core/text/TextStyle.js b/src/core/text/TextStyle.js index 3b25528..890950b 100644 --- a/src/core/text/TextStyle.js +++ b/src/core/text/TextStyle.js @@ -8,12 +8,14 @@ align: 'left', breakWords: false, dropShadow: false, + dropShadowAlpha: 1, dropShadowAngle: Math.PI / 6, dropShadowBlur: 0, dropShadowColor: '#000000', dropShadowDistance: 5, fill: 'black', fillGradientType: TEXT_GRADIENT.LINEAR_VERTICAL, + fillGradientStops: [], fontFamily: 'Arial', fontSize: 26, fontStyle: 'normal', @@ -27,6 +29,7 @@ stroke: 'black', strokeThickness: 0, textBaseline: 'alphabetic', + trim: false, wordWrap: false, wordWrapWidth: 100, }; @@ -47,6 +50,7 @@ * @param {boolean} [style.breakWords=false] - Indicates if lines can be wrapped within words, it * needs wordWrap to be set to true * @param {boolean} [style.dropShadow=false] - Set a drop shadow for the text + * @param {number} [style.dropShadowAlpha=1] - Set alpha for the drop shadow * @param {number} [style.dropShadowAngle=Math.PI/6] - Set a angle of the drop shadow * @param {number} [style.dropShadowBlur=0] - Set a shadow blur radius * @param {string} [style.dropShadowColor='#000000'] - A fill style to be used on the dropshadow e.g 'red', '#00FF00' @@ -55,8 +59,10 @@ * fillstyle that will be used on the text e.g 'red', '#00FF00'. Can be an array to create a gradient * eg ['#000000','#FFFFFF'] * {@link https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/fillStyle|MDN} - * @param {number} [style.fillGradientType=PIXI.TEXT_GRADIENT.LINEAR_VERTICAL] - If fills styles are - * supplied, this can change the type/direction of the gradient. See {@link PIXI.TEXT_GRADIENT} for possible values + * @param {number} [style.fillGradientType=PIXI.TEXT_GRADIENT.LINEAR_VERTICAL] - If fill is an array of colours + * to create a gradient, this can change the type/direction of the gradient. See {@link PIXI.TEXT_GRADIENT} + * @param {number[]} [style.fillGradientStops] - If fill is an array of colours to create a gradient, this array can set + * the stop points (numbers between 0 and 1) for the color, overriding the default behaviour of evenly spacing them. * @param {string|string[]} [style.fontFamily='Arial'] - The font family * @param {number|string} [style.fontSize=26] - The font size (as a number it converts to px, but as a string, * equivalents are '26px','20pt','160%' or '1.6em') @@ -76,6 +82,7 @@ * e.g 'blue', '#FCFF00' * @param {number} [style.strokeThickness=0] - A number that represents the thickness of the stroke. * Default is 0 (no stroke) + * @param {boolean} [style.trim=false] - Trim transparent borders * @param {string} [style.textBaseline='alphabetic'] - The baseline of the text that is rendered. * @param {boolean} [style.wordWrap=false] - Indicates if word wrap should be used * @param {number} [style.wordWrapWidth=100] - The width at which text will wrap, it needs wordWrap to be set to true @@ -152,6 +159,19 @@ } } + get dropShadowAlpha() + { + return this._dropShadowAlpha; + } + set dropShadowAlpha(dropShadowAlpha) + { + if (this._dropShadowAlpha !== dropShadowAlpha) + { + this._dropShadowAlpha = dropShadowAlpha; + this.styleID++; + } + } + get dropShadowAngle() { return this._dropShadowAngle; @@ -232,6 +252,19 @@ } } + get fillGradientStops() + { + return this._fillGradientStops; + } + set fillGradientStops(fillGradientStops) + { + if (!areArraysEqual(this._fillGradientStops,fillGradientStops)) + { + this._fillGradientStops = fillGradientStops; + this.styleID++; + } + } + get fontFamily() { return this._fontFamily; @@ -402,6 +435,19 @@ } } + get trim() + { + return this._trim; + } + set trim(trim) + { + if (this._trim !== trim) + { + this._trim = trim; + this.styleID++; + } + } + get wordWrap() { return this._wordWrap; @@ -475,3 +521,34 @@ return color; } } + +/** + * Utility function to convert hexadecimal colors to strings, and simply return the color if it's a string. + * This version can also convert array of colors + * + * @param {Array} array1 First array to compare + * @param {Array} array2 Second array to compare + * @return {boolean} Do the arrays contain the same values in the same order + */ +function areArraysEqual(array1, array2) +{ + if (!Array.isArray(array1) || !Array.isArray(array2)) + { + return false; + } + + if (array1.length !== array2.length) + { + return false; + } + + for (let i = 0; i < array1.length; ++i) + { + if (array1[i] !== array2[i]) + { + return false; + } + } + + return true; +} diff --git a/src/core/textures/BaseRenderTexture.js b/src/core/textures/BaseRenderTexture.js index 5d50146..aa727f0 100644 --- a/src/core/textures/BaseRenderTexture.js +++ b/src/core/textures/BaseRenderTexture.js @@ -1,8 +1,6 @@ import BaseTexture from './BaseTexture'; import settings from '../settings'; -const { RESOLUTION, SCALE_MODE } = settings; - /** * A BaseRenderTexture is a special texture that allows any Pixi display object to be rendered to it. * @@ -54,7 +52,7 @@ { super(null, scaleMode); - this.resolution = resolution || RESOLUTION; + this.resolution = resolution || settings.RESOLUTION; this.width = width; this.height = height; @@ -62,7 +60,7 @@ this.realWidth = this.width * this.resolution; this.realHeight = this.height * this.resolution; - this.scaleMode = scaleMode || SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; this.hasLoaded = true; /** diff --git a/src/core/textures/BaseTexture.js b/src/core/textures/BaseTexture.js index 676dca3..aeccf39 100644 --- a/src/core/textures/BaseTexture.js +++ b/src/core/textures/BaseTexture.js @@ -77,7 +77,7 @@ * @default PIXI.settings.SCALE_MODE * @see PIXI.SCALE_MODES */ - this.scaleMode = scaleMode || settings.SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; /** * Set to true once the base texture has successfully loaded. @@ -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/RenderTexture.js b/src/core/textures/RenderTexture.js index 4c5034f..b3c6297 100644 --- a/src/core/textures/RenderTexture.js +++ b/src/core/textures/RenderTexture.js @@ -54,8 +54,8 @@ /* eslint-disable prefer-rest-params, no-console */ const width = arguments[1]; const height = arguments[2]; - const scaleMode = arguments[3] || 0; - const resolution = arguments[4] || 1; + const scaleMode = arguments[3]; + const resolution = arguments[4]; // we have an old render texture.. console.warn(`Please use RenderTexture.create(${width}, ${height}) instead of the ctor directly.`); 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 ca31456..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 @@ -459,7 +497,9 @@ if (frame.x + frame.width > this.baseTexture.width || frame.y + frame.height > this.baseTexture.height) { - throw new Error(`Texture Error: frame does not fit inside the base Texture dimensions ${this}`); + throw new Error('Texture Error: frame does not fit inside the base Texture dimensions: ' + + `X: ${frame.x} + ${frame.width} > ${this.baseTexture.width} ` + + `Y: ${frame.y} + ${frame.height} > ${this.baseTexture.height}`); } // this.valid = frame && frame.width && frame.height && this.baseTexture.source && this.baseTexture.hasLoaded; @@ -520,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. @@ -528,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 2a7497b..99e585a 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)); } /** @@ -360,3 +362,43 @@ * @private */ export const BaseTextureCache = {}; + +/** + * 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/core/utils/trimCanvas.js b/src/core/utils/trimCanvas.js new file mode 100644 index 0000000..5af6de0 --- /dev/null +++ b/src/core/utils/trimCanvas.js @@ -0,0 +1,83 @@ +/** + * Trim transparent borders from a canvas + * + * @memberof PIXI + * @function trimCanvas + * @private + * @param {HTMLCanvasElement} canvas - the canvas to trim + * @returns {object} Trim data + */ +export default function trimCanvas(canvas) +{ + // https://gist.github.com/remy/784508 + + let width = canvas.width; + let height = canvas.height; + + const context = canvas.getContext('2d'); + const imageData = context.getImageData(0, 0, width, height); + const pixels = imageData.data; + const len = pixels.length; + + const bound = { + top: null, + left: null, + right: null, + bottom: null, + }; + let i; + let x; + let y; + + for (i = 0; i < len; i += 4) + { + if (pixels[i + 3] !== 0) + { + x = (i / 4) % width; + y = ~~((i / 4) / width); + + if (bound.top === null) + { + bound.top = y; + } + + if (bound.left === null) + { + bound.left = x; + } + else if (x < bound.left) + { + bound.left = x; + } + + if (bound.right === null) + { + bound.right = x + 1; + } + else if (bound.right < x) + { + bound.right = x + 1; + } + + if (bound.bottom === null) + { + bound.bottom = y; + } + else if (bound.bottom < y) + { + bound.bottom = y; + } + } + } + + width = bound.right - bound.left; + height = bound.bottom - bound.top + 1; + + const data = context.getImageData(bound.left, bound.top, width, height); + + return { + height, + width, + data, + }; +} diff --git a/src/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 1271fef..71ade15 100644 --- a/package.json +++ b/package.json @@ -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/scripts/release.js b/scripts/release.js index c4c8e36..156620c 100644 --- a/scripts/release.js +++ b/scripts/release.js @@ -12,7 +12,8 @@ 'dist/**/*', 'lib/**/*', 'src/**/*', - 'scripts/**/*', + 'scripts/*', + 'scripts/renders/*', 'test/**/*', '*.json', '*.md', diff --git a/scripts/renders/client.js b/scripts/renders/client.js index 16c9565..6faee34 100755 --- a/scripts/renders/client.js +++ b/scripts/renders/client.js @@ -1,6 +1,6 @@ 'use strict'; -require('../../bin/pixi'); +require('../../dist/pixi'); PIXI.utils.skipHello(); const fs = require('fs'); 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 bc730db..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. @@ -33,8 +33,11 @@ * need to call toDataUrl on the webgl context * @param {number} [options.resolution=1] - The resolution / device pixel ratio of the renderer, retina would be 2 * @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 @@ -43,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. */ @@ -73,7 +99,7 @@ */ stop() { - this.ticker.stop(); + this._ticker.stop(); } /** @@ -81,7 +107,7 @@ */ start() { - this.ticker.start(); + this._ticker.start(); } /** @@ -95,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 index 81e076b..78b17e6 100644 --- a/src/core/Shader.js +++ b/src/core/Shader.js @@ -1,9 +1,7 @@ import { GLShader } from 'pixi-gl-core'; import settings from './settings'; -const { PRECISION } = settings; - -function checkPrecision(src) +function checkPrecision(src, def) { if (src instanceof Array) { @@ -11,14 +9,14 @@ { const copy = src.slice(0); - copy.unshift(`precision ${PRECISION} float;`); + copy.unshift(`precision ${def} float;`); return copy; } } else if (src.substring(0, 9) !== 'precision') { - return `precision ${PRECISION} float;\n${src}`; + return `precision ${def} float;\n${src}`; } return src; @@ -42,6 +40,7 @@ */ constructor(gl, vertexSrc, fragmentSrc) { - super(gl, checkPrecision(vertexSrc), checkPrecision(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 3425b41..f9235e4 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -181,7 +181,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 b3792ef..17fde8d 100644 --- a/src/core/display/Container.js +++ b/src/core/display/Container.js @@ -73,13 +73,14 @@ } child.parent = this; - - // ensure a transform will be recalculated.. - this.transform._parentID = -1; - this._boundsID++; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.push(child); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(this.children.length - 1); child.emit('added', this); @@ -108,9 +109,14 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.splice(index, 0, child); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(index); child.emit('added', this); @@ -174,6 +180,7 @@ removeItems(this.children, currentIndex, 1); // remove from old position this.children.splice(index, 0, child); // add at new position + this.onChildrenChange(index); } @@ -220,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! @@ -244,9 +252,14 @@ { const child = this.getChildAt(index); + // ensure child transform will be recalculated.. child.parent = null; + child.transform._parentID = -1; removeItems(this.children, index, 1); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(index); child.emit('removed', this); @@ -275,8 +288,14 @@ for (let i = 0; i < removed.length; ++i) { removed[i].parent = null; + if (removed[i].transform) + { + removed[i].transform._parentID = -1; + } } + this._boundsID++; + this.onChildrenChange(beginIndex); for (let i = 0; i < removed.length; ++i) diff --git a/src/core/display/DisplayObject.js b/src/core/display/DisplayObject.js index 3affe36..4e6c77d 100644 --- a/src/core/display/DisplayObject.js +++ b/src/core/display/DisplayObject.js @@ -109,7 +109,7 @@ /** * The original, cached mask of the object * - * @member {PIXI.Rectangle} + * @member {PIXI.Graphics|PIXI.Sprite} * @private */ this._mask = null; diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index 95f2af4..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 ); @@ -1065,10 +1061,15 @@ canvasRenderer = new CanvasRenderer(); } - tempMatrix.tx = -bounds.x; - tempMatrix.ty = -bounds.y; + this.transform.updateLocalTransform(); + this.transform.localTransform.copy(tempMatrix); - canvasRenderer.render(this, canvasBuffer, false, tempMatrix); + tempMatrix.invert(); + + tempMatrix.tx -= bounds.x; + tempMatrix.ty -= bounds.y; + + canvasRenderer.render(this, canvasBuffer, true, tempMatrix); const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode); 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 e67680f..867dde2 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/buildCircle.js b/src/core/graphics/webgl/utils/buildCircle.js index 0400d13..abf5d02 100644 --- a/src/core/graphics/webgl/utils/buildCircle.js +++ b/src/core/graphics/webgl/utils/buildCircle.js @@ -33,6 +33,11 @@ height = circleData.height; } + if (width === 0 || height === 0) + { + return; + } + const totalSegs = Math.floor(30 * Math.sqrt(circleData.radius)) || Math.floor(15 * Math.sqrt(circleData.width + circleData.height)); diff --git a/src/core/graphics/webgl/utils/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 90155f9..f03fe15 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -30,6 +30,7 @@ export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; export { default as CanvasGraphicsRenderer } from './graphics/canvas/CanvasGraphicsRenderer'; +export { default as Spritesheet } from './textures/Spritesheet'; export { default as Texture } from './textures/Texture'; export { default as BaseTexture } from './textures/BaseTexture'; export { default as RenderTexture } from './textures/RenderTexture'; diff --git a/src/core/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 ccce680..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); } /** @@ -162,6 +162,9 @@ if (transform) { transform.copy(tempWt); + + // lets not forget to flag the parent transform as dirty... + this._tempDisplayObjectParent.transform._worldID = -1; } else { @@ -169,6 +172,7 @@ } displayObject.parent = this._tempDisplayObjectParent; + displayObject.updateTransform(); displayObject.parent = cacheParent; // displayObject.hitArea = //TODO add a temp hit area @@ -279,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/canvas/utils/CanvasRenderTarget.js b/src/core/renderers/canvas/utils/CanvasRenderTarget.js index 3de0aef..b409891 100644 --- a/src/core/renderers/canvas/utils/CanvasRenderTarget.js +++ b/src/core/renderers/canvas/utils/CanvasRenderTarget.js @@ -1,5 +1,4 @@ import settings from '../../../settings'; -const { RESOLUTION } = settings; /** * Creates a Canvas element of the given size. @@ -30,7 +29,7 @@ */ this.context = this.canvas.getContext('2d'); - this.resolution = resolution || RESOLUTION; + this.resolution = resolution || settings.RESOLUTION; this.resize(width, height); } 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 ce0e1c4..788ed6c 100644 --- a/src/core/renderers/webgl/WebGLRenderer.js +++ b/src/core/renderers/webgl/WebGLRenderer.js @@ -30,8 +30,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 @@ -49,10 +49,19 @@ * enable this if you need to call toDataUrl on the webgl context. * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when * rendering, stopping pixel interpolation. + * @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; + + if (this.legacy) + { + glCore.VertexArrayObject.FORCE_NATIVE = true; + } /** * The type of this renderer as a standardised const @@ -229,7 +238,7 @@ this.emit('context', gl); // setup the width/height properties and gl viewport - this.resize(this.width, this.height); + this.resize(this.screen.width, this.screen.height); } /** @@ -323,16 +332,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) { @@ -376,6 +385,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 @@ -440,9 +469,10 @@ * Changes the current shader to the one given in parameter * * @param {PIXI.Shader} shader - the new shader + * @param {boolean} [autoProject=true] - Whether automatically set the projection matrix * @return {PIXI.WebGLRenderer} Returns itself. */ - bindShader(shader) + bindShader(shader, autoProject) { // TODO cache if (this._activeShader !== shader) @@ -450,8 +480,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 bbab3b3..944f41d 100644 --- a/src/core/renderers/webgl/filters/Filter.js +++ b/src/core/renderers/webgl/filters/Filter.js @@ -1,6 +1,7 @@ import extractUniformsFromSrc from './extractUniformsFromSrc'; import { uid } from '../../../utils'; import { BLEND_MODES } from '../../../const'; +import settings from '../../../settings'; const SOURCE_KEY_MAP = {}; @@ -10,7 +11,7 @@ * @memberof PIXI * @extends PIXI.Shader */ -class Filter +export default class Filter { /** * @param {string} [vertexSrc] - The source of the vertex shader. @@ -35,8 +36,6 @@ this.blendMode = BLEND_MODES.NORMAL; - // pull out the vertex and shader uniforms if they are not specified.. - // currently this does not extract structs only default types this.uniformData = uniforms || extractUniformsFromSrc(this.vertexSrc, this.fragmentSrc, 'projectionMatrix|uSampler'); /** @@ -80,7 +79,7 @@ * * @member {number} */ - this.resolution = 1; + this.resolution = settings.RESOLUTION; /** * If enabled is true the filter is applied, if false it will not. @@ -97,8 +96,11 @@ * @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 { // --- // // this.uniforms.filterMatrix = filterManager.calculateSpriteMatrix(tempMatrix, window.panda ); @@ -170,5 +172,3 @@ ].join('\n'); } } - -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 dc7072a..6c7422d 100644 --- a/src/core/renderers/webgl/managers/FilterManager.js +++ b/src/core/renderers/webgl/managers/FilterManager.js @@ -154,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 @@ -176,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; @@ -184,7 +184,7 @@ flop = t; } - filters[i].apply(this, flip, lastState.renderTarget, true); + filters[i].apply(this, flip, lastState.renderTarget, false, currentState); this.freePotRenderTarget(flip); this.freePotRenderTarget(flop); @@ -291,9 +291,13 @@ let textureCount = 1; let currentState; - if (shader.uniforms.data.filterArea) + // 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; @@ -306,9 +310,9 @@ // use this to clamp displaced texture coords so they belong to filterArea // see displacementFilter fragment shader for an example - if (shader.uniforms.data.filterClamp) + if (shader.uniforms.filterClamp) { - currentState = this.filterData.stack[this.filterData.index]; + currentState = currentState || this.filterData.stack[this.filterData.index]; const filterClamp = shader.uniforms.filterClamp; @@ -485,7 +489,7 @@ */ destroy() { - this.shaderCache = []; + this.shaderCache = {}; this.emptyPool(); } diff --git a/src/core/renderers/webgl/utils/RenderTarget.js b/src/core/renderers/webgl/utils/RenderTarget.js index afe51ab..6d2467f 100644 --- a/src/core/renderers/webgl/utils/RenderTarget.js +++ b/src/core/renderers/webgl/utils/RenderTarget.js @@ -124,7 +124,7 @@ * @default PIXI.settings.SCALE_MODE * @see PIXI.SCALE_MODES */ - this.scaleMode = scaleMode || settings.SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; /** * Whether this object is the root element or not 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 36ea5bb..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; @@ -346,8 +346,8 @@ /** * Gets the local bounds of the sprite object. * - * @param {Rectangle} rect - The output rectangle. - * @return {Rectangle} The bounds. + * @param {PIXI.Rectangle} rect - The output rectangle. + * @return {PIXI.Rectangle} The bounds. */ getLocalBounds(rect) { @@ -440,7 +440,7 @@ * * @static * @param {number|string|PIXI.BaseTexture|HTMLCanvasElement|HTMLVideoElement} source Source to create texture from - * @return {PIXI.Texture} 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 e77b39a..90b08c0 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++) @@ -103,11 +102,18 @@ { const gl = this.renderer.gl; - // step 1: first check max textures the GPU can handle. - this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); + if (this.renderer.legacy) + { + this.MAX_TEXTURES = 1; + } + else + { + // step 1: first check max textures the GPU can handle. + this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); - // step 2: check the maximum number of if statements the shader can have too.. - this.MAX_TEXTURES = checkMaxIfStatmentsInShader(this.MAX_TEXTURES, gl); + // step 2: check the maximum number of if statements the shader can have too.. + this.MAX_TEXTURES = checkMaxIfStatmentsInShader(this.MAX_TEXTURES, gl); + } const shader = this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); @@ -130,8 +136,12 @@ .addIndex(this.indexBuffer) .addAttribute(this.vertexBuffers[i], shader.attributes.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) .addAttribute(this.vertexBuffers[i], shader.attributes.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) - .addAttribute(this.vertexBuffers[i], shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4) - .addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + .addAttribute(this.vertexBuffers[i], shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); + + if (shader.attributes.aTextureId) + { + this.vaos[i].addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + } /* eslint-enable max-len */ } @@ -378,8 +388,12 @@ .addIndex(this.indexBuffer) .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) - .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4) - .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); + + if (this.shader.attributes.aTextureId) + { + this.vaos[this.vertexCount].addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + } /* eslint-enable max-len */ } diff --git a/src/core/text/Text.js b/src/core/text/Text.js index 013625a..cec8c32 100644 --- a/src/core/text/Text.js +++ b/src/core/text/Text.js @@ -6,6 +6,7 @@ import { TEXT_GRADIENT } from '../const'; import settings from '../settings'; import TextStyle from './TextStyle'; +import trimCanvas from '../utils/trimCanvas'; const defaultDestroyOptions = { texture: true, @@ -57,7 +58,7 @@ /** * The canvas 2d context that everything is drawn with - * @member {HTMLCanvasElement} + * @member {CanvasRenderingContext2D} */ this.context = this.canvas.getContext('2d'); @@ -189,10 +190,12 @@ if (style.dropShadow) { + this.context.shadowBlur = style.dropShadowBlur; + this.context.globalAlpha = style.dropShadowAlpha; + if (style.dropShadowBlur > 0) { this.context.shadowColor = style.dropShadowColor; - this.context.shadowBlur = style.dropShadowBlur; } else { @@ -237,6 +240,10 @@ } } + // reset the shadow blur and alpha that was set by the drop shadow, for the regular text + this.context.shadowBlur = 0; + this.context.globalAlpha = 1; + // set canvas text styles this.context.fillStyle = this._generateFillStyle(style, lines); @@ -326,6 +333,15 @@ */ updateTexture() { + if (this._style.trim) + { + const trimmed = trimCanvas(this.canvas); + + this.canvas.width = trimmed.width; + this.canvas.height = trimmed.height; + this.context.putImageData(trimmed.data, 0, 0); + } + const texture = this._texture; const style = this._style; @@ -475,6 +491,19 @@ } /** + * Gets the local bounds of the text object. + * + * @param {Rectangle} rect - The output rectangle. + * @return {Rectangle} The bounds. + */ + getLocalBounds(rect) + { + this.updateText(true); + + return super.getLocalBounds.call(this, rect); + } + + /** * calculates the bounds of the Text as a rectangle. The bounds calculation takes the worldTransform into account. */ _calculateBounds() @@ -525,6 +554,29 @@ const width = this.canvas.width / this.resolution; const height = this.canvas.height / this.resolution; + // make a copy of the style settings, so we can manipulate them later + const fill = style.fill.slice(); + const fillGradientStops = style.fillGradientStops.slice(); + + // wanting to evenly distribute the fills. So an array of 4 colours should give fills of 0.25, 0.5 and 0.75 + if (!fillGradientStops.length) + { + const lengthPlus1 = fill.length + 1; + + for (let i = 1; i < lengthPlus1; ++i) + { + fillGradientStops.push(i / lengthPlus1); + } + } + + // stop the bleeding of the last gradient on the line above to the top gradient of the this line + // by hard defining the first gradient colour at point 0, and last gradient colour at point 1 + fill.unshift(style.fill[0]); + fillGradientStops.unshift(0); + + fill.push(style.fill[style.fill.length - 1]); + fillGradientStops.push(1); + if (style.fillGradientType === TEXT_GRADIENT.LINEAR_VERTICAL) { // start the gradient at the top center of the canvas, and end at the bottom middle of the canvas @@ -532,15 +584,22 @@ // we need to repeat the gradient so that each individual line of text has the same vertical gradient effect // ['#FF0000', '#00FF00', '#0000FF'] over 2 lines would create stops at 0.125, 0.25, 0.375, 0.625, 0.75, 0.875 - totalIterations = (style.fill.length + 1) * lines.length; + totalIterations = (fill.length + 1) * lines.length; currentIteration = 0; for (let i = 0; i < lines.length; i++) { currentIteration += 1; - for (let j = 0; j < style.fill.length; j++) + for (let j = 0; j < fill.length; j++) { - stop = (currentIteration / totalIterations); - gradient.addColorStop(stop, style.fill[j]); + if (fillGradientStops[j]) + { + stop = (fillGradientStops[j] / lines.length) + (i / lines.length); + } + else + { + stop = currentIteration / totalIterations; + } + gradient.addColorStop(stop, fill[j]); currentIteration++; } } @@ -552,13 +611,20 @@ // can just evenly space out the gradients in this case, as multiple lines makes no difference // to an even left to right gradient - totalIterations = style.fill.length + 1; + totalIterations = fill.length + 1; currentIteration = 1; - for (let i = 0; i < style.fill.length; i++) + for (let i = 0; i < fill.length; i++) { - stop = currentIteration / totalIterations; - gradient.addColorStop(stop, style.fill[i]); + if (fillGradientStops[i]) + { + stop = fillGradientStops[i]; + } + else + { + stop = currentIteration / totalIterations; + } + gradient.addColorStop(stop, fill[i]); currentIteration++; } } @@ -569,7 +635,7 @@ /** * Destroys this text object. * Note* Unlike a Sprite, a Text object will automatically destroy its baseTexture and texture as - * the majorety of the time the texture will not be shared with any other Sprites. + * the majority of the time the texture will not be shared with any other Sprites. * * @param {object|boolean} [options] - Options parameter. A boolean will act as if all options * have been set to that value @@ -680,7 +746,7 @@ set text(text) // eslint-disable-line require-jsdoc { - text = String(text || ' '); + text = String(text === '' || text === null || text === undefined ? ' ' : text); if (this._text === text) { diff --git a/src/core/text/TextStyle.js b/src/core/text/TextStyle.js index 3b25528..890950b 100644 --- a/src/core/text/TextStyle.js +++ b/src/core/text/TextStyle.js @@ -8,12 +8,14 @@ align: 'left', breakWords: false, dropShadow: false, + dropShadowAlpha: 1, dropShadowAngle: Math.PI / 6, dropShadowBlur: 0, dropShadowColor: '#000000', dropShadowDistance: 5, fill: 'black', fillGradientType: TEXT_GRADIENT.LINEAR_VERTICAL, + fillGradientStops: [], fontFamily: 'Arial', fontSize: 26, fontStyle: 'normal', @@ -27,6 +29,7 @@ stroke: 'black', strokeThickness: 0, textBaseline: 'alphabetic', + trim: false, wordWrap: false, wordWrapWidth: 100, }; @@ -47,6 +50,7 @@ * @param {boolean} [style.breakWords=false] - Indicates if lines can be wrapped within words, it * needs wordWrap to be set to true * @param {boolean} [style.dropShadow=false] - Set a drop shadow for the text + * @param {number} [style.dropShadowAlpha=1] - Set alpha for the drop shadow * @param {number} [style.dropShadowAngle=Math.PI/6] - Set a angle of the drop shadow * @param {number} [style.dropShadowBlur=0] - Set a shadow blur radius * @param {string} [style.dropShadowColor='#000000'] - A fill style to be used on the dropshadow e.g 'red', '#00FF00' @@ -55,8 +59,10 @@ * fillstyle that will be used on the text e.g 'red', '#00FF00'. Can be an array to create a gradient * eg ['#000000','#FFFFFF'] * {@link https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/fillStyle|MDN} - * @param {number} [style.fillGradientType=PIXI.TEXT_GRADIENT.LINEAR_VERTICAL] - If fills styles are - * supplied, this can change the type/direction of the gradient. See {@link PIXI.TEXT_GRADIENT} for possible values + * @param {number} [style.fillGradientType=PIXI.TEXT_GRADIENT.LINEAR_VERTICAL] - If fill is an array of colours + * to create a gradient, this can change the type/direction of the gradient. See {@link PIXI.TEXT_GRADIENT} + * @param {number[]} [style.fillGradientStops] - If fill is an array of colours to create a gradient, this array can set + * the stop points (numbers between 0 and 1) for the color, overriding the default behaviour of evenly spacing them. * @param {string|string[]} [style.fontFamily='Arial'] - The font family * @param {number|string} [style.fontSize=26] - The font size (as a number it converts to px, but as a string, * equivalents are '26px','20pt','160%' or '1.6em') @@ -76,6 +82,7 @@ * e.g 'blue', '#FCFF00' * @param {number} [style.strokeThickness=0] - A number that represents the thickness of the stroke. * Default is 0 (no stroke) + * @param {boolean} [style.trim=false] - Trim transparent borders * @param {string} [style.textBaseline='alphabetic'] - The baseline of the text that is rendered. * @param {boolean} [style.wordWrap=false] - Indicates if word wrap should be used * @param {number} [style.wordWrapWidth=100] - The width at which text will wrap, it needs wordWrap to be set to true @@ -152,6 +159,19 @@ } } + get dropShadowAlpha() + { + return this._dropShadowAlpha; + } + set dropShadowAlpha(dropShadowAlpha) + { + if (this._dropShadowAlpha !== dropShadowAlpha) + { + this._dropShadowAlpha = dropShadowAlpha; + this.styleID++; + } + } + get dropShadowAngle() { return this._dropShadowAngle; @@ -232,6 +252,19 @@ } } + get fillGradientStops() + { + return this._fillGradientStops; + } + set fillGradientStops(fillGradientStops) + { + if (!areArraysEqual(this._fillGradientStops,fillGradientStops)) + { + this._fillGradientStops = fillGradientStops; + this.styleID++; + } + } + get fontFamily() { return this._fontFamily; @@ -402,6 +435,19 @@ } } + get trim() + { + return this._trim; + } + set trim(trim) + { + if (this._trim !== trim) + { + this._trim = trim; + this.styleID++; + } + } + get wordWrap() { return this._wordWrap; @@ -475,3 +521,34 @@ return color; } } + +/** + * Utility function to convert hexadecimal colors to strings, and simply return the color if it's a string. + * This version can also convert array of colors + * + * @param {Array} array1 First array to compare + * @param {Array} array2 Second array to compare + * @return {boolean} Do the arrays contain the same values in the same order + */ +function areArraysEqual(array1, array2) +{ + if (!Array.isArray(array1) || !Array.isArray(array2)) + { + return false; + } + + if (array1.length !== array2.length) + { + return false; + } + + for (let i = 0; i < array1.length; ++i) + { + if (array1[i] !== array2[i]) + { + return false; + } + } + + return true; +} diff --git a/src/core/textures/BaseRenderTexture.js b/src/core/textures/BaseRenderTexture.js index 5d50146..aa727f0 100644 --- a/src/core/textures/BaseRenderTexture.js +++ b/src/core/textures/BaseRenderTexture.js @@ -1,8 +1,6 @@ import BaseTexture from './BaseTexture'; import settings from '../settings'; -const { RESOLUTION, SCALE_MODE } = settings; - /** * A BaseRenderTexture is a special texture that allows any Pixi display object to be rendered to it. * @@ -54,7 +52,7 @@ { super(null, scaleMode); - this.resolution = resolution || RESOLUTION; + this.resolution = resolution || settings.RESOLUTION; this.width = width; this.height = height; @@ -62,7 +60,7 @@ this.realWidth = this.width * this.resolution; this.realHeight = this.height * this.resolution; - this.scaleMode = scaleMode || SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; this.hasLoaded = true; /** diff --git a/src/core/textures/BaseTexture.js b/src/core/textures/BaseTexture.js index 676dca3..aeccf39 100644 --- a/src/core/textures/BaseTexture.js +++ b/src/core/textures/BaseTexture.js @@ -77,7 +77,7 @@ * @default PIXI.settings.SCALE_MODE * @see PIXI.SCALE_MODES */ - this.scaleMode = scaleMode || settings.SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; /** * Set to true once the base texture has successfully loaded. @@ -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/RenderTexture.js b/src/core/textures/RenderTexture.js index 4c5034f..b3c6297 100644 --- a/src/core/textures/RenderTexture.js +++ b/src/core/textures/RenderTexture.js @@ -54,8 +54,8 @@ /* eslint-disable prefer-rest-params, no-console */ const width = arguments[1]; const height = arguments[2]; - const scaleMode = arguments[3] || 0; - const resolution = arguments[4] || 1; + const scaleMode = arguments[3]; + const resolution = arguments[4]; // we have an old render texture.. console.warn(`Please use RenderTexture.create(${width}, ${height}) instead of the ctor directly.`); 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 ca31456..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 @@ -459,7 +497,9 @@ if (frame.x + frame.width > this.baseTexture.width || frame.y + frame.height > this.baseTexture.height) { - throw new Error(`Texture Error: frame does not fit inside the base Texture dimensions ${this}`); + throw new Error('Texture Error: frame does not fit inside the base Texture dimensions: ' + + `X: ${frame.x} + ${frame.width} > ${this.baseTexture.width} ` + + `Y: ${frame.y} + ${frame.height} > ${this.baseTexture.height}`); } // this.valid = frame && frame.width && frame.height && this.baseTexture.source && this.baseTexture.hasLoaded; @@ -520,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. @@ -528,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 2a7497b..99e585a 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)); } /** @@ -360,3 +362,43 @@ * @private */ export const BaseTextureCache = {}; + +/** + * 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/core/utils/trimCanvas.js b/src/core/utils/trimCanvas.js new file mode 100644 index 0000000..5af6de0 --- /dev/null +++ b/src/core/utils/trimCanvas.js @@ -0,0 +1,83 @@ +/** + * Trim transparent borders from a canvas + * + * @memberof PIXI + * @function trimCanvas + * @private + * @param {HTMLCanvasElement} canvas - the canvas to trim + * @returns {object} Trim data + */ +export default function trimCanvas(canvas) +{ + // https://gist.github.com/remy/784508 + + let width = canvas.width; + let height = canvas.height; + + const context = canvas.getContext('2d'); + const imageData = context.getImageData(0, 0, width, height); + const pixels = imageData.data; + const len = pixels.length; + + const bound = { + top: null, + left: null, + right: null, + bottom: null, + }; + let i; + let x; + let y; + + for (i = 0; i < len; i += 4) + { + if (pixels[i + 3] !== 0) + { + x = (i / 4) % width; + y = ~~((i / 4) / width); + + if (bound.top === null) + { + bound.top = y; + } + + if (bound.left === null) + { + bound.left = x; + } + else if (x < bound.left) + { + bound.left = x; + } + + if (bound.right === null) + { + bound.right = x + 1; + } + else if (bound.right < x) + { + bound.right = x + 1; + } + + if (bound.bottom === null) + { + bound.bottom = y; + } + else if (bound.bottom < y) + { + bound.bottom = y; + } + } + } + + width = bound.right - bound.left; + height = bound.bottom - bound.top + 1; + + const data = context.getImageData(bound.left, bound.top, width, height); + + return { + height, + width, + data, + }; +} diff --git a/src/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 1271fef..71ade15 100644 --- a/package.json +++ b/package.json @@ -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/scripts/release.js b/scripts/release.js index c4c8e36..156620c 100644 --- a/scripts/release.js +++ b/scripts/release.js @@ -12,7 +12,8 @@ 'dist/**/*', 'lib/**/*', 'src/**/*', - 'scripts/**/*', + 'scripts/*', + 'scripts/renders/*', 'test/**/*', '*.json', '*.md', diff --git a/scripts/renders/client.js b/scripts/renders/client.js index 16c9565..6faee34 100755 --- a/scripts/renders/client.js +++ b/scripts/renders/client.js @@ -1,6 +1,6 @@ 'use strict'; -require('../../bin/pixi'); +require('../../dist/pixi'); PIXI.utils.skipHello(); const fs = require('fs'); 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 bc730db..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. @@ -33,8 +33,11 @@ * need to call toDataUrl on the webgl context * @param {number} [options.resolution=1] - The resolution / device pixel ratio of the renderer, retina would be 2 * @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 @@ -43,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. */ @@ -73,7 +99,7 @@ */ stop() { - this.ticker.stop(); + this._ticker.stop(); } /** @@ -81,7 +107,7 @@ */ start() { - this.ticker.start(); + this._ticker.start(); } /** @@ -95,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 index 81e076b..78b17e6 100644 --- a/src/core/Shader.js +++ b/src/core/Shader.js @@ -1,9 +1,7 @@ import { GLShader } from 'pixi-gl-core'; import settings from './settings'; -const { PRECISION } = settings; - -function checkPrecision(src) +function checkPrecision(src, def) { if (src instanceof Array) { @@ -11,14 +9,14 @@ { const copy = src.slice(0); - copy.unshift(`precision ${PRECISION} float;`); + copy.unshift(`precision ${def} float;`); return copy; } } else if (src.substring(0, 9) !== 'precision') { - return `precision ${PRECISION} float;\n${src}`; + return `precision ${def} float;\n${src}`; } return src; @@ -42,6 +40,7 @@ */ constructor(gl, vertexSrc, fragmentSrc) { - super(gl, checkPrecision(vertexSrc), checkPrecision(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 3425b41..f9235e4 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -181,7 +181,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 b3792ef..17fde8d 100644 --- a/src/core/display/Container.js +++ b/src/core/display/Container.js @@ -73,13 +73,14 @@ } child.parent = this; - - // ensure a transform will be recalculated.. - this.transform._parentID = -1; - this._boundsID++; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.push(child); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(this.children.length - 1); child.emit('added', this); @@ -108,9 +109,14 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.splice(index, 0, child); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(index); child.emit('added', this); @@ -174,6 +180,7 @@ removeItems(this.children, currentIndex, 1); // remove from old position this.children.splice(index, 0, child); // add at new position + this.onChildrenChange(index); } @@ -220,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! @@ -244,9 +252,14 @@ { const child = this.getChildAt(index); + // ensure child transform will be recalculated.. child.parent = null; + child.transform._parentID = -1; removeItems(this.children, index, 1); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(index); child.emit('removed', this); @@ -275,8 +288,14 @@ for (let i = 0; i < removed.length; ++i) { removed[i].parent = null; + if (removed[i].transform) + { + removed[i].transform._parentID = -1; + } } + this._boundsID++; + this.onChildrenChange(beginIndex); for (let i = 0; i < removed.length; ++i) diff --git a/src/core/display/DisplayObject.js b/src/core/display/DisplayObject.js index 3affe36..4e6c77d 100644 --- a/src/core/display/DisplayObject.js +++ b/src/core/display/DisplayObject.js @@ -109,7 +109,7 @@ /** * The original, cached mask of the object * - * @member {PIXI.Rectangle} + * @member {PIXI.Graphics|PIXI.Sprite} * @private */ this._mask = null; diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index 95f2af4..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 ); @@ -1065,10 +1061,15 @@ canvasRenderer = new CanvasRenderer(); } - tempMatrix.tx = -bounds.x; - tempMatrix.ty = -bounds.y; + this.transform.updateLocalTransform(); + this.transform.localTransform.copy(tempMatrix); - canvasRenderer.render(this, canvasBuffer, false, tempMatrix); + tempMatrix.invert(); + + tempMatrix.tx -= bounds.x; + tempMatrix.ty -= bounds.y; + + canvasRenderer.render(this, canvasBuffer, true, tempMatrix); const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode); 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 e67680f..867dde2 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/buildCircle.js b/src/core/graphics/webgl/utils/buildCircle.js index 0400d13..abf5d02 100644 --- a/src/core/graphics/webgl/utils/buildCircle.js +++ b/src/core/graphics/webgl/utils/buildCircle.js @@ -33,6 +33,11 @@ height = circleData.height; } + if (width === 0 || height === 0) + { + return; + } + const totalSegs = Math.floor(30 * Math.sqrt(circleData.radius)) || Math.floor(15 * Math.sqrt(circleData.width + circleData.height)); diff --git a/src/core/graphics/webgl/utils/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 90155f9..f03fe15 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -30,6 +30,7 @@ export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; export { default as CanvasGraphicsRenderer } from './graphics/canvas/CanvasGraphicsRenderer'; +export { default as Spritesheet } from './textures/Spritesheet'; export { default as Texture } from './textures/Texture'; export { default as BaseTexture } from './textures/BaseTexture'; export { default as RenderTexture } from './textures/RenderTexture'; diff --git a/src/core/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 ccce680..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); } /** @@ -162,6 +162,9 @@ if (transform) { transform.copy(tempWt); + + // lets not forget to flag the parent transform as dirty... + this._tempDisplayObjectParent.transform._worldID = -1; } else { @@ -169,6 +172,7 @@ } displayObject.parent = this._tempDisplayObjectParent; + displayObject.updateTransform(); displayObject.parent = cacheParent; // displayObject.hitArea = //TODO add a temp hit area @@ -279,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/canvas/utils/CanvasRenderTarget.js b/src/core/renderers/canvas/utils/CanvasRenderTarget.js index 3de0aef..b409891 100644 --- a/src/core/renderers/canvas/utils/CanvasRenderTarget.js +++ b/src/core/renderers/canvas/utils/CanvasRenderTarget.js @@ -1,5 +1,4 @@ import settings from '../../../settings'; -const { RESOLUTION } = settings; /** * Creates a Canvas element of the given size. @@ -30,7 +29,7 @@ */ this.context = this.canvas.getContext('2d'); - this.resolution = resolution || RESOLUTION; + this.resolution = resolution || settings.RESOLUTION; this.resize(width, height); } 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 ce0e1c4..788ed6c 100644 --- a/src/core/renderers/webgl/WebGLRenderer.js +++ b/src/core/renderers/webgl/WebGLRenderer.js @@ -30,8 +30,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 @@ -49,10 +49,19 @@ * enable this if you need to call toDataUrl on the webgl context. * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when * rendering, stopping pixel interpolation. + * @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; + + if (this.legacy) + { + glCore.VertexArrayObject.FORCE_NATIVE = true; + } /** * The type of this renderer as a standardised const @@ -229,7 +238,7 @@ this.emit('context', gl); // setup the width/height properties and gl viewport - this.resize(this.width, this.height); + this.resize(this.screen.width, this.screen.height); } /** @@ -323,16 +332,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) { @@ -376,6 +385,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 @@ -440,9 +469,10 @@ * Changes the current shader to the one given in parameter * * @param {PIXI.Shader} shader - the new shader + * @param {boolean} [autoProject=true] - Whether automatically set the projection matrix * @return {PIXI.WebGLRenderer} Returns itself. */ - bindShader(shader) + bindShader(shader, autoProject) { // TODO cache if (this._activeShader !== shader) @@ -450,8 +480,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 bbab3b3..944f41d 100644 --- a/src/core/renderers/webgl/filters/Filter.js +++ b/src/core/renderers/webgl/filters/Filter.js @@ -1,6 +1,7 @@ import extractUniformsFromSrc from './extractUniformsFromSrc'; import { uid } from '../../../utils'; import { BLEND_MODES } from '../../../const'; +import settings from '../../../settings'; const SOURCE_KEY_MAP = {}; @@ -10,7 +11,7 @@ * @memberof PIXI * @extends PIXI.Shader */ -class Filter +export default class Filter { /** * @param {string} [vertexSrc] - The source of the vertex shader. @@ -35,8 +36,6 @@ this.blendMode = BLEND_MODES.NORMAL; - // pull out the vertex and shader uniforms if they are not specified.. - // currently this does not extract structs only default types this.uniformData = uniforms || extractUniformsFromSrc(this.vertexSrc, this.fragmentSrc, 'projectionMatrix|uSampler'); /** @@ -80,7 +79,7 @@ * * @member {number} */ - this.resolution = 1; + this.resolution = settings.RESOLUTION; /** * If enabled is true the filter is applied, if false it will not. @@ -97,8 +96,11 @@ * @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 { // --- // // this.uniforms.filterMatrix = filterManager.calculateSpriteMatrix(tempMatrix, window.panda ); @@ -170,5 +172,3 @@ ].join('\n'); } } - -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 dc7072a..6c7422d 100644 --- a/src/core/renderers/webgl/managers/FilterManager.js +++ b/src/core/renderers/webgl/managers/FilterManager.js @@ -154,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 @@ -176,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; @@ -184,7 +184,7 @@ flop = t; } - filters[i].apply(this, flip, lastState.renderTarget, true); + filters[i].apply(this, flip, lastState.renderTarget, false, currentState); this.freePotRenderTarget(flip); this.freePotRenderTarget(flop); @@ -291,9 +291,13 @@ let textureCount = 1; let currentState; - if (shader.uniforms.data.filterArea) + // 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; @@ -306,9 +310,9 @@ // use this to clamp displaced texture coords so they belong to filterArea // see displacementFilter fragment shader for an example - if (shader.uniforms.data.filterClamp) + if (shader.uniforms.filterClamp) { - currentState = this.filterData.stack[this.filterData.index]; + currentState = currentState || this.filterData.stack[this.filterData.index]; const filterClamp = shader.uniforms.filterClamp; @@ -485,7 +489,7 @@ */ destroy() { - this.shaderCache = []; + this.shaderCache = {}; this.emptyPool(); } diff --git a/src/core/renderers/webgl/utils/RenderTarget.js b/src/core/renderers/webgl/utils/RenderTarget.js index afe51ab..6d2467f 100644 --- a/src/core/renderers/webgl/utils/RenderTarget.js +++ b/src/core/renderers/webgl/utils/RenderTarget.js @@ -124,7 +124,7 @@ * @default PIXI.settings.SCALE_MODE * @see PIXI.SCALE_MODES */ - this.scaleMode = scaleMode || settings.SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; /** * Whether this object is the root element or not 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 36ea5bb..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; @@ -346,8 +346,8 @@ /** * Gets the local bounds of the sprite object. * - * @param {Rectangle} rect - The output rectangle. - * @return {Rectangle} The bounds. + * @param {PIXI.Rectangle} rect - The output rectangle. + * @return {PIXI.Rectangle} The bounds. */ getLocalBounds(rect) { @@ -440,7 +440,7 @@ * * @static * @param {number|string|PIXI.BaseTexture|HTMLCanvasElement|HTMLVideoElement} source Source to create texture from - * @return {PIXI.Texture} 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 e77b39a..90b08c0 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++) @@ -103,11 +102,18 @@ { const gl = this.renderer.gl; - // step 1: first check max textures the GPU can handle. - this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); + if (this.renderer.legacy) + { + this.MAX_TEXTURES = 1; + } + else + { + // step 1: first check max textures the GPU can handle. + this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); - // step 2: check the maximum number of if statements the shader can have too.. - this.MAX_TEXTURES = checkMaxIfStatmentsInShader(this.MAX_TEXTURES, gl); + // step 2: check the maximum number of if statements the shader can have too.. + this.MAX_TEXTURES = checkMaxIfStatmentsInShader(this.MAX_TEXTURES, gl); + } const shader = this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); @@ -130,8 +136,12 @@ .addIndex(this.indexBuffer) .addAttribute(this.vertexBuffers[i], shader.attributes.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) .addAttribute(this.vertexBuffers[i], shader.attributes.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) - .addAttribute(this.vertexBuffers[i], shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4) - .addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + .addAttribute(this.vertexBuffers[i], shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); + + if (shader.attributes.aTextureId) + { + this.vaos[i].addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + } /* eslint-enable max-len */ } @@ -378,8 +388,12 @@ .addIndex(this.indexBuffer) .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) - .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4) - .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); + + if (this.shader.attributes.aTextureId) + { + this.vaos[this.vertexCount].addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + } /* eslint-enable max-len */ } diff --git a/src/core/text/Text.js b/src/core/text/Text.js index 013625a..cec8c32 100644 --- a/src/core/text/Text.js +++ b/src/core/text/Text.js @@ -6,6 +6,7 @@ import { TEXT_GRADIENT } from '../const'; import settings from '../settings'; import TextStyle from './TextStyle'; +import trimCanvas from '../utils/trimCanvas'; const defaultDestroyOptions = { texture: true, @@ -57,7 +58,7 @@ /** * The canvas 2d context that everything is drawn with - * @member {HTMLCanvasElement} + * @member {CanvasRenderingContext2D} */ this.context = this.canvas.getContext('2d'); @@ -189,10 +190,12 @@ if (style.dropShadow) { + this.context.shadowBlur = style.dropShadowBlur; + this.context.globalAlpha = style.dropShadowAlpha; + if (style.dropShadowBlur > 0) { this.context.shadowColor = style.dropShadowColor; - this.context.shadowBlur = style.dropShadowBlur; } else { @@ -237,6 +240,10 @@ } } + // reset the shadow blur and alpha that was set by the drop shadow, for the regular text + this.context.shadowBlur = 0; + this.context.globalAlpha = 1; + // set canvas text styles this.context.fillStyle = this._generateFillStyle(style, lines); @@ -326,6 +333,15 @@ */ updateTexture() { + if (this._style.trim) + { + const trimmed = trimCanvas(this.canvas); + + this.canvas.width = trimmed.width; + this.canvas.height = trimmed.height; + this.context.putImageData(trimmed.data, 0, 0); + } + const texture = this._texture; const style = this._style; @@ -475,6 +491,19 @@ } /** + * Gets the local bounds of the text object. + * + * @param {Rectangle} rect - The output rectangle. + * @return {Rectangle} The bounds. + */ + getLocalBounds(rect) + { + this.updateText(true); + + return super.getLocalBounds.call(this, rect); + } + + /** * calculates the bounds of the Text as a rectangle. The bounds calculation takes the worldTransform into account. */ _calculateBounds() @@ -525,6 +554,29 @@ const width = this.canvas.width / this.resolution; const height = this.canvas.height / this.resolution; + // make a copy of the style settings, so we can manipulate them later + const fill = style.fill.slice(); + const fillGradientStops = style.fillGradientStops.slice(); + + // wanting to evenly distribute the fills. So an array of 4 colours should give fills of 0.25, 0.5 and 0.75 + if (!fillGradientStops.length) + { + const lengthPlus1 = fill.length + 1; + + for (let i = 1; i < lengthPlus1; ++i) + { + fillGradientStops.push(i / lengthPlus1); + } + } + + // stop the bleeding of the last gradient on the line above to the top gradient of the this line + // by hard defining the first gradient colour at point 0, and last gradient colour at point 1 + fill.unshift(style.fill[0]); + fillGradientStops.unshift(0); + + fill.push(style.fill[style.fill.length - 1]); + fillGradientStops.push(1); + if (style.fillGradientType === TEXT_GRADIENT.LINEAR_VERTICAL) { // start the gradient at the top center of the canvas, and end at the bottom middle of the canvas @@ -532,15 +584,22 @@ // we need to repeat the gradient so that each individual line of text has the same vertical gradient effect // ['#FF0000', '#00FF00', '#0000FF'] over 2 lines would create stops at 0.125, 0.25, 0.375, 0.625, 0.75, 0.875 - totalIterations = (style.fill.length + 1) * lines.length; + totalIterations = (fill.length + 1) * lines.length; currentIteration = 0; for (let i = 0; i < lines.length; i++) { currentIteration += 1; - for (let j = 0; j < style.fill.length; j++) + for (let j = 0; j < fill.length; j++) { - stop = (currentIteration / totalIterations); - gradient.addColorStop(stop, style.fill[j]); + if (fillGradientStops[j]) + { + stop = (fillGradientStops[j] / lines.length) + (i / lines.length); + } + else + { + stop = currentIteration / totalIterations; + } + gradient.addColorStop(stop, fill[j]); currentIteration++; } } @@ -552,13 +611,20 @@ // can just evenly space out the gradients in this case, as multiple lines makes no difference // to an even left to right gradient - totalIterations = style.fill.length + 1; + totalIterations = fill.length + 1; currentIteration = 1; - for (let i = 0; i < style.fill.length; i++) + for (let i = 0; i < fill.length; i++) { - stop = currentIteration / totalIterations; - gradient.addColorStop(stop, style.fill[i]); + if (fillGradientStops[i]) + { + stop = fillGradientStops[i]; + } + else + { + stop = currentIteration / totalIterations; + } + gradient.addColorStop(stop, fill[i]); currentIteration++; } } @@ -569,7 +635,7 @@ /** * Destroys this text object. * Note* Unlike a Sprite, a Text object will automatically destroy its baseTexture and texture as - * the majorety of the time the texture will not be shared with any other Sprites. + * the majority of the time the texture will not be shared with any other Sprites. * * @param {object|boolean} [options] - Options parameter. A boolean will act as if all options * have been set to that value @@ -680,7 +746,7 @@ set text(text) // eslint-disable-line require-jsdoc { - text = String(text || ' '); + text = String(text === '' || text === null || text === undefined ? ' ' : text); if (this._text === text) { diff --git a/src/core/text/TextStyle.js b/src/core/text/TextStyle.js index 3b25528..890950b 100644 --- a/src/core/text/TextStyle.js +++ b/src/core/text/TextStyle.js @@ -8,12 +8,14 @@ align: 'left', breakWords: false, dropShadow: false, + dropShadowAlpha: 1, dropShadowAngle: Math.PI / 6, dropShadowBlur: 0, dropShadowColor: '#000000', dropShadowDistance: 5, fill: 'black', fillGradientType: TEXT_GRADIENT.LINEAR_VERTICAL, + fillGradientStops: [], fontFamily: 'Arial', fontSize: 26, fontStyle: 'normal', @@ -27,6 +29,7 @@ stroke: 'black', strokeThickness: 0, textBaseline: 'alphabetic', + trim: false, wordWrap: false, wordWrapWidth: 100, }; @@ -47,6 +50,7 @@ * @param {boolean} [style.breakWords=false] - Indicates if lines can be wrapped within words, it * needs wordWrap to be set to true * @param {boolean} [style.dropShadow=false] - Set a drop shadow for the text + * @param {number} [style.dropShadowAlpha=1] - Set alpha for the drop shadow * @param {number} [style.dropShadowAngle=Math.PI/6] - Set a angle of the drop shadow * @param {number} [style.dropShadowBlur=0] - Set a shadow blur radius * @param {string} [style.dropShadowColor='#000000'] - A fill style to be used on the dropshadow e.g 'red', '#00FF00' @@ -55,8 +59,10 @@ * fillstyle that will be used on the text e.g 'red', '#00FF00'. Can be an array to create a gradient * eg ['#000000','#FFFFFF'] * {@link https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/fillStyle|MDN} - * @param {number} [style.fillGradientType=PIXI.TEXT_GRADIENT.LINEAR_VERTICAL] - If fills styles are - * supplied, this can change the type/direction of the gradient. See {@link PIXI.TEXT_GRADIENT} for possible values + * @param {number} [style.fillGradientType=PIXI.TEXT_GRADIENT.LINEAR_VERTICAL] - If fill is an array of colours + * to create a gradient, this can change the type/direction of the gradient. See {@link PIXI.TEXT_GRADIENT} + * @param {number[]} [style.fillGradientStops] - If fill is an array of colours to create a gradient, this array can set + * the stop points (numbers between 0 and 1) for the color, overriding the default behaviour of evenly spacing them. * @param {string|string[]} [style.fontFamily='Arial'] - The font family * @param {number|string} [style.fontSize=26] - The font size (as a number it converts to px, but as a string, * equivalents are '26px','20pt','160%' or '1.6em') @@ -76,6 +82,7 @@ * e.g 'blue', '#FCFF00' * @param {number} [style.strokeThickness=0] - A number that represents the thickness of the stroke. * Default is 0 (no stroke) + * @param {boolean} [style.trim=false] - Trim transparent borders * @param {string} [style.textBaseline='alphabetic'] - The baseline of the text that is rendered. * @param {boolean} [style.wordWrap=false] - Indicates if word wrap should be used * @param {number} [style.wordWrapWidth=100] - The width at which text will wrap, it needs wordWrap to be set to true @@ -152,6 +159,19 @@ } } + get dropShadowAlpha() + { + return this._dropShadowAlpha; + } + set dropShadowAlpha(dropShadowAlpha) + { + if (this._dropShadowAlpha !== dropShadowAlpha) + { + this._dropShadowAlpha = dropShadowAlpha; + this.styleID++; + } + } + get dropShadowAngle() { return this._dropShadowAngle; @@ -232,6 +252,19 @@ } } + get fillGradientStops() + { + return this._fillGradientStops; + } + set fillGradientStops(fillGradientStops) + { + if (!areArraysEqual(this._fillGradientStops,fillGradientStops)) + { + this._fillGradientStops = fillGradientStops; + this.styleID++; + } + } + get fontFamily() { return this._fontFamily; @@ -402,6 +435,19 @@ } } + get trim() + { + return this._trim; + } + set trim(trim) + { + if (this._trim !== trim) + { + this._trim = trim; + this.styleID++; + } + } + get wordWrap() { return this._wordWrap; @@ -475,3 +521,34 @@ return color; } } + +/** + * Utility function to convert hexadecimal colors to strings, and simply return the color if it's a string. + * This version can also convert array of colors + * + * @param {Array} array1 First array to compare + * @param {Array} array2 Second array to compare + * @return {boolean} Do the arrays contain the same values in the same order + */ +function areArraysEqual(array1, array2) +{ + if (!Array.isArray(array1) || !Array.isArray(array2)) + { + return false; + } + + if (array1.length !== array2.length) + { + return false; + } + + for (let i = 0; i < array1.length; ++i) + { + if (array1[i] !== array2[i]) + { + return false; + } + } + + return true; +} diff --git a/src/core/textures/BaseRenderTexture.js b/src/core/textures/BaseRenderTexture.js index 5d50146..aa727f0 100644 --- a/src/core/textures/BaseRenderTexture.js +++ b/src/core/textures/BaseRenderTexture.js @@ -1,8 +1,6 @@ import BaseTexture from './BaseTexture'; import settings from '../settings'; -const { RESOLUTION, SCALE_MODE } = settings; - /** * A BaseRenderTexture is a special texture that allows any Pixi display object to be rendered to it. * @@ -54,7 +52,7 @@ { super(null, scaleMode); - this.resolution = resolution || RESOLUTION; + this.resolution = resolution || settings.RESOLUTION; this.width = width; this.height = height; @@ -62,7 +60,7 @@ this.realWidth = this.width * this.resolution; this.realHeight = this.height * this.resolution; - this.scaleMode = scaleMode || SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; this.hasLoaded = true; /** diff --git a/src/core/textures/BaseTexture.js b/src/core/textures/BaseTexture.js index 676dca3..aeccf39 100644 --- a/src/core/textures/BaseTexture.js +++ b/src/core/textures/BaseTexture.js @@ -77,7 +77,7 @@ * @default PIXI.settings.SCALE_MODE * @see PIXI.SCALE_MODES */ - this.scaleMode = scaleMode || settings.SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; /** * Set to true once the base texture has successfully loaded. @@ -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/RenderTexture.js b/src/core/textures/RenderTexture.js index 4c5034f..b3c6297 100644 --- a/src/core/textures/RenderTexture.js +++ b/src/core/textures/RenderTexture.js @@ -54,8 +54,8 @@ /* eslint-disable prefer-rest-params, no-console */ const width = arguments[1]; const height = arguments[2]; - const scaleMode = arguments[3] || 0; - const resolution = arguments[4] || 1; + const scaleMode = arguments[3]; + const resolution = arguments[4]; // we have an old render texture.. console.warn(`Please use RenderTexture.create(${width}, ${height}) instead of the ctor directly.`); 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 ca31456..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 @@ -459,7 +497,9 @@ if (frame.x + frame.width > this.baseTexture.width || frame.y + frame.height > this.baseTexture.height) { - throw new Error(`Texture Error: frame does not fit inside the base Texture dimensions ${this}`); + throw new Error('Texture Error: frame does not fit inside the base Texture dimensions: ' + + `X: ${frame.x} + ${frame.width} > ${this.baseTexture.width} ` + + `Y: ${frame.y} + ${frame.height} > ${this.baseTexture.height}`); } // this.valid = frame && frame.width && frame.height && this.baseTexture.source && this.baseTexture.hasLoaded; @@ -520,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. @@ -528,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 2a7497b..99e585a 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)); } /** @@ -360,3 +362,43 @@ * @private */ export const BaseTextureCache = {}; + +/** + * 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/core/utils/trimCanvas.js b/src/core/utils/trimCanvas.js new file mode 100644 index 0000000..5af6de0 --- /dev/null +++ b/src/core/utils/trimCanvas.js @@ -0,0 +1,83 @@ +/** + * Trim transparent borders from a canvas + * + * @memberof PIXI + * @function trimCanvas + * @private + * @param {HTMLCanvasElement} canvas - the canvas to trim + * @returns {object} Trim data + */ +export default function trimCanvas(canvas) +{ + // https://gist.github.com/remy/784508 + + let width = canvas.width; + let height = canvas.height; + + const context = canvas.getContext('2d'); + const imageData = context.getImageData(0, 0, width, height); + const pixels = imageData.data; + const len = pixels.length; + + const bound = { + top: null, + left: null, + right: null, + bottom: null, + }; + let i; + let x; + let y; + + for (i = 0; i < len; i += 4) + { + if (pixels[i + 3] !== 0) + { + x = (i / 4) % width; + y = ~~((i / 4) / width); + + if (bound.top === null) + { + bound.top = y; + } + + if (bound.left === null) + { + bound.left = x; + } + else if (x < bound.left) + { + bound.left = x; + } + + if (bound.right === null) + { + bound.right = x + 1; + } + else if (bound.right < x) + { + bound.right = x + 1; + } + + if (bound.bottom === null) + { + bound.bottom = y; + } + else if (bound.bottom < y) + { + bound.bottom = y; + } + } + } + + width = bound.right - bound.left; + height = bound.bottom - bound.top + 1; + + const data = context.getImageData(bound.left, bound.top, width, height); + + return { + height, + width, + data, + }; +} diff --git a/src/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/extras/TilingSprite.js b/src/extras/TilingSprite.js index f90d248..fe82451 100644 --- a/src/extras/TilingSprite.js +++ b/src/extras/TilingSprite.js @@ -220,20 +220,33 @@ transform.tx * resolution, transform.ty * resolution); - // TODO - this should be rolled into the setTransform above.. - context.scale(this.tileScale.x / baseTextureResolution, this.tileScale.y / baseTextureResolution); - - context.translate(modX + (this.anchor.x * -this._width), - modY + (this.anchor.y * -this._height)); - renderer.setBlendMode(this.blendMode); // fill the pattern! context.fillStyle = this._canvasPattern; - context.fillRect(-modX, - -modY, - this._width / this.tileScale.x * baseTextureResolution, - this._height / this.tileScale.y * baseTextureResolution); + + // TODO - this should be rolled into the setTransform above.. + context.scale(this.tileScale.x / baseTextureResolution, this.tileScale.y / baseTextureResolution); + + const anchorX = this.anchor.x * -this._width; + const anchorY = this.anchor.y * -this._height; + + if (this.uvRespectAnchor) + { + context.translate(modX, modY); + + context.fillRect(-modX + anchorX, -modY + anchorY, + this._width / this.tileScale.x * baseTextureResolution, + this._height / this.tileScale.y * baseTextureResolution); + } + else + { + context.translate(modX + anchorX, modY + anchorY); + + context.fillRect(-modX, -modY, + this._width / this.tileScale.x * baseTextureResolution, + this._height / this.tileScale.y * baseTextureResolution); + } } /** diff --git a/package.json b/package.json index 1271fef..71ade15 100644 --- a/package.json +++ b/package.json @@ -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/scripts/release.js b/scripts/release.js index c4c8e36..156620c 100644 --- a/scripts/release.js +++ b/scripts/release.js @@ -12,7 +12,8 @@ 'dist/**/*', 'lib/**/*', 'src/**/*', - 'scripts/**/*', + 'scripts/*', + 'scripts/renders/*', 'test/**/*', '*.json', '*.md', diff --git a/scripts/renders/client.js b/scripts/renders/client.js index 16c9565..6faee34 100755 --- a/scripts/renders/client.js +++ b/scripts/renders/client.js @@ -1,6 +1,6 @@ 'use strict'; -require('../../bin/pixi'); +require('../../dist/pixi'); PIXI.utils.skipHello(); const fs = require('fs'); 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 bc730db..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. @@ -33,8 +33,11 @@ * need to call toDataUrl on the webgl context * @param {number} [options.resolution=1] - The resolution / device pixel ratio of the renderer, retina would be 2 * @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 @@ -43,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. */ @@ -73,7 +99,7 @@ */ stop() { - this.ticker.stop(); + this._ticker.stop(); } /** @@ -81,7 +107,7 @@ */ start() { - this.ticker.start(); + this._ticker.start(); } /** @@ -95,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 index 81e076b..78b17e6 100644 --- a/src/core/Shader.js +++ b/src/core/Shader.js @@ -1,9 +1,7 @@ import { GLShader } from 'pixi-gl-core'; import settings from './settings'; -const { PRECISION } = settings; - -function checkPrecision(src) +function checkPrecision(src, def) { if (src instanceof Array) { @@ -11,14 +9,14 @@ { const copy = src.slice(0); - copy.unshift(`precision ${PRECISION} float;`); + copy.unshift(`precision ${def} float;`); return copy; } } else if (src.substring(0, 9) !== 'precision') { - return `precision ${PRECISION} float;\n${src}`; + return `precision ${def} float;\n${src}`; } return src; @@ -42,6 +40,7 @@ */ constructor(gl, vertexSrc, fragmentSrc) { - super(gl, checkPrecision(vertexSrc), checkPrecision(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 3425b41..f9235e4 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -181,7 +181,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 b3792ef..17fde8d 100644 --- a/src/core/display/Container.js +++ b/src/core/display/Container.js @@ -73,13 +73,14 @@ } child.parent = this; - - // ensure a transform will be recalculated.. - this.transform._parentID = -1; - this._boundsID++; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.push(child); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(this.children.length - 1); child.emit('added', this); @@ -108,9 +109,14 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.splice(index, 0, child); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(index); child.emit('added', this); @@ -174,6 +180,7 @@ removeItems(this.children, currentIndex, 1); // remove from old position this.children.splice(index, 0, child); // add at new position + this.onChildrenChange(index); } @@ -220,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! @@ -244,9 +252,14 @@ { const child = this.getChildAt(index); + // ensure child transform will be recalculated.. child.parent = null; + child.transform._parentID = -1; removeItems(this.children, index, 1); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(index); child.emit('removed', this); @@ -275,8 +288,14 @@ for (let i = 0; i < removed.length; ++i) { removed[i].parent = null; + if (removed[i].transform) + { + removed[i].transform._parentID = -1; + } } + this._boundsID++; + this.onChildrenChange(beginIndex); for (let i = 0; i < removed.length; ++i) diff --git a/src/core/display/DisplayObject.js b/src/core/display/DisplayObject.js index 3affe36..4e6c77d 100644 --- a/src/core/display/DisplayObject.js +++ b/src/core/display/DisplayObject.js @@ -109,7 +109,7 @@ /** * The original, cached mask of the object * - * @member {PIXI.Rectangle} + * @member {PIXI.Graphics|PIXI.Sprite} * @private */ this._mask = null; diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index 95f2af4..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 ); @@ -1065,10 +1061,15 @@ canvasRenderer = new CanvasRenderer(); } - tempMatrix.tx = -bounds.x; - tempMatrix.ty = -bounds.y; + this.transform.updateLocalTransform(); + this.transform.localTransform.copy(tempMatrix); - canvasRenderer.render(this, canvasBuffer, false, tempMatrix); + tempMatrix.invert(); + + tempMatrix.tx -= bounds.x; + tempMatrix.ty -= bounds.y; + + canvasRenderer.render(this, canvasBuffer, true, tempMatrix); const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode); 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 e67680f..867dde2 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/buildCircle.js b/src/core/graphics/webgl/utils/buildCircle.js index 0400d13..abf5d02 100644 --- a/src/core/graphics/webgl/utils/buildCircle.js +++ b/src/core/graphics/webgl/utils/buildCircle.js @@ -33,6 +33,11 @@ height = circleData.height; } + if (width === 0 || height === 0) + { + return; + } + const totalSegs = Math.floor(30 * Math.sqrt(circleData.radius)) || Math.floor(15 * Math.sqrt(circleData.width + circleData.height)); diff --git a/src/core/graphics/webgl/utils/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 90155f9..f03fe15 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -30,6 +30,7 @@ export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; export { default as CanvasGraphicsRenderer } from './graphics/canvas/CanvasGraphicsRenderer'; +export { default as Spritesheet } from './textures/Spritesheet'; export { default as Texture } from './textures/Texture'; export { default as BaseTexture } from './textures/BaseTexture'; export { default as RenderTexture } from './textures/RenderTexture'; diff --git a/src/core/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 ccce680..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); } /** @@ -162,6 +162,9 @@ if (transform) { transform.copy(tempWt); + + // lets not forget to flag the parent transform as dirty... + this._tempDisplayObjectParent.transform._worldID = -1; } else { @@ -169,6 +172,7 @@ } displayObject.parent = this._tempDisplayObjectParent; + displayObject.updateTransform(); displayObject.parent = cacheParent; // displayObject.hitArea = //TODO add a temp hit area @@ -279,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/canvas/utils/CanvasRenderTarget.js b/src/core/renderers/canvas/utils/CanvasRenderTarget.js index 3de0aef..b409891 100644 --- a/src/core/renderers/canvas/utils/CanvasRenderTarget.js +++ b/src/core/renderers/canvas/utils/CanvasRenderTarget.js @@ -1,5 +1,4 @@ import settings from '../../../settings'; -const { RESOLUTION } = settings; /** * Creates a Canvas element of the given size. @@ -30,7 +29,7 @@ */ this.context = this.canvas.getContext('2d'); - this.resolution = resolution || RESOLUTION; + this.resolution = resolution || settings.RESOLUTION; this.resize(width, height); } 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 ce0e1c4..788ed6c 100644 --- a/src/core/renderers/webgl/WebGLRenderer.js +++ b/src/core/renderers/webgl/WebGLRenderer.js @@ -30,8 +30,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 @@ -49,10 +49,19 @@ * enable this if you need to call toDataUrl on the webgl context. * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when * rendering, stopping pixel interpolation. + * @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; + + if (this.legacy) + { + glCore.VertexArrayObject.FORCE_NATIVE = true; + } /** * The type of this renderer as a standardised const @@ -229,7 +238,7 @@ this.emit('context', gl); // setup the width/height properties and gl viewport - this.resize(this.width, this.height); + this.resize(this.screen.width, this.screen.height); } /** @@ -323,16 +332,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) { @@ -376,6 +385,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 @@ -440,9 +469,10 @@ * Changes the current shader to the one given in parameter * * @param {PIXI.Shader} shader - the new shader + * @param {boolean} [autoProject=true] - Whether automatically set the projection matrix * @return {PIXI.WebGLRenderer} Returns itself. */ - bindShader(shader) + bindShader(shader, autoProject) { // TODO cache if (this._activeShader !== shader) @@ -450,8 +480,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 bbab3b3..944f41d 100644 --- a/src/core/renderers/webgl/filters/Filter.js +++ b/src/core/renderers/webgl/filters/Filter.js @@ -1,6 +1,7 @@ import extractUniformsFromSrc from './extractUniformsFromSrc'; import { uid } from '../../../utils'; import { BLEND_MODES } from '../../../const'; +import settings from '../../../settings'; const SOURCE_KEY_MAP = {}; @@ -10,7 +11,7 @@ * @memberof PIXI * @extends PIXI.Shader */ -class Filter +export default class Filter { /** * @param {string} [vertexSrc] - The source of the vertex shader. @@ -35,8 +36,6 @@ this.blendMode = BLEND_MODES.NORMAL; - // pull out the vertex and shader uniforms if they are not specified.. - // currently this does not extract structs only default types this.uniformData = uniforms || extractUniformsFromSrc(this.vertexSrc, this.fragmentSrc, 'projectionMatrix|uSampler'); /** @@ -80,7 +79,7 @@ * * @member {number} */ - this.resolution = 1; + this.resolution = settings.RESOLUTION; /** * If enabled is true the filter is applied, if false it will not. @@ -97,8 +96,11 @@ * @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 { // --- // // this.uniforms.filterMatrix = filterManager.calculateSpriteMatrix(tempMatrix, window.panda ); @@ -170,5 +172,3 @@ ].join('\n'); } } - -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 dc7072a..6c7422d 100644 --- a/src/core/renderers/webgl/managers/FilterManager.js +++ b/src/core/renderers/webgl/managers/FilterManager.js @@ -154,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 @@ -176,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; @@ -184,7 +184,7 @@ flop = t; } - filters[i].apply(this, flip, lastState.renderTarget, true); + filters[i].apply(this, flip, lastState.renderTarget, false, currentState); this.freePotRenderTarget(flip); this.freePotRenderTarget(flop); @@ -291,9 +291,13 @@ let textureCount = 1; let currentState; - if (shader.uniforms.data.filterArea) + // 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; @@ -306,9 +310,9 @@ // use this to clamp displaced texture coords so they belong to filterArea // see displacementFilter fragment shader for an example - if (shader.uniforms.data.filterClamp) + if (shader.uniforms.filterClamp) { - currentState = this.filterData.stack[this.filterData.index]; + currentState = currentState || this.filterData.stack[this.filterData.index]; const filterClamp = shader.uniforms.filterClamp; @@ -485,7 +489,7 @@ */ destroy() { - this.shaderCache = []; + this.shaderCache = {}; this.emptyPool(); } diff --git a/src/core/renderers/webgl/utils/RenderTarget.js b/src/core/renderers/webgl/utils/RenderTarget.js index afe51ab..6d2467f 100644 --- a/src/core/renderers/webgl/utils/RenderTarget.js +++ b/src/core/renderers/webgl/utils/RenderTarget.js @@ -124,7 +124,7 @@ * @default PIXI.settings.SCALE_MODE * @see PIXI.SCALE_MODES */ - this.scaleMode = scaleMode || settings.SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; /** * Whether this object is the root element or not 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 36ea5bb..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; @@ -346,8 +346,8 @@ /** * Gets the local bounds of the sprite object. * - * @param {Rectangle} rect - The output rectangle. - * @return {Rectangle} The bounds. + * @param {PIXI.Rectangle} rect - The output rectangle. + * @return {PIXI.Rectangle} The bounds. */ getLocalBounds(rect) { @@ -440,7 +440,7 @@ * * @static * @param {number|string|PIXI.BaseTexture|HTMLCanvasElement|HTMLVideoElement} source Source to create texture from - * @return {PIXI.Texture} 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 e77b39a..90b08c0 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++) @@ -103,11 +102,18 @@ { const gl = this.renderer.gl; - // step 1: first check max textures the GPU can handle. - this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); + if (this.renderer.legacy) + { + this.MAX_TEXTURES = 1; + } + else + { + // step 1: first check max textures the GPU can handle. + this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); - // step 2: check the maximum number of if statements the shader can have too.. - this.MAX_TEXTURES = checkMaxIfStatmentsInShader(this.MAX_TEXTURES, gl); + // step 2: check the maximum number of if statements the shader can have too.. + this.MAX_TEXTURES = checkMaxIfStatmentsInShader(this.MAX_TEXTURES, gl); + } const shader = this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); @@ -130,8 +136,12 @@ .addIndex(this.indexBuffer) .addAttribute(this.vertexBuffers[i], shader.attributes.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) .addAttribute(this.vertexBuffers[i], shader.attributes.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) - .addAttribute(this.vertexBuffers[i], shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4) - .addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + .addAttribute(this.vertexBuffers[i], shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); + + if (shader.attributes.aTextureId) + { + this.vaos[i].addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + } /* eslint-enable max-len */ } @@ -378,8 +388,12 @@ .addIndex(this.indexBuffer) .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) - .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4) - .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); + + if (this.shader.attributes.aTextureId) + { + this.vaos[this.vertexCount].addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + } /* eslint-enable max-len */ } diff --git a/src/core/text/Text.js b/src/core/text/Text.js index 013625a..cec8c32 100644 --- a/src/core/text/Text.js +++ b/src/core/text/Text.js @@ -6,6 +6,7 @@ import { TEXT_GRADIENT } from '../const'; import settings from '../settings'; import TextStyle from './TextStyle'; +import trimCanvas from '../utils/trimCanvas'; const defaultDestroyOptions = { texture: true, @@ -57,7 +58,7 @@ /** * The canvas 2d context that everything is drawn with - * @member {HTMLCanvasElement} + * @member {CanvasRenderingContext2D} */ this.context = this.canvas.getContext('2d'); @@ -189,10 +190,12 @@ if (style.dropShadow) { + this.context.shadowBlur = style.dropShadowBlur; + this.context.globalAlpha = style.dropShadowAlpha; + if (style.dropShadowBlur > 0) { this.context.shadowColor = style.dropShadowColor; - this.context.shadowBlur = style.dropShadowBlur; } else { @@ -237,6 +240,10 @@ } } + // reset the shadow blur and alpha that was set by the drop shadow, for the regular text + this.context.shadowBlur = 0; + this.context.globalAlpha = 1; + // set canvas text styles this.context.fillStyle = this._generateFillStyle(style, lines); @@ -326,6 +333,15 @@ */ updateTexture() { + if (this._style.trim) + { + const trimmed = trimCanvas(this.canvas); + + this.canvas.width = trimmed.width; + this.canvas.height = trimmed.height; + this.context.putImageData(trimmed.data, 0, 0); + } + const texture = this._texture; const style = this._style; @@ -475,6 +491,19 @@ } /** + * Gets the local bounds of the text object. + * + * @param {Rectangle} rect - The output rectangle. + * @return {Rectangle} The bounds. + */ + getLocalBounds(rect) + { + this.updateText(true); + + return super.getLocalBounds.call(this, rect); + } + + /** * calculates the bounds of the Text as a rectangle. The bounds calculation takes the worldTransform into account. */ _calculateBounds() @@ -525,6 +554,29 @@ const width = this.canvas.width / this.resolution; const height = this.canvas.height / this.resolution; + // make a copy of the style settings, so we can manipulate them later + const fill = style.fill.slice(); + const fillGradientStops = style.fillGradientStops.slice(); + + // wanting to evenly distribute the fills. So an array of 4 colours should give fills of 0.25, 0.5 and 0.75 + if (!fillGradientStops.length) + { + const lengthPlus1 = fill.length + 1; + + for (let i = 1; i < lengthPlus1; ++i) + { + fillGradientStops.push(i / lengthPlus1); + } + } + + // stop the bleeding of the last gradient on the line above to the top gradient of the this line + // by hard defining the first gradient colour at point 0, and last gradient colour at point 1 + fill.unshift(style.fill[0]); + fillGradientStops.unshift(0); + + fill.push(style.fill[style.fill.length - 1]); + fillGradientStops.push(1); + if (style.fillGradientType === TEXT_GRADIENT.LINEAR_VERTICAL) { // start the gradient at the top center of the canvas, and end at the bottom middle of the canvas @@ -532,15 +584,22 @@ // we need to repeat the gradient so that each individual line of text has the same vertical gradient effect // ['#FF0000', '#00FF00', '#0000FF'] over 2 lines would create stops at 0.125, 0.25, 0.375, 0.625, 0.75, 0.875 - totalIterations = (style.fill.length + 1) * lines.length; + totalIterations = (fill.length + 1) * lines.length; currentIteration = 0; for (let i = 0; i < lines.length; i++) { currentIteration += 1; - for (let j = 0; j < style.fill.length; j++) + for (let j = 0; j < fill.length; j++) { - stop = (currentIteration / totalIterations); - gradient.addColorStop(stop, style.fill[j]); + if (fillGradientStops[j]) + { + stop = (fillGradientStops[j] / lines.length) + (i / lines.length); + } + else + { + stop = currentIteration / totalIterations; + } + gradient.addColorStop(stop, fill[j]); currentIteration++; } } @@ -552,13 +611,20 @@ // can just evenly space out the gradients in this case, as multiple lines makes no difference // to an even left to right gradient - totalIterations = style.fill.length + 1; + totalIterations = fill.length + 1; currentIteration = 1; - for (let i = 0; i < style.fill.length; i++) + for (let i = 0; i < fill.length; i++) { - stop = currentIteration / totalIterations; - gradient.addColorStop(stop, style.fill[i]); + if (fillGradientStops[i]) + { + stop = fillGradientStops[i]; + } + else + { + stop = currentIteration / totalIterations; + } + gradient.addColorStop(stop, fill[i]); currentIteration++; } } @@ -569,7 +635,7 @@ /** * Destroys this text object. * Note* Unlike a Sprite, a Text object will automatically destroy its baseTexture and texture as - * the majorety of the time the texture will not be shared with any other Sprites. + * the majority of the time the texture will not be shared with any other Sprites. * * @param {object|boolean} [options] - Options parameter. A boolean will act as if all options * have been set to that value @@ -680,7 +746,7 @@ set text(text) // eslint-disable-line require-jsdoc { - text = String(text || ' '); + text = String(text === '' || text === null || text === undefined ? ' ' : text); if (this._text === text) { diff --git a/src/core/text/TextStyle.js b/src/core/text/TextStyle.js index 3b25528..890950b 100644 --- a/src/core/text/TextStyle.js +++ b/src/core/text/TextStyle.js @@ -8,12 +8,14 @@ align: 'left', breakWords: false, dropShadow: false, + dropShadowAlpha: 1, dropShadowAngle: Math.PI / 6, dropShadowBlur: 0, dropShadowColor: '#000000', dropShadowDistance: 5, fill: 'black', fillGradientType: TEXT_GRADIENT.LINEAR_VERTICAL, + fillGradientStops: [], fontFamily: 'Arial', fontSize: 26, fontStyle: 'normal', @@ -27,6 +29,7 @@ stroke: 'black', strokeThickness: 0, textBaseline: 'alphabetic', + trim: false, wordWrap: false, wordWrapWidth: 100, }; @@ -47,6 +50,7 @@ * @param {boolean} [style.breakWords=false] - Indicates if lines can be wrapped within words, it * needs wordWrap to be set to true * @param {boolean} [style.dropShadow=false] - Set a drop shadow for the text + * @param {number} [style.dropShadowAlpha=1] - Set alpha for the drop shadow * @param {number} [style.dropShadowAngle=Math.PI/6] - Set a angle of the drop shadow * @param {number} [style.dropShadowBlur=0] - Set a shadow blur radius * @param {string} [style.dropShadowColor='#000000'] - A fill style to be used on the dropshadow e.g 'red', '#00FF00' @@ -55,8 +59,10 @@ * fillstyle that will be used on the text e.g 'red', '#00FF00'. Can be an array to create a gradient * eg ['#000000','#FFFFFF'] * {@link https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/fillStyle|MDN} - * @param {number} [style.fillGradientType=PIXI.TEXT_GRADIENT.LINEAR_VERTICAL] - If fills styles are - * supplied, this can change the type/direction of the gradient. See {@link PIXI.TEXT_GRADIENT} for possible values + * @param {number} [style.fillGradientType=PIXI.TEXT_GRADIENT.LINEAR_VERTICAL] - If fill is an array of colours + * to create a gradient, this can change the type/direction of the gradient. See {@link PIXI.TEXT_GRADIENT} + * @param {number[]} [style.fillGradientStops] - If fill is an array of colours to create a gradient, this array can set + * the stop points (numbers between 0 and 1) for the color, overriding the default behaviour of evenly spacing them. * @param {string|string[]} [style.fontFamily='Arial'] - The font family * @param {number|string} [style.fontSize=26] - The font size (as a number it converts to px, but as a string, * equivalents are '26px','20pt','160%' or '1.6em') @@ -76,6 +82,7 @@ * e.g 'blue', '#FCFF00' * @param {number} [style.strokeThickness=0] - A number that represents the thickness of the stroke. * Default is 0 (no stroke) + * @param {boolean} [style.trim=false] - Trim transparent borders * @param {string} [style.textBaseline='alphabetic'] - The baseline of the text that is rendered. * @param {boolean} [style.wordWrap=false] - Indicates if word wrap should be used * @param {number} [style.wordWrapWidth=100] - The width at which text will wrap, it needs wordWrap to be set to true @@ -152,6 +159,19 @@ } } + get dropShadowAlpha() + { + return this._dropShadowAlpha; + } + set dropShadowAlpha(dropShadowAlpha) + { + if (this._dropShadowAlpha !== dropShadowAlpha) + { + this._dropShadowAlpha = dropShadowAlpha; + this.styleID++; + } + } + get dropShadowAngle() { return this._dropShadowAngle; @@ -232,6 +252,19 @@ } } + get fillGradientStops() + { + return this._fillGradientStops; + } + set fillGradientStops(fillGradientStops) + { + if (!areArraysEqual(this._fillGradientStops,fillGradientStops)) + { + this._fillGradientStops = fillGradientStops; + this.styleID++; + } + } + get fontFamily() { return this._fontFamily; @@ -402,6 +435,19 @@ } } + get trim() + { + return this._trim; + } + set trim(trim) + { + if (this._trim !== trim) + { + this._trim = trim; + this.styleID++; + } + } + get wordWrap() { return this._wordWrap; @@ -475,3 +521,34 @@ return color; } } + +/** + * Utility function to convert hexadecimal colors to strings, and simply return the color if it's a string. + * This version can also convert array of colors + * + * @param {Array} array1 First array to compare + * @param {Array} array2 Second array to compare + * @return {boolean} Do the arrays contain the same values in the same order + */ +function areArraysEqual(array1, array2) +{ + if (!Array.isArray(array1) || !Array.isArray(array2)) + { + return false; + } + + if (array1.length !== array2.length) + { + return false; + } + + for (let i = 0; i < array1.length; ++i) + { + if (array1[i] !== array2[i]) + { + return false; + } + } + + return true; +} diff --git a/src/core/textures/BaseRenderTexture.js b/src/core/textures/BaseRenderTexture.js index 5d50146..aa727f0 100644 --- a/src/core/textures/BaseRenderTexture.js +++ b/src/core/textures/BaseRenderTexture.js @@ -1,8 +1,6 @@ import BaseTexture from './BaseTexture'; import settings from '../settings'; -const { RESOLUTION, SCALE_MODE } = settings; - /** * A BaseRenderTexture is a special texture that allows any Pixi display object to be rendered to it. * @@ -54,7 +52,7 @@ { super(null, scaleMode); - this.resolution = resolution || RESOLUTION; + this.resolution = resolution || settings.RESOLUTION; this.width = width; this.height = height; @@ -62,7 +60,7 @@ this.realWidth = this.width * this.resolution; this.realHeight = this.height * this.resolution; - this.scaleMode = scaleMode || SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; this.hasLoaded = true; /** diff --git a/src/core/textures/BaseTexture.js b/src/core/textures/BaseTexture.js index 676dca3..aeccf39 100644 --- a/src/core/textures/BaseTexture.js +++ b/src/core/textures/BaseTexture.js @@ -77,7 +77,7 @@ * @default PIXI.settings.SCALE_MODE * @see PIXI.SCALE_MODES */ - this.scaleMode = scaleMode || settings.SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; /** * Set to true once the base texture has successfully loaded. @@ -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/RenderTexture.js b/src/core/textures/RenderTexture.js index 4c5034f..b3c6297 100644 --- a/src/core/textures/RenderTexture.js +++ b/src/core/textures/RenderTexture.js @@ -54,8 +54,8 @@ /* eslint-disable prefer-rest-params, no-console */ const width = arguments[1]; const height = arguments[2]; - const scaleMode = arguments[3] || 0; - const resolution = arguments[4] || 1; + const scaleMode = arguments[3]; + const resolution = arguments[4]; // we have an old render texture.. console.warn(`Please use RenderTexture.create(${width}, ${height}) instead of the ctor directly.`); 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 ca31456..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 @@ -459,7 +497,9 @@ if (frame.x + frame.width > this.baseTexture.width || frame.y + frame.height > this.baseTexture.height) { - throw new Error(`Texture Error: frame does not fit inside the base Texture dimensions ${this}`); + throw new Error('Texture Error: frame does not fit inside the base Texture dimensions: ' + + `X: ${frame.x} + ${frame.width} > ${this.baseTexture.width} ` + + `Y: ${frame.y} + ${frame.height} > ${this.baseTexture.height}`); } // this.valid = frame && frame.width && frame.height && this.baseTexture.source && this.baseTexture.hasLoaded; @@ -520,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. @@ -528,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 2a7497b..99e585a 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)); } /** @@ -360,3 +362,43 @@ * @private */ export const BaseTextureCache = {}; + +/** + * 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/core/utils/trimCanvas.js b/src/core/utils/trimCanvas.js new file mode 100644 index 0000000..5af6de0 --- /dev/null +++ b/src/core/utils/trimCanvas.js @@ -0,0 +1,83 @@ +/** + * Trim transparent borders from a canvas + * + * @memberof PIXI + * @function trimCanvas + * @private + * @param {HTMLCanvasElement} canvas - the canvas to trim + * @returns {object} Trim data + */ +export default function trimCanvas(canvas) +{ + // https://gist.github.com/remy/784508 + + let width = canvas.width; + let height = canvas.height; + + const context = canvas.getContext('2d'); + const imageData = context.getImageData(0, 0, width, height); + const pixels = imageData.data; + const len = pixels.length; + + const bound = { + top: null, + left: null, + right: null, + bottom: null, + }; + let i; + let x; + let y; + + for (i = 0; i < len; i += 4) + { + if (pixels[i + 3] !== 0) + { + x = (i / 4) % width; + y = ~~((i / 4) / width); + + if (bound.top === null) + { + bound.top = y; + } + + if (bound.left === null) + { + bound.left = x; + } + else if (x < bound.left) + { + bound.left = x; + } + + if (bound.right === null) + { + bound.right = x + 1; + } + else if (bound.right < x) + { + bound.right = x + 1; + } + + if (bound.bottom === null) + { + bound.bottom = y; + } + else if (bound.bottom < y) + { + bound.bottom = y; + } + } + } + + width = bound.right - bound.left; + height = bound.bottom - bound.top + 1; + + const data = context.getImageData(bound.left, bound.top, width, height); + + return { + height, + width, + data, + }; +} diff --git a/src/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/extras/TilingSprite.js b/src/extras/TilingSprite.js index f90d248..fe82451 100644 --- a/src/extras/TilingSprite.js +++ b/src/extras/TilingSprite.js @@ -220,20 +220,33 @@ transform.tx * resolution, transform.ty * resolution); - // TODO - this should be rolled into the setTransform above.. - context.scale(this.tileScale.x / baseTextureResolution, this.tileScale.y / baseTextureResolution); - - context.translate(modX + (this.anchor.x * -this._width), - modY + (this.anchor.y * -this._height)); - renderer.setBlendMode(this.blendMode); // fill the pattern! context.fillStyle = this._canvasPattern; - context.fillRect(-modX, - -modY, - this._width / this.tileScale.x * baseTextureResolution, - this._height / this.tileScale.y * baseTextureResolution); + + // TODO - this should be rolled into the setTransform above.. + context.scale(this.tileScale.x / baseTextureResolution, this.tileScale.y / baseTextureResolution); + + const anchorX = this.anchor.x * -this._width; + const anchorY = this.anchor.y * -this._height; + + if (this.uvRespectAnchor) + { + context.translate(modX, modY); + + context.fillRect(-modX + anchorX, -modY + anchorY, + this._width / this.tileScale.x * baseTextureResolution, + this._height / this.tileScale.y * baseTextureResolution); + } + else + { + context.translate(modX + anchorX, modY + anchorY); + + context.fillRect(-modX, -modY, + this._width / this.tileScale.x * baseTextureResolution, + this._height / this.tileScale.y * baseTextureResolution); + } } /** diff --git a/src/extras/cacheAsBitmap.js b/src/extras/cacheAsBitmap.js index 18043a7..5dd5640 100644 --- a/src/extras/cacheAsBitmap.js +++ b/src/extras/cacheAsBitmap.js @@ -41,6 +41,9 @@ * provide a performance benefit for complex static displayObjects. * To remove simply set this property to 'false' * + * IMPORTANT GOTCHA - make sure that all your textures are preloaded BEFORE setting this property to true + * as it will take a snapshot of what is currently there. If the textures have not loaded then they will not appear. + * * @member {boolean} * @memberof PIXI.DisplayObject# */ diff --git a/package.json b/package.json index 1271fef..71ade15 100644 --- a/package.json +++ b/package.json @@ -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/scripts/release.js b/scripts/release.js index c4c8e36..156620c 100644 --- a/scripts/release.js +++ b/scripts/release.js @@ -12,7 +12,8 @@ 'dist/**/*', 'lib/**/*', 'src/**/*', - 'scripts/**/*', + 'scripts/*', + 'scripts/renders/*', 'test/**/*', '*.json', '*.md', diff --git a/scripts/renders/client.js b/scripts/renders/client.js index 16c9565..6faee34 100755 --- a/scripts/renders/client.js +++ b/scripts/renders/client.js @@ -1,6 +1,6 @@ 'use strict'; -require('../../bin/pixi'); +require('../../dist/pixi'); PIXI.utils.skipHello(); const fs = require('fs'); 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 bc730db..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. @@ -33,8 +33,11 @@ * need to call toDataUrl on the webgl context * @param {number} [options.resolution=1] - The resolution / device pixel ratio of the renderer, retina would be 2 * @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 @@ -43,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. */ @@ -73,7 +99,7 @@ */ stop() { - this.ticker.stop(); + this._ticker.stop(); } /** @@ -81,7 +107,7 @@ */ start() { - this.ticker.start(); + this._ticker.start(); } /** @@ -95,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 index 81e076b..78b17e6 100644 --- a/src/core/Shader.js +++ b/src/core/Shader.js @@ -1,9 +1,7 @@ import { GLShader } from 'pixi-gl-core'; import settings from './settings'; -const { PRECISION } = settings; - -function checkPrecision(src) +function checkPrecision(src, def) { if (src instanceof Array) { @@ -11,14 +9,14 @@ { const copy = src.slice(0); - copy.unshift(`precision ${PRECISION} float;`); + copy.unshift(`precision ${def} float;`); return copy; } } else if (src.substring(0, 9) !== 'precision') { - return `precision ${PRECISION} float;\n${src}`; + return `precision ${def} float;\n${src}`; } return src; @@ -42,6 +40,7 @@ */ constructor(gl, vertexSrc, fragmentSrc) { - super(gl, checkPrecision(vertexSrc), checkPrecision(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 3425b41..f9235e4 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -181,7 +181,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 b3792ef..17fde8d 100644 --- a/src/core/display/Container.js +++ b/src/core/display/Container.js @@ -73,13 +73,14 @@ } child.parent = this; - - // ensure a transform will be recalculated.. - this.transform._parentID = -1; - this._boundsID++; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.push(child); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(this.children.length - 1); child.emit('added', this); @@ -108,9 +109,14 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.splice(index, 0, child); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(index); child.emit('added', this); @@ -174,6 +180,7 @@ removeItems(this.children, currentIndex, 1); // remove from old position this.children.splice(index, 0, child); // add at new position + this.onChildrenChange(index); } @@ -220,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! @@ -244,9 +252,14 @@ { const child = this.getChildAt(index); + // ensure child transform will be recalculated.. child.parent = null; + child.transform._parentID = -1; removeItems(this.children, index, 1); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(index); child.emit('removed', this); @@ -275,8 +288,14 @@ for (let i = 0; i < removed.length; ++i) { removed[i].parent = null; + if (removed[i].transform) + { + removed[i].transform._parentID = -1; + } } + this._boundsID++; + this.onChildrenChange(beginIndex); for (let i = 0; i < removed.length; ++i) diff --git a/src/core/display/DisplayObject.js b/src/core/display/DisplayObject.js index 3affe36..4e6c77d 100644 --- a/src/core/display/DisplayObject.js +++ b/src/core/display/DisplayObject.js @@ -109,7 +109,7 @@ /** * The original, cached mask of the object * - * @member {PIXI.Rectangle} + * @member {PIXI.Graphics|PIXI.Sprite} * @private */ this._mask = null; diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index 95f2af4..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 ); @@ -1065,10 +1061,15 @@ canvasRenderer = new CanvasRenderer(); } - tempMatrix.tx = -bounds.x; - tempMatrix.ty = -bounds.y; + this.transform.updateLocalTransform(); + this.transform.localTransform.copy(tempMatrix); - canvasRenderer.render(this, canvasBuffer, false, tempMatrix); + tempMatrix.invert(); + + tempMatrix.tx -= bounds.x; + tempMatrix.ty -= bounds.y; + + canvasRenderer.render(this, canvasBuffer, true, tempMatrix); const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode); 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 e67680f..867dde2 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/buildCircle.js b/src/core/graphics/webgl/utils/buildCircle.js index 0400d13..abf5d02 100644 --- a/src/core/graphics/webgl/utils/buildCircle.js +++ b/src/core/graphics/webgl/utils/buildCircle.js @@ -33,6 +33,11 @@ height = circleData.height; } + if (width === 0 || height === 0) + { + return; + } + const totalSegs = Math.floor(30 * Math.sqrt(circleData.radius)) || Math.floor(15 * Math.sqrt(circleData.width + circleData.height)); diff --git a/src/core/graphics/webgl/utils/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 90155f9..f03fe15 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -30,6 +30,7 @@ export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; export { default as CanvasGraphicsRenderer } from './graphics/canvas/CanvasGraphicsRenderer'; +export { default as Spritesheet } from './textures/Spritesheet'; export { default as Texture } from './textures/Texture'; export { default as BaseTexture } from './textures/BaseTexture'; export { default as RenderTexture } from './textures/RenderTexture'; diff --git a/src/core/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 ccce680..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); } /** @@ -162,6 +162,9 @@ if (transform) { transform.copy(tempWt); + + // lets not forget to flag the parent transform as dirty... + this._tempDisplayObjectParent.transform._worldID = -1; } else { @@ -169,6 +172,7 @@ } displayObject.parent = this._tempDisplayObjectParent; + displayObject.updateTransform(); displayObject.parent = cacheParent; // displayObject.hitArea = //TODO add a temp hit area @@ -279,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/canvas/utils/CanvasRenderTarget.js b/src/core/renderers/canvas/utils/CanvasRenderTarget.js index 3de0aef..b409891 100644 --- a/src/core/renderers/canvas/utils/CanvasRenderTarget.js +++ b/src/core/renderers/canvas/utils/CanvasRenderTarget.js @@ -1,5 +1,4 @@ import settings from '../../../settings'; -const { RESOLUTION } = settings; /** * Creates a Canvas element of the given size. @@ -30,7 +29,7 @@ */ this.context = this.canvas.getContext('2d'); - this.resolution = resolution || RESOLUTION; + this.resolution = resolution || settings.RESOLUTION; this.resize(width, height); } 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 ce0e1c4..788ed6c 100644 --- a/src/core/renderers/webgl/WebGLRenderer.js +++ b/src/core/renderers/webgl/WebGLRenderer.js @@ -30,8 +30,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 @@ -49,10 +49,19 @@ * enable this if you need to call toDataUrl on the webgl context. * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when * rendering, stopping pixel interpolation. + * @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; + + if (this.legacy) + { + glCore.VertexArrayObject.FORCE_NATIVE = true; + } /** * The type of this renderer as a standardised const @@ -229,7 +238,7 @@ this.emit('context', gl); // setup the width/height properties and gl viewport - this.resize(this.width, this.height); + this.resize(this.screen.width, this.screen.height); } /** @@ -323,16 +332,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) { @@ -376,6 +385,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 @@ -440,9 +469,10 @@ * Changes the current shader to the one given in parameter * * @param {PIXI.Shader} shader - the new shader + * @param {boolean} [autoProject=true] - Whether automatically set the projection matrix * @return {PIXI.WebGLRenderer} Returns itself. */ - bindShader(shader) + bindShader(shader, autoProject) { // TODO cache if (this._activeShader !== shader) @@ -450,8 +480,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 bbab3b3..944f41d 100644 --- a/src/core/renderers/webgl/filters/Filter.js +++ b/src/core/renderers/webgl/filters/Filter.js @@ -1,6 +1,7 @@ import extractUniformsFromSrc from './extractUniformsFromSrc'; import { uid } from '../../../utils'; import { BLEND_MODES } from '../../../const'; +import settings from '../../../settings'; const SOURCE_KEY_MAP = {}; @@ -10,7 +11,7 @@ * @memberof PIXI * @extends PIXI.Shader */ -class Filter +export default class Filter { /** * @param {string} [vertexSrc] - The source of the vertex shader. @@ -35,8 +36,6 @@ this.blendMode = BLEND_MODES.NORMAL; - // pull out the vertex and shader uniforms if they are not specified.. - // currently this does not extract structs only default types this.uniformData = uniforms || extractUniformsFromSrc(this.vertexSrc, this.fragmentSrc, 'projectionMatrix|uSampler'); /** @@ -80,7 +79,7 @@ * * @member {number} */ - this.resolution = 1; + this.resolution = settings.RESOLUTION; /** * If enabled is true the filter is applied, if false it will not. @@ -97,8 +96,11 @@ * @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 { // --- // // this.uniforms.filterMatrix = filterManager.calculateSpriteMatrix(tempMatrix, window.panda ); @@ -170,5 +172,3 @@ ].join('\n'); } } - -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 dc7072a..6c7422d 100644 --- a/src/core/renderers/webgl/managers/FilterManager.js +++ b/src/core/renderers/webgl/managers/FilterManager.js @@ -154,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 @@ -176,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; @@ -184,7 +184,7 @@ flop = t; } - filters[i].apply(this, flip, lastState.renderTarget, true); + filters[i].apply(this, flip, lastState.renderTarget, false, currentState); this.freePotRenderTarget(flip); this.freePotRenderTarget(flop); @@ -291,9 +291,13 @@ let textureCount = 1; let currentState; - if (shader.uniforms.data.filterArea) + // 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; @@ -306,9 +310,9 @@ // use this to clamp displaced texture coords so they belong to filterArea // see displacementFilter fragment shader for an example - if (shader.uniforms.data.filterClamp) + if (shader.uniforms.filterClamp) { - currentState = this.filterData.stack[this.filterData.index]; + currentState = currentState || this.filterData.stack[this.filterData.index]; const filterClamp = shader.uniforms.filterClamp; @@ -485,7 +489,7 @@ */ destroy() { - this.shaderCache = []; + this.shaderCache = {}; this.emptyPool(); } diff --git a/src/core/renderers/webgl/utils/RenderTarget.js b/src/core/renderers/webgl/utils/RenderTarget.js index afe51ab..6d2467f 100644 --- a/src/core/renderers/webgl/utils/RenderTarget.js +++ b/src/core/renderers/webgl/utils/RenderTarget.js @@ -124,7 +124,7 @@ * @default PIXI.settings.SCALE_MODE * @see PIXI.SCALE_MODES */ - this.scaleMode = scaleMode || settings.SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; /** * Whether this object is the root element or not 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 36ea5bb..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; @@ -346,8 +346,8 @@ /** * Gets the local bounds of the sprite object. * - * @param {Rectangle} rect - The output rectangle. - * @return {Rectangle} The bounds. + * @param {PIXI.Rectangle} rect - The output rectangle. + * @return {PIXI.Rectangle} The bounds. */ getLocalBounds(rect) { @@ -440,7 +440,7 @@ * * @static * @param {number|string|PIXI.BaseTexture|HTMLCanvasElement|HTMLVideoElement} source Source to create texture from - * @return {PIXI.Texture} 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 e77b39a..90b08c0 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++) @@ -103,11 +102,18 @@ { const gl = this.renderer.gl; - // step 1: first check max textures the GPU can handle. - this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); + if (this.renderer.legacy) + { + this.MAX_TEXTURES = 1; + } + else + { + // step 1: first check max textures the GPU can handle. + this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); - // step 2: check the maximum number of if statements the shader can have too.. - this.MAX_TEXTURES = checkMaxIfStatmentsInShader(this.MAX_TEXTURES, gl); + // step 2: check the maximum number of if statements the shader can have too.. + this.MAX_TEXTURES = checkMaxIfStatmentsInShader(this.MAX_TEXTURES, gl); + } const shader = this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); @@ -130,8 +136,12 @@ .addIndex(this.indexBuffer) .addAttribute(this.vertexBuffers[i], shader.attributes.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) .addAttribute(this.vertexBuffers[i], shader.attributes.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) - .addAttribute(this.vertexBuffers[i], shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4) - .addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + .addAttribute(this.vertexBuffers[i], shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); + + if (shader.attributes.aTextureId) + { + this.vaos[i].addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + } /* eslint-enable max-len */ } @@ -378,8 +388,12 @@ .addIndex(this.indexBuffer) .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) - .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4) - .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); + + if (this.shader.attributes.aTextureId) + { + this.vaos[this.vertexCount].addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + } /* eslint-enable max-len */ } diff --git a/src/core/text/Text.js b/src/core/text/Text.js index 013625a..cec8c32 100644 --- a/src/core/text/Text.js +++ b/src/core/text/Text.js @@ -6,6 +6,7 @@ import { TEXT_GRADIENT } from '../const'; import settings from '../settings'; import TextStyle from './TextStyle'; +import trimCanvas from '../utils/trimCanvas'; const defaultDestroyOptions = { texture: true, @@ -57,7 +58,7 @@ /** * The canvas 2d context that everything is drawn with - * @member {HTMLCanvasElement} + * @member {CanvasRenderingContext2D} */ this.context = this.canvas.getContext('2d'); @@ -189,10 +190,12 @@ if (style.dropShadow) { + this.context.shadowBlur = style.dropShadowBlur; + this.context.globalAlpha = style.dropShadowAlpha; + if (style.dropShadowBlur > 0) { this.context.shadowColor = style.dropShadowColor; - this.context.shadowBlur = style.dropShadowBlur; } else { @@ -237,6 +240,10 @@ } } + // reset the shadow blur and alpha that was set by the drop shadow, for the regular text + this.context.shadowBlur = 0; + this.context.globalAlpha = 1; + // set canvas text styles this.context.fillStyle = this._generateFillStyle(style, lines); @@ -326,6 +333,15 @@ */ updateTexture() { + if (this._style.trim) + { + const trimmed = trimCanvas(this.canvas); + + this.canvas.width = trimmed.width; + this.canvas.height = trimmed.height; + this.context.putImageData(trimmed.data, 0, 0); + } + const texture = this._texture; const style = this._style; @@ -475,6 +491,19 @@ } /** + * Gets the local bounds of the text object. + * + * @param {Rectangle} rect - The output rectangle. + * @return {Rectangle} The bounds. + */ + getLocalBounds(rect) + { + this.updateText(true); + + return super.getLocalBounds.call(this, rect); + } + + /** * calculates the bounds of the Text as a rectangle. The bounds calculation takes the worldTransform into account. */ _calculateBounds() @@ -525,6 +554,29 @@ const width = this.canvas.width / this.resolution; const height = this.canvas.height / this.resolution; + // make a copy of the style settings, so we can manipulate them later + const fill = style.fill.slice(); + const fillGradientStops = style.fillGradientStops.slice(); + + // wanting to evenly distribute the fills. So an array of 4 colours should give fills of 0.25, 0.5 and 0.75 + if (!fillGradientStops.length) + { + const lengthPlus1 = fill.length + 1; + + for (let i = 1; i < lengthPlus1; ++i) + { + fillGradientStops.push(i / lengthPlus1); + } + } + + // stop the bleeding of the last gradient on the line above to the top gradient of the this line + // by hard defining the first gradient colour at point 0, and last gradient colour at point 1 + fill.unshift(style.fill[0]); + fillGradientStops.unshift(0); + + fill.push(style.fill[style.fill.length - 1]); + fillGradientStops.push(1); + if (style.fillGradientType === TEXT_GRADIENT.LINEAR_VERTICAL) { // start the gradient at the top center of the canvas, and end at the bottom middle of the canvas @@ -532,15 +584,22 @@ // we need to repeat the gradient so that each individual line of text has the same vertical gradient effect // ['#FF0000', '#00FF00', '#0000FF'] over 2 lines would create stops at 0.125, 0.25, 0.375, 0.625, 0.75, 0.875 - totalIterations = (style.fill.length + 1) * lines.length; + totalIterations = (fill.length + 1) * lines.length; currentIteration = 0; for (let i = 0; i < lines.length; i++) { currentIteration += 1; - for (let j = 0; j < style.fill.length; j++) + for (let j = 0; j < fill.length; j++) { - stop = (currentIteration / totalIterations); - gradient.addColorStop(stop, style.fill[j]); + if (fillGradientStops[j]) + { + stop = (fillGradientStops[j] / lines.length) + (i / lines.length); + } + else + { + stop = currentIteration / totalIterations; + } + gradient.addColorStop(stop, fill[j]); currentIteration++; } } @@ -552,13 +611,20 @@ // can just evenly space out the gradients in this case, as multiple lines makes no difference // to an even left to right gradient - totalIterations = style.fill.length + 1; + totalIterations = fill.length + 1; currentIteration = 1; - for (let i = 0; i < style.fill.length; i++) + for (let i = 0; i < fill.length; i++) { - stop = currentIteration / totalIterations; - gradient.addColorStop(stop, style.fill[i]); + if (fillGradientStops[i]) + { + stop = fillGradientStops[i]; + } + else + { + stop = currentIteration / totalIterations; + } + gradient.addColorStop(stop, fill[i]); currentIteration++; } } @@ -569,7 +635,7 @@ /** * Destroys this text object. * Note* Unlike a Sprite, a Text object will automatically destroy its baseTexture and texture as - * the majorety of the time the texture will not be shared with any other Sprites. + * the majority of the time the texture will not be shared with any other Sprites. * * @param {object|boolean} [options] - Options parameter. A boolean will act as if all options * have been set to that value @@ -680,7 +746,7 @@ set text(text) // eslint-disable-line require-jsdoc { - text = String(text || ' '); + text = String(text === '' || text === null || text === undefined ? ' ' : text); if (this._text === text) { diff --git a/src/core/text/TextStyle.js b/src/core/text/TextStyle.js index 3b25528..890950b 100644 --- a/src/core/text/TextStyle.js +++ b/src/core/text/TextStyle.js @@ -8,12 +8,14 @@ align: 'left', breakWords: false, dropShadow: false, + dropShadowAlpha: 1, dropShadowAngle: Math.PI / 6, dropShadowBlur: 0, dropShadowColor: '#000000', dropShadowDistance: 5, fill: 'black', fillGradientType: TEXT_GRADIENT.LINEAR_VERTICAL, + fillGradientStops: [], fontFamily: 'Arial', fontSize: 26, fontStyle: 'normal', @@ -27,6 +29,7 @@ stroke: 'black', strokeThickness: 0, textBaseline: 'alphabetic', + trim: false, wordWrap: false, wordWrapWidth: 100, }; @@ -47,6 +50,7 @@ * @param {boolean} [style.breakWords=false] - Indicates if lines can be wrapped within words, it * needs wordWrap to be set to true * @param {boolean} [style.dropShadow=false] - Set a drop shadow for the text + * @param {number} [style.dropShadowAlpha=1] - Set alpha for the drop shadow * @param {number} [style.dropShadowAngle=Math.PI/6] - Set a angle of the drop shadow * @param {number} [style.dropShadowBlur=0] - Set a shadow blur radius * @param {string} [style.dropShadowColor='#000000'] - A fill style to be used on the dropshadow e.g 'red', '#00FF00' @@ -55,8 +59,10 @@ * fillstyle that will be used on the text e.g 'red', '#00FF00'. Can be an array to create a gradient * eg ['#000000','#FFFFFF'] * {@link https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/fillStyle|MDN} - * @param {number} [style.fillGradientType=PIXI.TEXT_GRADIENT.LINEAR_VERTICAL] - If fills styles are - * supplied, this can change the type/direction of the gradient. See {@link PIXI.TEXT_GRADIENT} for possible values + * @param {number} [style.fillGradientType=PIXI.TEXT_GRADIENT.LINEAR_VERTICAL] - If fill is an array of colours + * to create a gradient, this can change the type/direction of the gradient. See {@link PIXI.TEXT_GRADIENT} + * @param {number[]} [style.fillGradientStops] - If fill is an array of colours to create a gradient, this array can set + * the stop points (numbers between 0 and 1) for the color, overriding the default behaviour of evenly spacing them. * @param {string|string[]} [style.fontFamily='Arial'] - The font family * @param {number|string} [style.fontSize=26] - The font size (as a number it converts to px, but as a string, * equivalents are '26px','20pt','160%' or '1.6em') @@ -76,6 +82,7 @@ * e.g 'blue', '#FCFF00' * @param {number} [style.strokeThickness=0] - A number that represents the thickness of the stroke. * Default is 0 (no stroke) + * @param {boolean} [style.trim=false] - Trim transparent borders * @param {string} [style.textBaseline='alphabetic'] - The baseline of the text that is rendered. * @param {boolean} [style.wordWrap=false] - Indicates if word wrap should be used * @param {number} [style.wordWrapWidth=100] - The width at which text will wrap, it needs wordWrap to be set to true @@ -152,6 +159,19 @@ } } + get dropShadowAlpha() + { + return this._dropShadowAlpha; + } + set dropShadowAlpha(dropShadowAlpha) + { + if (this._dropShadowAlpha !== dropShadowAlpha) + { + this._dropShadowAlpha = dropShadowAlpha; + this.styleID++; + } + } + get dropShadowAngle() { return this._dropShadowAngle; @@ -232,6 +252,19 @@ } } + get fillGradientStops() + { + return this._fillGradientStops; + } + set fillGradientStops(fillGradientStops) + { + if (!areArraysEqual(this._fillGradientStops,fillGradientStops)) + { + this._fillGradientStops = fillGradientStops; + this.styleID++; + } + } + get fontFamily() { return this._fontFamily; @@ -402,6 +435,19 @@ } } + get trim() + { + return this._trim; + } + set trim(trim) + { + if (this._trim !== trim) + { + this._trim = trim; + this.styleID++; + } + } + get wordWrap() { return this._wordWrap; @@ -475,3 +521,34 @@ return color; } } + +/** + * Utility function to convert hexadecimal colors to strings, and simply return the color if it's a string. + * This version can also convert array of colors + * + * @param {Array} array1 First array to compare + * @param {Array} array2 Second array to compare + * @return {boolean} Do the arrays contain the same values in the same order + */ +function areArraysEqual(array1, array2) +{ + if (!Array.isArray(array1) || !Array.isArray(array2)) + { + return false; + } + + if (array1.length !== array2.length) + { + return false; + } + + for (let i = 0; i < array1.length; ++i) + { + if (array1[i] !== array2[i]) + { + return false; + } + } + + return true; +} diff --git a/src/core/textures/BaseRenderTexture.js b/src/core/textures/BaseRenderTexture.js index 5d50146..aa727f0 100644 --- a/src/core/textures/BaseRenderTexture.js +++ b/src/core/textures/BaseRenderTexture.js @@ -1,8 +1,6 @@ import BaseTexture from './BaseTexture'; import settings from '../settings'; -const { RESOLUTION, SCALE_MODE } = settings; - /** * A BaseRenderTexture is a special texture that allows any Pixi display object to be rendered to it. * @@ -54,7 +52,7 @@ { super(null, scaleMode); - this.resolution = resolution || RESOLUTION; + this.resolution = resolution || settings.RESOLUTION; this.width = width; this.height = height; @@ -62,7 +60,7 @@ this.realWidth = this.width * this.resolution; this.realHeight = this.height * this.resolution; - this.scaleMode = scaleMode || SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; this.hasLoaded = true; /** diff --git a/src/core/textures/BaseTexture.js b/src/core/textures/BaseTexture.js index 676dca3..aeccf39 100644 --- a/src/core/textures/BaseTexture.js +++ b/src/core/textures/BaseTexture.js @@ -77,7 +77,7 @@ * @default PIXI.settings.SCALE_MODE * @see PIXI.SCALE_MODES */ - this.scaleMode = scaleMode || settings.SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; /** * Set to true once the base texture has successfully loaded. @@ -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/RenderTexture.js b/src/core/textures/RenderTexture.js index 4c5034f..b3c6297 100644 --- a/src/core/textures/RenderTexture.js +++ b/src/core/textures/RenderTexture.js @@ -54,8 +54,8 @@ /* eslint-disable prefer-rest-params, no-console */ const width = arguments[1]; const height = arguments[2]; - const scaleMode = arguments[3] || 0; - const resolution = arguments[4] || 1; + const scaleMode = arguments[3]; + const resolution = arguments[4]; // we have an old render texture.. console.warn(`Please use RenderTexture.create(${width}, ${height}) instead of the ctor directly.`); 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 ca31456..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 @@ -459,7 +497,9 @@ if (frame.x + frame.width > this.baseTexture.width || frame.y + frame.height > this.baseTexture.height) { - throw new Error(`Texture Error: frame does not fit inside the base Texture dimensions ${this}`); + throw new Error('Texture Error: frame does not fit inside the base Texture dimensions: ' + + `X: ${frame.x} + ${frame.width} > ${this.baseTexture.width} ` + + `Y: ${frame.y} + ${frame.height} > ${this.baseTexture.height}`); } // this.valid = frame && frame.width && frame.height && this.baseTexture.source && this.baseTexture.hasLoaded; @@ -520,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. @@ -528,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 2a7497b..99e585a 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)); } /** @@ -360,3 +362,43 @@ * @private */ export const BaseTextureCache = {}; + +/** + * 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/core/utils/trimCanvas.js b/src/core/utils/trimCanvas.js new file mode 100644 index 0000000..5af6de0 --- /dev/null +++ b/src/core/utils/trimCanvas.js @@ -0,0 +1,83 @@ +/** + * Trim transparent borders from a canvas + * + * @memberof PIXI + * @function trimCanvas + * @private + * @param {HTMLCanvasElement} canvas - the canvas to trim + * @returns {object} Trim data + */ +export default function trimCanvas(canvas) +{ + // https://gist.github.com/remy/784508 + + let width = canvas.width; + let height = canvas.height; + + const context = canvas.getContext('2d'); + const imageData = context.getImageData(0, 0, width, height); + const pixels = imageData.data; + const len = pixels.length; + + const bound = { + top: null, + left: null, + right: null, + bottom: null, + }; + let i; + let x; + let y; + + for (i = 0; i < len; i += 4) + { + if (pixels[i + 3] !== 0) + { + x = (i / 4) % width; + y = ~~((i / 4) / width); + + if (bound.top === null) + { + bound.top = y; + } + + if (bound.left === null) + { + bound.left = x; + } + else if (x < bound.left) + { + bound.left = x; + } + + if (bound.right === null) + { + bound.right = x + 1; + } + else if (bound.right < x) + { + bound.right = x + 1; + } + + if (bound.bottom === null) + { + bound.bottom = y; + } + else if (bound.bottom < y) + { + bound.bottom = y; + } + } + } + + width = bound.right - bound.left; + height = bound.bottom - bound.top + 1; + + const data = context.getImageData(bound.left, bound.top, width, height); + + return { + height, + width, + data, + }; +} diff --git a/src/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/extras/TilingSprite.js b/src/extras/TilingSprite.js index f90d248..fe82451 100644 --- a/src/extras/TilingSprite.js +++ b/src/extras/TilingSprite.js @@ -220,20 +220,33 @@ transform.tx * resolution, transform.ty * resolution); - // TODO - this should be rolled into the setTransform above.. - context.scale(this.tileScale.x / baseTextureResolution, this.tileScale.y / baseTextureResolution); - - context.translate(modX + (this.anchor.x * -this._width), - modY + (this.anchor.y * -this._height)); - renderer.setBlendMode(this.blendMode); // fill the pattern! context.fillStyle = this._canvasPattern; - context.fillRect(-modX, - -modY, - this._width / this.tileScale.x * baseTextureResolution, - this._height / this.tileScale.y * baseTextureResolution); + + // TODO - this should be rolled into the setTransform above.. + context.scale(this.tileScale.x / baseTextureResolution, this.tileScale.y / baseTextureResolution); + + const anchorX = this.anchor.x * -this._width; + const anchorY = this.anchor.y * -this._height; + + if (this.uvRespectAnchor) + { + context.translate(modX, modY); + + context.fillRect(-modX + anchorX, -modY + anchorY, + this._width / this.tileScale.x * baseTextureResolution, + this._height / this.tileScale.y * baseTextureResolution); + } + else + { + context.translate(modX + anchorX, modY + anchorY); + + context.fillRect(-modX, -modY, + this._width / this.tileScale.x * baseTextureResolution, + this._height / this.tileScale.y * baseTextureResolution); + } } /** diff --git a/src/extras/cacheAsBitmap.js b/src/extras/cacheAsBitmap.js index 18043a7..5dd5640 100644 --- a/src/extras/cacheAsBitmap.js +++ b/src/extras/cacheAsBitmap.js @@ -41,6 +41,9 @@ * provide a performance benefit for complex static displayObjects. * To remove simply set this property to 'false' * + * IMPORTANT GOTCHA - make sure that all your textures are preloaded BEFORE setting this property to true + * as it will take a snapshot of what is currently there. If the textures have not loaded then they will not appear. + * * @member {boolean} * @memberof PIXI.DisplayObject# */ diff --git a/src/filters/blur/BlurFilter.js b/src/filters/blur/BlurFilter.js index 340fc58..1575db9 100644 --- a/src/filters/blur/BlurFilter.js +++ b/src/filters/blur/BlurFilter.js @@ -24,10 +24,9 @@ this.blurXFilter = new BlurXFilter(strength, quality, resolution, kernelSize); this.blurYFilter = new BlurYFilter(strength, quality, resolution, kernelSize); - this.resolution = 1; this.padding = 0; - this.resolution = resolution || 1; + this.resolution = resolution || core.settings.RESOLUTION; this.quality = quality || 4; this.blur = strength || 8; } diff --git a/package.json b/package.json index 1271fef..71ade15 100644 --- a/package.json +++ b/package.json @@ -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/scripts/release.js b/scripts/release.js index c4c8e36..156620c 100644 --- a/scripts/release.js +++ b/scripts/release.js @@ -12,7 +12,8 @@ 'dist/**/*', 'lib/**/*', 'src/**/*', - 'scripts/**/*', + 'scripts/*', + 'scripts/renders/*', 'test/**/*', '*.json', '*.md', diff --git a/scripts/renders/client.js b/scripts/renders/client.js index 16c9565..6faee34 100755 --- a/scripts/renders/client.js +++ b/scripts/renders/client.js @@ -1,6 +1,6 @@ 'use strict'; -require('../../bin/pixi'); +require('../../dist/pixi'); PIXI.utils.skipHello(); const fs = require('fs'); 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 bc730db..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. @@ -33,8 +33,11 @@ * need to call toDataUrl on the webgl context * @param {number} [options.resolution=1] - The resolution / device pixel ratio of the renderer, retina would be 2 * @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 @@ -43,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. */ @@ -73,7 +99,7 @@ */ stop() { - this.ticker.stop(); + this._ticker.stop(); } /** @@ -81,7 +107,7 @@ */ start() { - this.ticker.start(); + this._ticker.start(); } /** @@ -95,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 index 81e076b..78b17e6 100644 --- a/src/core/Shader.js +++ b/src/core/Shader.js @@ -1,9 +1,7 @@ import { GLShader } from 'pixi-gl-core'; import settings from './settings'; -const { PRECISION } = settings; - -function checkPrecision(src) +function checkPrecision(src, def) { if (src instanceof Array) { @@ -11,14 +9,14 @@ { const copy = src.slice(0); - copy.unshift(`precision ${PRECISION} float;`); + copy.unshift(`precision ${def} float;`); return copy; } } else if (src.substring(0, 9) !== 'precision') { - return `precision ${PRECISION} float;\n${src}`; + return `precision ${def} float;\n${src}`; } return src; @@ -42,6 +40,7 @@ */ constructor(gl, vertexSrc, fragmentSrc) { - super(gl, checkPrecision(vertexSrc), checkPrecision(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 3425b41..f9235e4 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -181,7 +181,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 b3792ef..17fde8d 100644 --- a/src/core/display/Container.js +++ b/src/core/display/Container.js @@ -73,13 +73,14 @@ } child.parent = this; - - // ensure a transform will be recalculated.. - this.transform._parentID = -1; - this._boundsID++; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.push(child); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(this.children.length - 1); child.emit('added', this); @@ -108,9 +109,14 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.splice(index, 0, child); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(index); child.emit('added', this); @@ -174,6 +180,7 @@ removeItems(this.children, currentIndex, 1); // remove from old position this.children.splice(index, 0, child); // add at new position + this.onChildrenChange(index); } @@ -220,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! @@ -244,9 +252,14 @@ { const child = this.getChildAt(index); + // ensure child transform will be recalculated.. child.parent = null; + child.transform._parentID = -1; removeItems(this.children, index, 1); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(index); child.emit('removed', this); @@ -275,8 +288,14 @@ for (let i = 0; i < removed.length; ++i) { removed[i].parent = null; + if (removed[i].transform) + { + removed[i].transform._parentID = -1; + } } + this._boundsID++; + this.onChildrenChange(beginIndex); for (let i = 0; i < removed.length; ++i) diff --git a/src/core/display/DisplayObject.js b/src/core/display/DisplayObject.js index 3affe36..4e6c77d 100644 --- a/src/core/display/DisplayObject.js +++ b/src/core/display/DisplayObject.js @@ -109,7 +109,7 @@ /** * The original, cached mask of the object * - * @member {PIXI.Rectangle} + * @member {PIXI.Graphics|PIXI.Sprite} * @private */ this._mask = null; diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index 95f2af4..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 ); @@ -1065,10 +1061,15 @@ canvasRenderer = new CanvasRenderer(); } - tempMatrix.tx = -bounds.x; - tempMatrix.ty = -bounds.y; + this.transform.updateLocalTransform(); + this.transform.localTransform.copy(tempMatrix); - canvasRenderer.render(this, canvasBuffer, false, tempMatrix); + tempMatrix.invert(); + + tempMatrix.tx -= bounds.x; + tempMatrix.ty -= bounds.y; + + canvasRenderer.render(this, canvasBuffer, true, tempMatrix); const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode); 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 e67680f..867dde2 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/buildCircle.js b/src/core/graphics/webgl/utils/buildCircle.js index 0400d13..abf5d02 100644 --- a/src/core/graphics/webgl/utils/buildCircle.js +++ b/src/core/graphics/webgl/utils/buildCircle.js @@ -33,6 +33,11 @@ height = circleData.height; } + if (width === 0 || height === 0) + { + return; + } + const totalSegs = Math.floor(30 * Math.sqrt(circleData.radius)) || Math.floor(15 * Math.sqrt(circleData.width + circleData.height)); diff --git a/src/core/graphics/webgl/utils/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 90155f9..f03fe15 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -30,6 +30,7 @@ export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; export { default as CanvasGraphicsRenderer } from './graphics/canvas/CanvasGraphicsRenderer'; +export { default as Spritesheet } from './textures/Spritesheet'; export { default as Texture } from './textures/Texture'; export { default as BaseTexture } from './textures/BaseTexture'; export { default as RenderTexture } from './textures/RenderTexture'; diff --git a/src/core/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 ccce680..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); } /** @@ -162,6 +162,9 @@ if (transform) { transform.copy(tempWt); + + // lets not forget to flag the parent transform as dirty... + this._tempDisplayObjectParent.transform._worldID = -1; } else { @@ -169,6 +172,7 @@ } displayObject.parent = this._tempDisplayObjectParent; + displayObject.updateTransform(); displayObject.parent = cacheParent; // displayObject.hitArea = //TODO add a temp hit area @@ -279,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/canvas/utils/CanvasRenderTarget.js b/src/core/renderers/canvas/utils/CanvasRenderTarget.js index 3de0aef..b409891 100644 --- a/src/core/renderers/canvas/utils/CanvasRenderTarget.js +++ b/src/core/renderers/canvas/utils/CanvasRenderTarget.js @@ -1,5 +1,4 @@ import settings from '../../../settings'; -const { RESOLUTION } = settings; /** * Creates a Canvas element of the given size. @@ -30,7 +29,7 @@ */ this.context = this.canvas.getContext('2d'); - this.resolution = resolution || RESOLUTION; + this.resolution = resolution || settings.RESOLUTION; this.resize(width, height); } 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 ce0e1c4..788ed6c 100644 --- a/src/core/renderers/webgl/WebGLRenderer.js +++ b/src/core/renderers/webgl/WebGLRenderer.js @@ -30,8 +30,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 @@ -49,10 +49,19 @@ * enable this if you need to call toDataUrl on the webgl context. * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when * rendering, stopping pixel interpolation. + * @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; + + if (this.legacy) + { + glCore.VertexArrayObject.FORCE_NATIVE = true; + } /** * The type of this renderer as a standardised const @@ -229,7 +238,7 @@ this.emit('context', gl); // setup the width/height properties and gl viewport - this.resize(this.width, this.height); + this.resize(this.screen.width, this.screen.height); } /** @@ -323,16 +332,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) { @@ -376,6 +385,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 @@ -440,9 +469,10 @@ * Changes the current shader to the one given in parameter * * @param {PIXI.Shader} shader - the new shader + * @param {boolean} [autoProject=true] - Whether automatically set the projection matrix * @return {PIXI.WebGLRenderer} Returns itself. */ - bindShader(shader) + bindShader(shader, autoProject) { // TODO cache if (this._activeShader !== shader) @@ -450,8 +480,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 bbab3b3..944f41d 100644 --- a/src/core/renderers/webgl/filters/Filter.js +++ b/src/core/renderers/webgl/filters/Filter.js @@ -1,6 +1,7 @@ import extractUniformsFromSrc from './extractUniformsFromSrc'; import { uid } from '../../../utils'; import { BLEND_MODES } from '../../../const'; +import settings from '../../../settings'; const SOURCE_KEY_MAP = {}; @@ -10,7 +11,7 @@ * @memberof PIXI * @extends PIXI.Shader */ -class Filter +export default class Filter { /** * @param {string} [vertexSrc] - The source of the vertex shader. @@ -35,8 +36,6 @@ this.blendMode = BLEND_MODES.NORMAL; - // pull out the vertex and shader uniforms if they are not specified.. - // currently this does not extract structs only default types this.uniformData = uniforms || extractUniformsFromSrc(this.vertexSrc, this.fragmentSrc, 'projectionMatrix|uSampler'); /** @@ -80,7 +79,7 @@ * * @member {number} */ - this.resolution = 1; + this.resolution = settings.RESOLUTION; /** * If enabled is true the filter is applied, if false it will not. @@ -97,8 +96,11 @@ * @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 { // --- // // this.uniforms.filterMatrix = filterManager.calculateSpriteMatrix(tempMatrix, window.panda ); @@ -170,5 +172,3 @@ ].join('\n'); } } - -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 dc7072a..6c7422d 100644 --- a/src/core/renderers/webgl/managers/FilterManager.js +++ b/src/core/renderers/webgl/managers/FilterManager.js @@ -154,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 @@ -176,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; @@ -184,7 +184,7 @@ flop = t; } - filters[i].apply(this, flip, lastState.renderTarget, true); + filters[i].apply(this, flip, lastState.renderTarget, false, currentState); this.freePotRenderTarget(flip); this.freePotRenderTarget(flop); @@ -291,9 +291,13 @@ let textureCount = 1; let currentState; - if (shader.uniforms.data.filterArea) + // 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; @@ -306,9 +310,9 @@ // use this to clamp displaced texture coords so they belong to filterArea // see displacementFilter fragment shader for an example - if (shader.uniforms.data.filterClamp) + if (shader.uniforms.filterClamp) { - currentState = this.filterData.stack[this.filterData.index]; + currentState = currentState || this.filterData.stack[this.filterData.index]; const filterClamp = shader.uniforms.filterClamp; @@ -485,7 +489,7 @@ */ destroy() { - this.shaderCache = []; + this.shaderCache = {}; this.emptyPool(); } diff --git a/src/core/renderers/webgl/utils/RenderTarget.js b/src/core/renderers/webgl/utils/RenderTarget.js index afe51ab..6d2467f 100644 --- a/src/core/renderers/webgl/utils/RenderTarget.js +++ b/src/core/renderers/webgl/utils/RenderTarget.js @@ -124,7 +124,7 @@ * @default PIXI.settings.SCALE_MODE * @see PIXI.SCALE_MODES */ - this.scaleMode = scaleMode || settings.SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; /** * Whether this object is the root element or not 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 36ea5bb..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; @@ -346,8 +346,8 @@ /** * Gets the local bounds of the sprite object. * - * @param {Rectangle} rect - The output rectangle. - * @return {Rectangle} The bounds. + * @param {PIXI.Rectangle} rect - The output rectangle. + * @return {PIXI.Rectangle} The bounds. */ getLocalBounds(rect) { @@ -440,7 +440,7 @@ * * @static * @param {number|string|PIXI.BaseTexture|HTMLCanvasElement|HTMLVideoElement} source Source to create texture from - * @return {PIXI.Texture} 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 e77b39a..90b08c0 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++) @@ -103,11 +102,18 @@ { const gl = this.renderer.gl; - // step 1: first check max textures the GPU can handle. - this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); + if (this.renderer.legacy) + { + this.MAX_TEXTURES = 1; + } + else + { + // step 1: first check max textures the GPU can handle. + this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); - // step 2: check the maximum number of if statements the shader can have too.. - this.MAX_TEXTURES = checkMaxIfStatmentsInShader(this.MAX_TEXTURES, gl); + // step 2: check the maximum number of if statements the shader can have too.. + this.MAX_TEXTURES = checkMaxIfStatmentsInShader(this.MAX_TEXTURES, gl); + } const shader = this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); @@ -130,8 +136,12 @@ .addIndex(this.indexBuffer) .addAttribute(this.vertexBuffers[i], shader.attributes.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) .addAttribute(this.vertexBuffers[i], shader.attributes.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) - .addAttribute(this.vertexBuffers[i], shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4) - .addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + .addAttribute(this.vertexBuffers[i], shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); + + if (shader.attributes.aTextureId) + { + this.vaos[i].addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + } /* eslint-enable max-len */ } @@ -378,8 +388,12 @@ .addIndex(this.indexBuffer) .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) - .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4) - .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); + + if (this.shader.attributes.aTextureId) + { + this.vaos[this.vertexCount].addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + } /* eslint-enable max-len */ } diff --git a/src/core/text/Text.js b/src/core/text/Text.js index 013625a..cec8c32 100644 --- a/src/core/text/Text.js +++ b/src/core/text/Text.js @@ -6,6 +6,7 @@ import { TEXT_GRADIENT } from '../const'; import settings from '../settings'; import TextStyle from './TextStyle'; +import trimCanvas from '../utils/trimCanvas'; const defaultDestroyOptions = { texture: true, @@ -57,7 +58,7 @@ /** * The canvas 2d context that everything is drawn with - * @member {HTMLCanvasElement} + * @member {CanvasRenderingContext2D} */ this.context = this.canvas.getContext('2d'); @@ -189,10 +190,12 @@ if (style.dropShadow) { + this.context.shadowBlur = style.dropShadowBlur; + this.context.globalAlpha = style.dropShadowAlpha; + if (style.dropShadowBlur > 0) { this.context.shadowColor = style.dropShadowColor; - this.context.shadowBlur = style.dropShadowBlur; } else { @@ -237,6 +240,10 @@ } } + // reset the shadow blur and alpha that was set by the drop shadow, for the regular text + this.context.shadowBlur = 0; + this.context.globalAlpha = 1; + // set canvas text styles this.context.fillStyle = this._generateFillStyle(style, lines); @@ -326,6 +333,15 @@ */ updateTexture() { + if (this._style.trim) + { + const trimmed = trimCanvas(this.canvas); + + this.canvas.width = trimmed.width; + this.canvas.height = trimmed.height; + this.context.putImageData(trimmed.data, 0, 0); + } + const texture = this._texture; const style = this._style; @@ -475,6 +491,19 @@ } /** + * Gets the local bounds of the text object. + * + * @param {Rectangle} rect - The output rectangle. + * @return {Rectangle} The bounds. + */ + getLocalBounds(rect) + { + this.updateText(true); + + return super.getLocalBounds.call(this, rect); + } + + /** * calculates the bounds of the Text as a rectangle. The bounds calculation takes the worldTransform into account. */ _calculateBounds() @@ -525,6 +554,29 @@ const width = this.canvas.width / this.resolution; const height = this.canvas.height / this.resolution; + // make a copy of the style settings, so we can manipulate them later + const fill = style.fill.slice(); + const fillGradientStops = style.fillGradientStops.slice(); + + // wanting to evenly distribute the fills. So an array of 4 colours should give fills of 0.25, 0.5 and 0.75 + if (!fillGradientStops.length) + { + const lengthPlus1 = fill.length + 1; + + for (let i = 1; i < lengthPlus1; ++i) + { + fillGradientStops.push(i / lengthPlus1); + } + } + + // stop the bleeding of the last gradient on the line above to the top gradient of the this line + // by hard defining the first gradient colour at point 0, and last gradient colour at point 1 + fill.unshift(style.fill[0]); + fillGradientStops.unshift(0); + + fill.push(style.fill[style.fill.length - 1]); + fillGradientStops.push(1); + if (style.fillGradientType === TEXT_GRADIENT.LINEAR_VERTICAL) { // start the gradient at the top center of the canvas, and end at the bottom middle of the canvas @@ -532,15 +584,22 @@ // we need to repeat the gradient so that each individual line of text has the same vertical gradient effect // ['#FF0000', '#00FF00', '#0000FF'] over 2 lines would create stops at 0.125, 0.25, 0.375, 0.625, 0.75, 0.875 - totalIterations = (style.fill.length + 1) * lines.length; + totalIterations = (fill.length + 1) * lines.length; currentIteration = 0; for (let i = 0; i < lines.length; i++) { currentIteration += 1; - for (let j = 0; j < style.fill.length; j++) + for (let j = 0; j < fill.length; j++) { - stop = (currentIteration / totalIterations); - gradient.addColorStop(stop, style.fill[j]); + if (fillGradientStops[j]) + { + stop = (fillGradientStops[j] / lines.length) + (i / lines.length); + } + else + { + stop = currentIteration / totalIterations; + } + gradient.addColorStop(stop, fill[j]); currentIteration++; } } @@ -552,13 +611,20 @@ // can just evenly space out the gradients in this case, as multiple lines makes no difference // to an even left to right gradient - totalIterations = style.fill.length + 1; + totalIterations = fill.length + 1; currentIteration = 1; - for (let i = 0; i < style.fill.length; i++) + for (let i = 0; i < fill.length; i++) { - stop = currentIteration / totalIterations; - gradient.addColorStop(stop, style.fill[i]); + if (fillGradientStops[i]) + { + stop = fillGradientStops[i]; + } + else + { + stop = currentIteration / totalIterations; + } + gradient.addColorStop(stop, fill[i]); currentIteration++; } } @@ -569,7 +635,7 @@ /** * Destroys this text object. * Note* Unlike a Sprite, a Text object will automatically destroy its baseTexture and texture as - * the majorety of the time the texture will not be shared with any other Sprites. + * the majority of the time the texture will not be shared with any other Sprites. * * @param {object|boolean} [options] - Options parameter. A boolean will act as if all options * have been set to that value @@ -680,7 +746,7 @@ set text(text) // eslint-disable-line require-jsdoc { - text = String(text || ' '); + text = String(text === '' || text === null || text === undefined ? ' ' : text); if (this._text === text) { diff --git a/src/core/text/TextStyle.js b/src/core/text/TextStyle.js index 3b25528..890950b 100644 --- a/src/core/text/TextStyle.js +++ b/src/core/text/TextStyle.js @@ -8,12 +8,14 @@ align: 'left', breakWords: false, dropShadow: false, + dropShadowAlpha: 1, dropShadowAngle: Math.PI / 6, dropShadowBlur: 0, dropShadowColor: '#000000', dropShadowDistance: 5, fill: 'black', fillGradientType: TEXT_GRADIENT.LINEAR_VERTICAL, + fillGradientStops: [], fontFamily: 'Arial', fontSize: 26, fontStyle: 'normal', @@ -27,6 +29,7 @@ stroke: 'black', strokeThickness: 0, textBaseline: 'alphabetic', + trim: false, wordWrap: false, wordWrapWidth: 100, }; @@ -47,6 +50,7 @@ * @param {boolean} [style.breakWords=false] - Indicates if lines can be wrapped within words, it * needs wordWrap to be set to true * @param {boolean} [style.dropShadow=false] - Set a drop shadow for the text + * @param {number} [style.dropShadowAlpha=1] - Set alpha for the drop shadow * @param {number} [style.dropShadowAngle=Math.PI/6] - Set a angle of the drop shadow * @param {number} [style.dropShadowBlur=0] - Set a shadow blur radius * @param {string} [style.dropShadowColor='#000000'] - A fill style to be used on the dropshadow e.g 'red', '#00FF00' @@ -55,8 +59,10 @@ * fillstyle that will be used on the text e.g 'red', '#00FF00'. Can be an array to create a gradient * eg ['#000000','#FFFFFF'] * {@link https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/fillStyle|MDN} - * @param {number} [style.fillGradientType=PIXI.TEXT_GRADIENT.LINEAR_VERTICAL] - If fills styles are - * supplied, this can change the type/direction of the gradient. See {@link PIXI.TEXT_GRADIENT} for possible values + * @param {number} [style.fillGradientType=PIXI.TEXT_GRADIENT.LINEAR_VERTICAL] - If fill is an array of colours + * to create a gradient, this can change the type/direction of the gradient. See {@link PIXI.TEXT_GRADIENT} + * @param {number[]} [style.fillGradientStops] - If fill is an array of colours to create a gradient, this array can set + * the stop points (numbers between 0 and 1) for the color, overriding the default behaviour of evenly spacing them. * @param {string|string[]} [style.fontFamily='Arial'] - The font family * @param {number|string} [style.fontSize=26] - The font size (as a number it converts to px, but as a string, * equivalents are '26px','20pt','160%' or '1.6em') @@ -76,6 +82,7 @@ * e.g 'blue', '#FCFF00' * @param {number} [style.strokeThickness=0] - A number that represents the thickness of the stroke. * Default is 0 (no stroke) + * @param {boolean} [style.trim=false] - Trim transparent borders * @param {string} [style.textBaseline='alphabetic'] - The baseline of the text that is rendered. * @param {boolean} [style.wordWrap=false] - Indicates if word wrap should be used * @param {number} [style.wordWrapWidth=100] - The width at which text will wrap, it needs wordWrap to be set to true @@ -152,6 +159,19 @@ } } + get dropShadowAlpha() + { + return this._dropShadowAlpha; + } + set dropShadowAlpha(dropShadowAlpha) + { + if (this._dropShadowAlpha !== dropShadowAlpha) + { + this._dropShadowAlpha = dropShadowAlpha; + this.styleID++; + } + } + get dropShadowAngle() { return this._dropShadowAngle; @@ -232,6 +252,19 @@ } } + get fillGradientStops() + { + return this._fillGradientStops; + } + set fillGradientStops(fillGradientStops) + { + if (!areArraysEqual(this._fillGradientStops,fillGradientStops)) + { + this._fillGradientStops = fillGradientStops; + this.styleID++; + } + } + get fontFamily() { return this._fontFamily; @@ -402,6 +435,19 @@ } } + get trim() + { + return this._trim; + } + set trim(trim) + { + if (this._trim !== trim) + { + this._trim = trim; + this.styleID++; + } + } + get wordWrap() { return this._wordWrap; @@ -475,3 +521,34 @@ return color; } } + +/** + * Utility function to convert hexadecimal colors to strings, and simply return the color if it's a string. + * This version can also convert array of colors + * + * @param {Array} array1 First array to compare + * @param {Array} array2 Second array to compare + * @return {boolean} Do the arrays contain the same values in the same order + */ +function areArraysEqual(array1, array2) +{ + if (!Array.isArray(array1) || !Array.isArray(array2)) + { + return false; + } + + if (array1.length !== array2.length) + { + return false; + } + + for (let i = 0; i < array1.length; ++i) + { + if (array1[i] !== array2[i]) + { + return false; + } + } + + return true; +} diff --git a/src/core/textures/BaseRenderTexture.js b/src/core/textures/BaseRenderTexture.js index 5d50146..aa727f0 100644 --- a/src/core/textures/BaseRenderTexture.js +++ b/src/core/textures/BaseRenderTexture.js @@ -1,8 +1,6 @@ import BaseTexture from './BaseTexture'; import settings from '../settings'; -const { RESOLUTION, SCALE_MODE } = settings; - /** * A BaseRenderTexture is a special texture that allows any Pixi display object to be rendered to it. * @@ -54,7 +52,7 @@ { super(null, scaleMode); - this.resolution = resolution || RESOLUTION; + this.resolution = resolution || settings.RESOLUTION; this.width = width; this.height = height; @@ -62,7 +60,7 @@ this.realWidth = this.width * this.resolution; this.realHeight = this.height * this.resolution; - this.scaleMode = scaleMode || SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; this.hasLoaded = true; /** diff --git a/src/core/textures/BaseTexture.js b/src/core/textures/BaseTexture.js index 676dca3..aeccf39 100644 --- a/src/core/textures/BaseTexture.js +++ b/src/core/textures/BaseTexture.js @@ -77,7 +77,7 @@ * @default PIXI.settings.SCALE_MODE * @see PIXI.SCALE_MODES */ - this.scaleMode = scaleMode || settings.SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; /** * Set to true once the base texture has successfully loaded. @@ -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/RenderTexture.js b/src/core/textures/RenderTexture.js index 4c5034f..b3c6297 100644 --- a/src/core/textures/RenderTexture.js +++ b/src/core/textures/RenderTexture.js @@ -54,8 +54,8 @@ /* eslint-disable prefer-rest-params, no-console */ const width = arguments[1]; const height = arguments[2]; - const scaleMode = arguments[3] || 0; - const resolution = arguments[4] || 1; + const scaleMode = arguments[3]; + const resolution = arguments[4]; // we have an old render texture.. console.warn(`Please use RenderTexture.create(${width}, ${height}) instead of the ctor directly.`); 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 ca31456..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 @@ -459,7 +497,9 @@ if (frame.x + frame.width > this.baseTexture.width || frame.y + frame.height > this.baseTexture.height) { - throw new Error(`Texture Error: frame does not fit inside the base Texture dimensions ${this}`); + throw new Error('Texture Error: frame does not fit inside the base Texture dimensions: ' + + `X: ${frame.x} + ${frame.width} > ${this.baseTexture.width} ` + + `Y: ${frame.y} + ${frame.height} > ${this.baseTexture.height}`); } // this.valid = frame && frame.width && frame.height && this.baseTexture.source && this.baseTexture.hasLoaded; @@ -520,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. @@ -528,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 2a7497b..99e585a 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)); } /** @@ -360,3 +362,43 @@ * @private */ export const BaseTextureCache = {}; + +/** + * 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/core/utils/trimCanvas.js b/src/core/utils/trimCanvas.js new file mode 100644 index 0000000..5af6de0 --- /dev/null +++ b/src/core/utils/trimCanvas.js @@ -0,0 +1,83 @@ +/** + * Trim transparent borders from a canvas + * + * @memberof PIXI + * @function trimCanvas + * @private + * @param {HTMLCanvasElement} canvas - the canvas to trim + * @returns {object} Trim data + */ +export default function trimCanvas(canvas) +{ + // https://gist.github.com/remy/784508 + + let width = canvas.width; + let height = canvas.height; + + const context = canvas.getContext('2d'); + const imageData = context.getImageData(0, 0, width, height); + const pixels = imageData.data; + const len = pixels.length; + + const bound = { + top: null, + left: null, + right: null, + bottom: null, + }; + let i; + let x; + let y; + + for (i = 0; i < len; i += 4) + { + if (pixels[i + 3] !== 0) + { + x = (i / 4) % width; + y = ~~((i / 4) / width); + + if (bound.top === null) + { + bound.top = y; + } + + if (bound.left === null) + { + bound.left = x; + } + else if (x < bound.left) + { + bound.left = x; + } + + if (bound.right === null) + { + bound.right = x + 1; + } + else if (bound.right < x) + { + bound.right = x + 1; + } + + if (bound.bottom === null) + { + bound.bottom = y; + } + else if (bound.bottom < y) + { + bound.bottom = y; + } + } + } + + width = bound.right - bound.left; + height = bound.bottom - bound.top + 1; + + const data = context.getImageData(bound.left, bound.top, width, height); + + return { + height, + width, + data, + }; +} diff --git a/src/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/extras/TilingSprite.js b/src/extras/TilingSprite.js index f90d248..fe82451 100644 --- a/src/extras/TilingSprite.js +++ b/src/extras/TilingSprite.js @@ -220,20 +220,33 @@ transform.tx * resolution, transform.ty * resolution); - // TODO - this should be rolled into the setTransform above.. - context.scale(this.tileScale.x / baseTextureResolution, this.tileScale.y / baseTextureResolution); - - context.translate(modX + (this.anchor.x * -this._width), - modY + (this.anchor.y * -this._height)); - renderer.setBlendMode(this.blendMode); // fill the pattern! context.fillStyle = this._canvasPattern; - context.fillRect(-modX, - -modY, - this._width / this.tileScale.x * baseTextureResolution, - this._height / this.tileScale.y * baseTextureResolution); + + // TODO - this should be rolled into the setTransform above.. + context.scale(this.tileScale.x / baseTextureResolution, this.tileScale.y / baseTextureResolution); + + const anchorX = this.anchor.x * -this._width; + const anchorY = this.anchor.y * -this._height; + + if (this.uvRespectAnchor) + { + context.translate(modX, modY); + + context.fillRect(-modX + anchorX, -modY + anchorY, + this._width / this.tileScale.x * baseTextureResolution, + this._height / this.tileScale.y * baseTextureResolution); + } + else + { + context.translate(modX + anchorX, modY + anchorY); + + context.fillRect(-modX, -modY, + this._width / this.tileScale.x * baseTextureResolution, + this._height / this.tileScale.y * baseTextureResolution); + } } /** diff --git a/src/extras/cacheAsBitmap.js b/src/extras/cacheAsBitmap.js index 18043a7..5dd5640 100644 --- a/src/extras/cacheAsBitmap.js +++ b/src/extras/cacheAsBitmap.js @@ -41,6 +41,9 @@ * provide a performance benefit for complex static displayObjects. * To remove simply set this property to 'false' * + * IMPORTANT GOTCHA - make sure that all your textures are preloaded BEFORE setting this property to true + * as it will take a snapshot of what is currently there. If the textures have not loaded then they will not appear. + * * @member {boolean} * @memberof PIXI.DisplayObject# */ diff --git a/src/filters/blur/BlurFilter.js b/src/filters/blur/BlurFilter.js index 340fc58..1575db9 100644 --- a/src/filters/blur/BlurFilter.js +++ b/src/filters/blur/BlurFilter.js @@ -24,10 +24,9 @@ this.blurXFilter = new BlurXFilter(strength, quality, resolution, kernelSize); this.blurYFilter = new BlurYFilter(strength, quality, resolution, kernelSize); - this.resolution = 1; this.padding = 0; - this.resolution = resolution || 1; + this.resolution = resolution || core.settings.RESOLUTION; this.quality = quality || 4; this.blur = strength || 8; } diff --git a/src/filters/blur/BlurXFilter.js b/src/filters/blur/BlurXFilter.js index def99c3..0f48889 100644 --- a/src/filters/blur/BlurXFilter.js +++ b/src/filters/blur/BlurXFilter.js @@ -31,7 +31,7 @@ fragSrc ); - this.resolution = resolution || 1; + this.resolution = resolution || core.settings.RESOLUTION; this._quality = 0; diff --git a/package.json b/package.json index 1271fef..71ade15 100644 --- a/package.json +++ b/package.json @@ -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/scripts/release.js b/scripts/release.js index c4c8e36..156620c 100644 --- a/scripts/release.js +++ b/scripts/release.js @@ -12,7 +12,8 @@ 'dist/**/*', 'lib/**/*', 'src/**/*', - 'scripts/**/*', + 'scripts/*', + 'scripts/renders/*', 'test/**/*', '*.json', '*.md', diff --git a/scripts/renders/client.js b/scripts/renders/client.js index 16c9565..6faee34 100755 --- a/scripts/renders/client.js +++ b/scripts/renders/client.js @@ -1,6 +1,6 @@ 'use strict'; -require('../../bin/pixi'); +require('../../dist/pixi'); PIXI.utils.skipHello(); const fs = require('fs'); 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 bc730db..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. @@ -33,8 +33,11 @@ * need to call toDataUrl on the webgl context * @param {number} [options.resolution=1] - The resolution / device pixel ratio of the renderer, retina would be 2 * @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 @@ -43,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. */ @@ -73,7 +99,7 @@ */ stop() { - this.ticker.stop(); + this._ticker.stop(); } /** @@ -81,7 +107,7 @@ */ start() { - this.ticker.start(); + this._ticker.start(); } /** @@ -95,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 index 81e076b..78b17e6 100644 --- a/src/core/Shader.js +++ b/src/core/Shader.js @@ -1,9 +1,7 @@ import { GLShader } from 'pixi-gl-core'; import settings from './settings'; -const { PRECISION } = settings; - -function checkPrecision(src) +function checkPrecision(src, def) { if (src instanceof Array) { @@ -11,14 +9,14 @@ { const copy = src.slice(0); - copy.unshift(`precision ${PRECISION} float;`); + copy.unshift(`precision ${def} float;`); return copy; } } else if (src.substring(0, 9) !== 'precision') { - return `precision ${PRECISION} float;\n${src}`; + return `precision ${def} float;\n${src}`; } return src; @@ -42,6 +40,7 @@ */ constructor(gl, vertexSrc, fragmentSrc) { - super(gl, checkPrecision(vertexSrc), checkPrecision(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 3425b41..f9235e4 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -181,7 +181,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 b3792ef..17fde8d 100644 --- a/src/core/display/Container.js +++ b/src/core/display/Container.js @@ -73,13 +73,14 @@ } child.parent = this; - - // ensure a transform will be recalculated.. - this.transform._parentID = -1; - this._boundsID++; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.push(child); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(this.children.length - 1); child.emit('added', this); @@ -108,9 +109,14 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.splice(index, 0, child); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(index); child.emit('added', this); @@ -174,6 +180,7 @@ removeItems(this.children, currentIndex, 1); // remove from old position this.children.splice(index, 0, child); // add at new position + this.onChildrenChange(index); } @@ -220,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! @@ -244,9 +252,14 @@ { const child = this.getChildAt(index); + // ensure child transform will be recalculated.. child.parent = null; + child.transform._parentID = -1; removeItems(this.children, index, 1); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(index); child.emit('removed', this); @@ -275,8 +288,14 @@ for (let i = 0; i < removed.length; ++i) { removed[i].parent = null; + if (removed[i].transform) + { + removed[i].transform._parentID = -1; + } } + this._boundsID++; + this.onChildrenChange(beginIndex); for (let i = 0; i < removed.length; ++i) diff --git a/src/core/display/DisplayObject.js b/src/core/display/DisplayObject.js index 3affe36..4e6c77d 100644 --- a/src/core/display/DisplayObject.js +++ b/src/core/display/DisplayObject.js @@ -109,7 +109,7 @@ /** * The original, cached mask of the object * - * @member {PIXI.Rectangle} + * @member {PIXI.Graphics|PIXI.Sprite} * @private */ this._mask = null; diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index 95f2af4..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 ); @@ -1065,10 +1061,15 @@ canvasRenderer = new CanvasRenderer(); } - tempMatrix.tx = -bounds.x; - tempMatrix.ty = -bounds.y; + this.transform.updateLocalTransform(); + this.transform.localTransform.copy(tempMatrix); - canvasRenderer.render(this, canvasBuffer, false, tempMatrix); + tempMatrix.invert(); + + tempMatrix.tx -= bounds.x; + tempMatrix.ty -= bounds.y; + + canvasRenderer.render(this, canvasBuffer, true, tempMatrix); const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode); 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 e67680f..867dde2 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/buildCircle.js b/src/core/graphics/webgl/utils/buildCircle.js index 0400d13..abf5d02 100644 --- a/src/core/graphics/webgl/utils/buildCircle.js +++ b/src/core/graphics/webgl/utils/buildCircle.js @@ -33,6 +33,11 @@ height = circleData.height; } + if (width === 0 || height === 0) + { + return; + } + const totalSegs = Math.floor(30 * Math.sqrt(circleData.radius)) || Math.floor(15 * Math.sqrt(circleData.width + circleData.height)); diff --git a/src/core/graphics/webgl/utils/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 90155f9..f03fe15 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -30,6 +30,7 @@ export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; export { default as CanvasGraphicsRenderer } from './graphics/canvas/CanvasGraphicsRenderer'; +export { default as Spritesheet } from './textures/Spritesheet'; export { default as Texture } from './textures/Texture'; export { default as BaseTexture } from './textures/BaseTexture'; export { default as RenderTexture } from './textures/RenderTexture'; diff --git a/src/core/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 ccce680..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); } /** @@ -162,6 +162,9 @@ if (transform) { transform.copy(tempWt); + + // lets not forget to flag the parent transform as dirty... + this._tempDisplayObjectParent.transform._worldID = -1; } else { @@ -169,6 +172,7 @@ } displayObject.parent = this._tempDisplayObjectParent; + displayObject.updateTransform(); displayObject.parent = cacheParent; // displayObject.hitArea = //TODO add a temp hit area @@ -279,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/canvas/utils/CanvasRenderTarget.js b/src/core/renderers/canvas/utils/CanvasRenderTarget.js index 3de0aef..b409891 100644 --- a/src/core/renderers/canvas/utils/CanvasRenderTarget.js +++ b/src/core/renderers/canvas/utils/CanvasRenderTarget.js @@ -1,5 +1,4 @@ import settings from '../../../settings'; -const { RESOLUTION } = settings; /** * Creates a Canvas element of the given size. @@ -30,7 +29,7 @@ */ this.context = this.canvas.getContext('2d'); - this.resolution = resolution || RESOLUTION; + this.resolution = resolution || settings.RESOLUTION; this.resize(width, height); } 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 ce0e1c4..788ed6c 100644 --- a/src/core/renderers/webgl/WebGLRenderer.js +++ b/src/core/renderers/webgl/WebGLRenderer.js @@ -30,8 +30,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 @@ -49,10 +49,19 @@ * enable this if you need to call toDataUrl on the webgl context. * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when * rendering, stopping pixel interpolation. + * @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; + + if (this.legacy) + { + glCore.VertexArrayObject.FORCE_NATIVE = true; + } /** * The type of this renderer as a standardised const @@ -229,7 +238,7 @@ this.emit('context', gl); // setup the width/height properties and gl viewport - this.resize(this.width, this.height); + this.resize(this.screen.width, this.screen.height); } /** @@ -323,16 +332,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) { @@ -376,6 +385,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 @@ -440,9 +469,10 @@ * Changes the current shader to the one given in parameter * * @param {PIXI.Shader} shader - the new shader + * @param {boolean} [autoProject=true] - Whether automatically set the projection matrix * @return {PIXI.WebGLRenderer} Returns itself. */ - bindShader(shader) + bindShader(shader, autoProject) { // TODO cache if (this._activeShader !== shader) @@ -450,8 +480,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 bbab3b3..944f41d 100644 --- a/src/core/renderers/webgl/filters/Filter.js +++ b/src/core/renderers/webgl/filters/Filter.js @@ -1,6 +1,7 @@ import extractUniformsFromSrc from './extractUniformsFromSrc'; import { uid } from '../../../utils'; import { BLEND_MODES } from '../../../const'; +import settings from '../../../settings'; const SOURCE_KEY_MAP = {}; @@ -10,7 +11,7 @@ * @memberof PIXI * @extends PIXI.Shader */ -class Filter +export default class Filter { /** * @param {string} [vertexSrc] - The source of the vertex shader. @@ -35,8 +36,6 @@ this.blendMode = BLEND_MODES.NORMAL; - // pull out the vertex and shader uniforms if they are not specified.. - // currently this does not extract structs only default types this.uniformData = uniforms || extractUniformsFromSrc(this.vertexSrc, this.fragmentSrc, 'projectionMatrix|uSampler'); /** @@ -80,7 +79,7 @@ * * @member {number} */ - this.resolution = 1; + this.resolution = settings.RESOLUTION; /** * If enabled is true the filter is applied, if false it will not. @@ -97,8 +96,11 @@ * @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 { // --- // // this.uniforms.filterMatrix = filterManager.calculateSpriteMatrix(tempMatrix, window.panda ); @@ -170,5 +172,3 @@ ].join('\n'); } } - -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 dc7072a..6c7422d 100644 --- a/src/core/renderers/webgl/managers/FilterManager.js +++ b/src/core/renderers/webgl/managers/FilterManager.js @@ -154,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 @@ -176,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; @@ -184,7 +184,7 @@ flop = t; } - filters[i].apply(this, flip, lastState.renderTarget, true); + filters[i].apply(this, flip, lastState.renderTarget, false, currentState); this.freePotRenderTarget(flip); this.freePotRenderTarget(flop); @@ -291,9 +291,13 @@ let textureCount = 1; let currentState; - if (shader.uniforms.data.filterArea) + // 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; @@ -306,9 +310,9 @@ // use this to clamp displaced texture coords so they belong to filterArea // see displacementFilter fragment shader for an example - if (shader.uniforms.data.filterClamp) + if (shader.uniforms.filterClamp) { - currentState = this.filterData.stack[this.filterData.index]; + currentState = currentState || this.filterData.stack[this.filterData.index]; const filterClamp = shader.uniforms.filterClamp; @@ -485,7 +489,7 @@ */ destroy() { - this.shaderCache = []; + this.shaderCache = {}; this.emptyPool(); } diff --git a/src/core/renderers/webgl/utils/RenderTarget.js b/src/core/renderers/webgl/utils/RenderTarget.js index afe51ab..6d2467f 100644 --- a/src/core/renderers/webgl/utils/RenderTarget.js +++ b/src/core/renderers/webgl/utils/RenderTarget.js @@ -124,7 +124,7 @@ * @default PIXI.settings.SCALE_MODE * @see PIXI.SCALE_MODES */ - this.scaleMode = scaleMode || settings.SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; /** * Whether this object is the root element or not 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 36ea5bb..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; @@ -346,8 +346,8 @@ /** * Gets the local bounds of the sprite object. * - * @param {Rectangle} rect - The output rectangle. - * @return {Rectangle} The bounds. + * @param {PIXI.Rectangle} rect - The output rectangle. + * @return {PIXI.Rectangle} The bounds. */ getLocalBounds(rect) { @@ -440,7 +440,7 @@ * * @static * @param {number|string|PIXI.BaseTexture|HTMLCanvasElement|HTMLVideoElement} source Source to create texture from - * @return {PIXI.Texture} 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 e77b39a..90b08c0 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++) @@ -103,11 +102,18 @@ { const gl = this.renderer.gl; - // step 1: first check max textures the GPU can handle. - this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); + if (this.renderer.legacy) + { + this.MAX_TEXTURES = 1; + } + else + { + // step 1: first check max textures the GPU can handle. + this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); - // step 2: check the maximum number of if statements the shader can have too.. - this.MAX_TEXTURES = checkMaxIfStatmentsInShader(this.MAX_TEXTURES, gl); + // step 2: check the maximum number of if statements the shader can have too.. + this.MAX_TEXTURES = checkMaxIfStatmentsInShader(this.MAX_TEXTURES, gl); + } const shader = this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); @@ -130,8 +136,12 @@ .addIndex(this.indexBuffer) .addAttribute(this.vertexBuffers[i], shader.attributes.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) .addAttribute(this.vertexBuffers[i], shader.attributes.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) - .addAttribute(this.vertexBuffers[i], shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4) - .addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + .addAttribute(this.vertexBuffers[i], shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); + + if (shader.attributes.aTextureId) + { + this.vaos[i].addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + } /* eslint-enable max-len */ } @@ -378,8 +388,12 @@ .addIndex(this.indexBuffer) .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) - .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4) - .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); + + if (this.shader.attributes.aTextureId) + { + this.vaos[this.vertexCount].addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + } /* eslint-enable max-len */ } diff --git a/src/core/text/Text.js b/src/core/text/Text.js index 013625a..cec8c32 100644 --- a/src/core/text/Text.js +++ b/src/core/text/Text.js @@ -6,6 +6,7 @@ import { TEXT_GRADIENT } from '../const'; import settings from '../settings'; import TextStyle from './TextStyle'; +import trimCanvas from '../utils/trimCanvas'; const defaultDestroyOptions = { texture: true, @@ -57,7 +58,7 @@ /** * The canvas 2d context that everything is drawn with - * @member {HTMLCanvasElement} + * @member {CanvasRenderingContext2D} */ this.context = this.canvas.getContext('2d'); @@ -189,10 +190,12 @@ if (style.dropShadow) { + this.context.shadowBlur = style.dropShadowBlur; + this.context.globalAlpha = style.dropShadowAlpha; + if (style.dropShadowBlur > 0) { this.context.shadowColor = style.dropShadowColor; - this.context.shadowBlur = style.dropShadowBlur; } else { @@ -237,6 +240,10 @@ } } + // reset the shadow blur and alpha that was set by the drop shadow, for the regular text + this.context.shadowBlur = 0; + this.context.globalAlpha = 1; + // set canvas text styles this.context.fillStyle = this._generateFillStyle(style, lines); @@ -326,6 +333,15 @@ */ updateTexture() { + if (this._style.trim) + { + const trimmed = trimCanvas(this.canvas); + + this.canvas.width = trimmed.width; + this.canvas.height = trimmed.height; + this.context.putImageData(trimmed.data, 0, 0); + } + const texture = this._texture; const style = this._style; @@ -475,6 +491,19 @@ } /** + * Gets the local bounds of the text object. + * + * @param {Rectangle} rect - The output rectangle. + * @return {Rectangle} The bounds. + */ + getLocalBounds(rect) + { + this.updateText(true); + + return super.getLocalBounds.call(this, rect); + } + + /** * calculates the bounds of the Text as a rectangle. The bounds calculation takes the worldTransform into account. */ _calculateBounds() @@ -525,6 +554,29 @@ const width = this.canvas.width / this.resolution; const height = this.canvas.height / this.resolution; + // make a copy of the style settings, so we can manipulate them later + const fill = style.fill.slice(); + const fillGradientStops = style.fillGradientStops.slice(); + + // wanting to evenly distribute the fills. So an array of 4 colours should give fills of 0.25, 0.5 and 0.75 + if (!fillGradientStops.length) + { + const lengthPlus1 = fill.length + 1; + + for (let i = 1; i < lengthPlus1; ++i) + { + fillGradientStops.push(i / lengthPlus1); + } + } + + // stop the bleeding of the last gradient on the line above to the top gradient of the this line + // by hard defining the first gradient colour at point 0, and last gradient colour at point 1 + fill.unshift(style.fill[0]); + fillGradientStops.unshift(0); + + fill.push(style.fill[style.fill.length - 1]); + fillGradientStops.push(1); + if (style.fillGradientType === TEXT_GRADIENT.LINEAR_VERTICAL) { // start the gradient at the top center of the canvas, and end at the bottom middle of the canvas @@ -532,15 +584,22 @@ // we need to repeat the gradient so that each individual line of text has the same vertical gradient effect // ['#FF0000', '#00FF00', '#0000FF'] over 2 lines would create stops at 0.125, 0.25, 0.375, 0.625, 0.75, 0.875 - totalIterations = (style.fill.length + 1) * lines.length; + totalIterations = (fill.length + 1) * lines.length; currentIteration = 0; for (let i = 0; i < lines.length; i++) { currentIteration += 1; - for (let j = 0; j < style.fill.length; j++) + for (let j = 0; j < fill.length; j++) { - stop = (currentIteration / totalIterations); - gradient.addColorStop(stop, style.fill[j]); + if (fillGradientStops[j]) + { + stop = (fillGradientStops[j] / lines.length) + (i / lines.length); + } + else + { + stop = currentIteration / totalIterations; + } + gradient.addColorStop(stop, fill[j]); currentIteration++; } } @@ -552,13 +611,20 @@ // can just evenly space out the gradients in this case, as multiple lines makes no difference // to an even left to right gradient - totalIterations = style.fill.length + 1; + totalIterations = fill.length + 1; currentIteration = 1; - for (let i = 0; i < style.fill.length; i++) + for (let i = 0; i < fill.length; i++) { - stop = currentIteration / totalIterations; - gradient.addColorStop(stop, style.fill[i]); + if (fillGradientStops[i]) + { + stop = fillGradientStops[i]; + } + else + { + stop = currentIteration / totalIterations; + } + gradient.addColorStop(stop, fill[i]); currentIteration++; } } @@ -569,7 +635,7 @@ /** * Destroys this text object. * Note* Unlike a Sprite, a Text object will automatically destroy its baseTexture and texture as - * the majorety of the time the texture will not be shared with any other Sprites. + * the majority of the time the texture will not be shared with any other Sprites. * * @param {object|boolean} [options] - Options parameter. A boolean will act as if all options * have been set to that value @@ -680,7 +746,7 @@ set text(text) // eslint-disable-line require-jsdoc { - text = String(text || ' '); + text = String(text === '' || text === null || text === undefined ? ' ' : text); if (this._text === text) { diff --git a/src/core/text/TextStyle.js b/src/core/text/TextStyle.js index 3b25528..890950b 100644 --- a/src/core/text/TextStyle.js +++ b/src/core/text/TextStyle.js @@ -8,12 +8,14 @@ align: 'left', breakWords: false, dropShadow: false, + dropShadowAlpha: 1, dropShadowAngle: Math.PI / 6, dropShadowBlur: 0, dropShadowColor: '#000000', dropShadowDistance: 5, fill: 'black', fillGradientType: TEXT_GRADIENT.LINEAR_VERTICAL, + fillGradientStops: [], fontFamily: 'Arial', fontSize: 26, fontStyle: 'normal', @@ -27,6 +29,7 @@ stroke: 'black', strokeThickness: 0, textBaseline: 'alphabetic', + trim: false, wordWrap: false, wordWrapWidth: 100, }; @@ -47,6 +50,7 @@ * @param {boolean} [style.breakWords=false] - Indicates if lines can be wrapped within words, it * needs wordWrap to be set to true * @param {boolean} [style.dropShadow=false] - Set a drop shadow for the text + * @param {number} [style.dropShadowAlpha=1] - Set alpha for the drop shadow * @param {number} [style.dropShadowAngle=Math.PI/6] - Set a angle of the drop shadow * @param {number} [style.dropShadowBlur=0] - Set a shadow blur radius * @param {string} [style.dropShadowColor='#000000'] - A fill style to be used on the dropshadow e.g 'red', '#00FF00' @@ -55,8 +59,10 @@ * fillstyle that will be used on the text e.g 'red', '#00FF00'. Can be an array to create a gradient * eg ['#000000','#FFFFFF'] * {@link https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/fillStyle|MDN} - * @param {number} [style.fillGradientType=PIXI.TEXT_GRADIENT.LINEAR_VERTICAL] - If fills styles are - * supplied, this can change the type/direction of the gradient. See {@link PIXI.TEXT_GRADIENT} for possible values + * @param {number} [style.fillGradientType=PIXI.TEXT_GRADIENT.LINEAR_VERTICAL] - If fill is an array of colours + * to create a gradient, this can change the type/direction of the gradient. See {@link PIXI.TEXT_GRADIENT} + * @param {number[]} [style.fillGradientStops] - If fill is an array of colours to create a gradient, this array can set + * the stop points (numbers between 0 and 1) for the color, overriding the default behaviour of evenly spacing them. * @param {string|string[]} [style.fontFamily='Arial'] - The font family * @param {number|string} [style.fontSize=26] - The font size (as a number it converts to px, but as a string, * equivalents are '26px','20pt','160%' or '1.6em') @@ -76,6 +82,7 @@ * e.g 'blue', '#FCFF00' * @param {number} [style.strokeThickness=0] - A number that represents the thickness of the stroke. * Default is 0 (no stroke) + * @param {boolean} [style.trim=false] - Trim transparent borders * @param {string} [style.textBaseline='alphabetic'] - The baseline of the text that is rendered. * @param {boolean} [style.wordWrap=false] - Indicates if word wrap should be used * @param {number} [style.wordWrapWidth=100] - The width at which text will wrap, it needs wordWrap to be set to true @@ -152,6 +159,19 @@ } } + get dropShadowAlpha() + { + return this._dropShadowAlpha; + } + set dropShadowAlpha(dropShadowAlpha) + { + if (this._dropShadowAlpha !== dropShadowAlpha) + { + this._dropShadowAlpha = dropShadowAlpha; + this.styleID++; + } + } + get dropShadowAngle() { return this._dropShadowAngle; @@ -232,6 +252,19 @@ } } + get fillGradientStops() + { + return this._fillGradientStops; + } + set fillGradientStops(fillGradientStops) + { + if (!areArraysEqual(this._fillGradientStops,fillGradientStops)) + { + this._fillGradientStops = fillGradientStops; + this.styleID++; + } + } + get fontFamily() { return this._fontFamily; @@ -402,6 +435,19 @@ } } + get trim() + { + return this._trim; + } + set trim(trim) + { + if (this._trim !== trim) + { + this._trim = trim; + this.styleID++; + } + } + get wordWrap() { return this._wordWrap; @@ -475,3 +521,34 @@ return color; } } + +/** + * Utility function to convert hexadecimal colors to strings, and simply return the color if it's a string. + * This version can also convert array of colors + * + * @param {Array} array1 First array to compare + * @param {Array} array2 Second array to compare + * @return {boolean} Do the arrays contain the same values in the same order + */ +function areArraysEqual(array1, array2) +{ + if (!Array.isArray(array1) || !Array.isArray(array2)) + { + return false; + } + + if (array1.length !== array2.length) + { + return false; + } + + for (let i = 0; i < array1.length; ++i) + { + if (array1[i] !== array2[i]) + { + return false; + } + } + + return true; +} diff --git a/src/core/textures/BaseRenderTexture.js b/src/core/textures/BaseRenderTexture.js index 5d50146..aa727f0 100644 --- a/src/core/textures/BaseRenderTexture.js +++ b/src/core/textures/BaseRenderTexture.js @@ -1,8 +1,6 @@ import BaseTexture from './BaseTexture'; import settings from '../settings'; -const { RESOLUTION, SCALE_MODE } = settings; - /** * A BaseRenderTexture is a special texture that allows any Pixi display object to be rendered to it. * @@ -54,7 +52,7 @@ { super(null, scaleMode); - this.resolution = resolution || RESOLUTION; + this.resolution = resolution || settings.RESOLUTION; this.width = width; this.height = height; @@ -62,7 +60,7 @@ this.realWidth = this.width * this.resolution; this.realHeight = this.height * this.resolution; - this.scaleMode = scaleMode || SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; this.hasLoaded = true; /** diff --git a/src/core/textures/BaseTexture.js b/src/core/textures/BaseTexture.js index 676dca3..aeccf39 100644 --- a/src/core/textures/BaseTexture.js +++ b/src/core/textures/BaseTexture.js @@ -77,7 +77,7 @@ * @default PIXI.settings.SCALE_MODE * @see PIXI.SCALE_MODES */ - this.scaleMode = scaleMode || settings.SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; /** * Set to true once the base texture has successfully loaded. @@ -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/RenderTexture.js b/src/core/textures/RenderTexture.js index 4c5034f..b3c6297 100644 --- a/src/core/textures/RenderTexture.js +++ b/src/core/textures/RenderTexture.js @@ -54,8 +54,8 @@ /* eslint-disable prefer-rest-params, no-console */ const width = arguments[1]; const height = arguments[2]; - const scaleMode = arguments[3] || 0; - const resolution = arguments[4] || 1; + const scaleMode = arguments[3]; + const resolution = arguments[4]; // we have an old render texture.. console.warn(`Please use RenderTexture.create(${width}, ${height}) instead of the ctor directly.`); 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 ca31456..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 @@ -459,7 +497,9 @@ if (frame.x + frame.width > this.baseTexture.width || frame.y + frame.height > this.baseTexture.height) { - throw new Error(`Texture Error: frame does not fit inside the base Texture dimensions ${this}`); + throw new Error('Texture Error: frame does not fit inside the base Texture dimensions: ' + + `X: ${frame.x} + ${frame.width} > ${this.baseTexture.width} ` + + `Y: ${frame.y} + ${frame.height} > ${this.baseTexture.height}`); } // this.valid = frame && frame.width && frame.height && this.baseTexture.source && this.baseTexture.hasLoaded; @@ -520,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. @@ -528,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 2a7497b..99e585a 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)); } /** @@ -360,3 +362,43 @@ * @private */ export const BaseTextureCache = {}; + +/** + * 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/core/utils/trimCanvas.js b/src/core/utils/trimCanvas.js new file mode 100644 index 0000000..5af6de0 --- /dev/null +++ b/src/core/utils/trimCanvas.js @@ -0,0 +1,83 @@ +/** + * Trim transparent borders from a canvas + * + * @memberof PIXI + * @function trimCanvas + * @private + * @param {HTMLCanvasElement} canvas - the canvas to trim + * @returns {object} Trim data + */ +export default function trimCanvas(canvas) +{ + // https://gist.github.com/remy/784508 + + let width = canvas.width; + let height = canvas.height; + + const context = canvas.getContext('2d'); + const imageData = context.getImageData(0, 0, width, height); + const pixels = imageData.data; + const len = pixels.length; + + const bound = { + top: null, + left: null, + right: null, + bottom: null, + }; + let i; + let x; + let y; + + for (i = 0; i < len; i += 4) + { + if (pixels[i + 3] !== 0) + { + x = (i / 4) % width; + y = ~~((i / 4) / width); + + if (bound.top === null) + { + bound.top = y; + } + + if (bound.left === null) + { + bound.left = x; + } + else if (x < bound.left) + { + bound.left = x; + } + + if (bound.right === null) + { + bound.right = x + 1; + } + else if (bound.right < x) + { + bound.right = x + 1; + } + + if (bound.bottom === null) + { + bound.bottom = y; + } + else if (bound.bottom < y) + { + bound.bottom = y; + } + } + } + + width = bound.right - bound.left; + height = bound.bottom - bound.top + 1; + + const data = context.getImageData(bound.left, bound.top, width, height); + + return { + height, + width, + data, + }; +} diff --git a/src/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/extras/TilingSprite.js b/src/extras/TilingSprite.js index f90d248..fe82451 100644 --- a/src/extras/TilingSprite.js +++ b/src/extras/TilingSprite.js @@ -220,20 +220,33 @@ transform.tx * resolution, transform.ty * resolution); - // TODO - this should be rolled into the setTransform above.. - context.scale(this.tileScale.x / baseTextureResolution, this.tileScale.y / baseTextureResolution); - - context.translate(modX + (this.anchor.x * -this._width), - modY + (this.anchor.y * -this._height)); - renderer.setBlendMode(this.blendMode); // fill the pattern! context.fillStyle = this._canvasPattern; - context.fillRect(-modX, - -modY, - this._width / this.tileScale.x * baseTextureResolution, - this._height / this.tileScale.y * baseTextureResolution); + + // TODO - this should be rolled into the setTransform above.. + context.scale(this.tileScale.x / baseTextureResolution, this.tileScale.y / baseTextureResolution); + + const anchorX = this.anchor.x * -this._width; + const anchorY = this.anchor.y * -this._height; + + if (this.uvRespectAnchor) + { + context.translate(modX, modY); + + context.fillRect(-modX + anchorX, -modY + anchorY, + this._width / this.tileScale.x * baseTextureResolution, + this._height / this.tileScale.y * baseTextureResolution); + } + else + { + context.translate(modX + anchorX, modY + anchorY); + + context.fillRect(-modX, -modY, + this._width / this.tileScale.x * baseTextureResolution, + this._height / this.tileScale.y * baseTextureResolution); + } } /** diff --git a/src/extras/cacheAsBitmap.js b/src/extras/cacheAsBitmap.js index 18043a7..5dd5640 100644 --- a/src/extras/cacheAsBitmap.js +++ b/src/extras/cacheAsBitmap.js @@ -41,6 +41,9 @@ * provide a performance benefit for complex static displayObjects. * To remove simply set this property to 'false' * + * IMPORTANT GOTCHA - make sure that all your textures are preloaded BEFORE setting this property to true + * as it will take a snapshot of what is currently there. If the textures have not loaded then they will not appear. + * * @member {boolean} * @memberof PIXI.DisplayObject# */ diff --git a/src/filters/blur/BlurFilter.js b/src/filters/blur/BlurFilter.js index 340fc58..1575db9 100644 --- a/src/filters/blur/BlurFilter.js +++ b/src/filters/blur/BlurFilter.js @@ -24,10 +24,9 @@ this.blurXFilter = new BlurXFilter(strength, quality, resolution, kernelSize); this.blurYFilter = new BlurYFilter(strength, quality, resolution, kernelSize); - this.resolution = 1; this.padding = 0; - this.resolution = resolution || 1; + this.resolution = resolution || core.settings.RESOLUTION; this.quality = quality || 4; this.blur = strength || 8; } diff --git a/src/filters/blur/BlurXFilter.js b/src/filters/blur/BlurXFilter.js index def99c3..0f48889 100644 --- a/src/filters/blur/BlurXFilter.js +++ b/src/filters/blur/BlurXFilter.js @@ -31,7 +31,7 @@ fragSrc ); - this.resolution = resolution || 1; + this.resolution = resolution || core.settings.RESOLUTION; this._quality = 0; diff --git a/src/filters/blur/BlurYFilter.js b/src/filters/blur/BlurYFilter.js index 638e0f0..6d489c7 100644 --- a/src/filters/blur/BlurYFilter.js +++ b/src/filters/blur/BlurYFilter.js @@ -31,7 +31,7 @@ fragSrc ); - this.resolution = resolution || 1; + this.resolution = resolution || core.settings.RESOLUTION; this._quality = 0; diff --git a/package.json b/package.json index 1271fef..71ade15 100644 --- a/package.json +++ b/package.json @@ -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/scripts/release.js b/scripts/release.js index c4c8e36..156620c 100644 --- a/scripts/release.js +++ b/scripts/release.js @@ -12,7 +12,8 @@ 'dist/**/*', 'lib/**/*', 'src/**/*', - 'scripts/**/*', + 'scripts/*', + 'scripts/renders/*', 'test/**/*', '*.json', '*.md', diff --git a/scripts/renders/client.js b/scripts/renders/client.js index 16c9565..6faee34 100755 --- a/scripts/renders/client.js +++ b/scripts/renders/client.js @@ -1,6 +1,6 @@ 'use strict'; -require('../../bin/pixi'); +require('../../dist/pixi'); PIXI.utils.skipHello(); const fs = require('fs'); 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 bc730db..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. @@ -33,8 +33,11 @@ * need to call toDataUrl on the webgl context * @param {number} [options.resolution=1] - The resolution / device pixel ratio of the renderer, retina would be 2 * @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 @@ -43,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. */ @@ -73,7 +99,7 @@ */ stop() { - this.ticker.stop(); + this._ticker.stop(); } /** @@ -81,7 +107,7 @@ */ start() { - this.ticker.start(); + this._ticker.start(); } /** @@ -95,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 index 81e076b..78b17e6 100644 --- a/src/core/Shader.js +++ b/src/core/Shader.js @@ -1,9 +1,7 @@ import { GLShader } from 'pixi-gl-core'; import settings from './settings'; -const { PRECISION } = settings; - -function checkPrecision(src) +function checkPrecision(src, def) { if (src instanceof Array) { @@ -11,14 +9,14 @@ { const copy = src.slice(0); - copy.unshift(`precision ${PRECISION} float;`); + copy.unshift(`precision ${def} float;`); return copy; } } else if (src.substring(0, 9) !== 'precision') { - return `precision ${PRECISION} float;\n${src}`; + return `precision ${def} float;\n${src}`; } return src; @@ -42,6 +40,7 @@ */ constructor(gl, vertexSrc, fragmentSrc) { - super(gl, checkPrecision(vertexSrc), checkPrecision(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 3425b41..f9235e4 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -181,7 +181,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 b3792ef..17fde8d 100644 --- a/src/core/display/Container.js +++ b/src/core/display/Container.js @@ -73,13 +73,14 @@ } child.parent = this; - - // ensure a transform will be recalculated.. - this.transform._parentID = -1; - this._boundsID++; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.push(child); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(this.children.length - 1); child.emit('added', this); @@ -108,9 +109,14 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.splice(index, 0, child); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(index); child.emit('added', this); @@ -174,6 +180,7 @@ removeItems(this.children, currentIndex, 1); // remove from old position this.children.splice(index, 0, child); // add at new position + this.onChildrenChange(index); } @@ -220,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! @@ -244,9 +252,14 @@ { const child = this.getChildAt(index); + // ensure child transform will be recalculated.. child.parent = null; + child.transform._parentID = -1; removeItems(this.children, index, 1); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(index); child.emit('removed', this); @@ -275,8 +288,14 @@ for (let i = 0; i < removed.length; ++i) { removed[i].parent = null; + if (removed[i].transform) + { + removed[i].transform._parentID = -1; + } } + this._boundsID++; + this.onChildrenChange(beginIndex); for (let i = 0; i < removed.length; ++i) diff --git a/src/core/display/DisplayObject.js b/src/core/display/DisplayObject.js index 3affe36..4e6c77d 100644 --- a/src/core/display/DisplayObject.js +++ b/src/core/display/DisplayObject.js @@ -109,7 +109,7 @@ /** * The original, cached mask of the object * - * @member {PIXI.Rectangle} + * @member {PIXI.Graphics|PIXI.Sprite} * @private */ this._mask = null; diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index 95f2af4..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 ); @@ -1065,10 +1061,15 @@ canvasRenderer = new CanvasRenderer(); } - tempMatrix.tx = -bounds.x; - tempMatrix.ty = -bounds.y; + this.transform.updateLocalTransform(); + this.transform.localTransform.copy(tempMatrix); - canvasRenderer.render(this, canvasBuffer, false, tempMatrix); + tempMatrix.invert(); + + tempMatrix.tx -= bounds.x; + tempMatrix.ty -= bounds.y; + + canvasRenderer.render(this, canvasBuffer, true, tempMatrix); const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode); 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 e67680f..867dde2 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/buildCircle.js b/src/core/graphics/webgl/utils/buildCircle.js index 0400d13..abf5d02 100644 --- a/src/core/graphics/webgl/utils/buildCircle.js +++ b/src/core/graphics/webgl/utils/buildCircle.js @@ -33,6 +33,11 @@ height = circleData.height; } + if (width === 0 || height === 0) + { + return; + } + const totalSegs = Math.floor(30 * Math.sqrt(circleData.radius)) || Math.floor(15 * Math.sqrt(circleData.width + circleData.height)); diff --git a/src/core/graphics/webgl/utils/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 90155f9..f03fe15 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -30,6 +30,7 @@ export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; export { default as CanvasGraphicsRenderer } from './graphics/canvas/CanvasGraphicsRenderer'; +export { default as Spritesheet } from './textures/Spritesheet'; export { default as Texture } from './textures/Texture'; export { default as BaseTexture } from './textures/BaseTexture'; export { default as RenderTexture } from './textures/RenderTexture'; diff --git a/src/core/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 ccce680..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); } /** @@ -162,6 +162,9 @@ if (transform) { transform.copy(tempWt); + + // lets not forget to flag the parent transform as dirty... + this._tempDisplayObjectParent.transform._worldID = -1; } else { @@ -169,6 +172,7 @@ } displayObject.parent = this._tempDisplayObjectParent; + displayObject.updateTransform(); displayObject.parent = cacheParent; // displayObject.hitArea = //TODO add a temp hit area @@ -279,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/canvas/utils/CanvasRenderTarget.js b/src/core/renderers/canvas/utils/CanvasRenderTarget.js index 3de0aef..b409891 100644 --- a/src/core/renderers/canvas/utils/CanvasRenderTarget.js +++ b/src/core/renderers/canvas/utils/CanvasRenderTarget.js @@ -1,5 +1,4 @@ import settings from '../../../settings'; -const { RESOLUTION } = settings; /** * Creates a Canvas element of the given size. @@ -30,7 +29,7 @@ */ this.context = this.canvas.getContext('2d'); - this.resolution = resolution || RESOLUTION; + this.resolution = resolution || settings.RESOLUTION; this.resize(width, height); } 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 ce0e1c4..788ed6c 100644 --- a/src/core/renderers/webgl/WebGLRenderer.js +++ b/src/core/renderers/webgl/WebGLRenderer.js @@ -30,8 +30,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 @@ -49,10 +49,19 @@ * enable this if you need to call toDataUrl on the webgl context. * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when * rendering, stopping pixel interpolation. + * @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; + + if (this.legacy) + { + glCore.VertexArrayObject.FORCE_NATIVE = true; + } /** * The type of this renderer as a standardised const @@ -229,7 +238,7 @@ this.emit('context', gl); // setup the width/height properties and gl viewport - this.resize(this.width, this.height); + this.resize(this.screen.width, this.screen.height); } /** @@ -323,16 +332,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) { @@ -376,6 +385,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 @@ -440,9 +469,10 @@ * Changes the current shader to the one given in parameter * * @param {PIXI.Shader} shader - the new shader + * @param {boolean} [autoProject=true] - Whether automatically set the projection matrix * @return {PIXI.WebGLRenderer} Returns itself. */ - bindShader(shader) + bindShader(shader, autoProject) { // TODO cache if (this._activeShader !== shader) @@ -450,8 +480,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 bbab3b3..944f41d 100644 --- a/src/core/renderers/webgl/filters/Filter.js +++ b/src/core/renderers/webgl/filters/Filter.js @@ -1,6 +1,7 @@ import extractUniformsFromSrc from './extractUniformsFromSrc'; import { uid } from '../../../utils'; import { BLEND_MODES } from '../../../const'; +import settings from '../../../settings'; const SOURCE_KEY_MAP = {}; @@ -10,7 +11,7 @@ * @memberof PIXI * @extends PIXI.Shader */ -class Filter +export default class Filter { /** * @param {string} [vertexSrc] - The source of the vertex shader. @@ -35,8 +36,6 @@ this.blendMode = BLEND_MODES.NORMAL; - // pull out the vertex and shader uniforms if they are not specified.. - // currently this does not extract structs only default types this.uniformData = uniforms || extractUniformsFromSrc(this.vertexSrc, this.fragmentSrc, 'projectionMatrix|uSampler'); /** @@ -80,7 +79,7 @@ * * @member {number} */ - this.resolution = 1; + this.resolution = settings.RESOLUTION; /** * If enabled is true the filter is applied, if false it will not. @@ -97,8 +96,11 @@ * @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 { // --- // // this.uniforms.filterMatrix = filterManager.calculateSpriteMatrix(tempMatrix, window.panda ); @@ -170,5 +172,3 @@ ].join('\n'); } } - -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 dc7072a..6c7422d 100644 --- a/src/core/renderers/webgl/managers/FilterManager.js +++ b/src/core/renderers/webgl/managers/FilterManager.js @@ -154,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 @@ -176,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; @@ -184,7 +184,7 @@ flop = t; } - filters[i].apply(this, flip, lastState.renderTarget, true); + filters[i].apply(this, flip, lastState.renderTarget, false, currentState); this.freePotRenderTarget(flip); this.freePotRenderTarget(flop); @@ -291,9 +291,13 @@ let textureCount = 1; let currentState; - if (shader.uniforms.data.filterArea) + // 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; @@ -306,9 +310,9 @@ // use this to clamp displaced texture coords so they belong to filterArea // see displacementFilter fragment shader for an example - if (shader.uniforms.data.filterClamp) + if (shader.uniforms.filterClamp) { - currentState = this.filterData.stack[this.filterData.index]; + currentState = currentState || this.filterData.stack[this.filterData.index]; const filterClamp = shader.uniforms.filterClamp; @@ -485,7 +489,7 @@ */ destroy() { - this.shaderCache = []; + this.shaderCache = {}; this.emptyPool(); } diff --git a/src/core/renderers/webgl/utils/RenderTarget.js b/src/core/renderers/webgl/utils/RenderTarget.js index afe51ab..6d2467f 100644 --- a/src/core/renderers/webgl/utils/RenderTarget.js +++ b/src/core/renderers/webgl/utils/RenderTarget.js @@ -124,7 +124,7 @@ * @default PIXI.settings.SCALE_MODE * @see PIXI.SCALE_MODES */ - this.scaleMode = scaleMode || settings.SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; /** * Whether this object is the root element or not 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 36ea5bb..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; @@ -346,8 +346,8 @@ /** * Gets the local bounds of the sprite object. * - * @param {Rectangle} rect - The output rectangle. - * @return {Rectangle} The bounds. + * @param {PIXI.Rectangle} rect - The output rectangle. + * @return {PIXI.Rectangle} The bounds. */ getLocalBounds(rect) { @@ -440,7 +440,7 @@ * * @static * @param {number|string|PIXI.BaseTexture|HTMLCanvasElement|HTMLVideoElement} source Source to create texture from - * @return {PIXI.Texture} 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 e77b39a..90b08c0 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++) @@ -103,11 +102,18 @@ { const gl = this.renderer.gl; - // step 1: first check max textures the GPU can handle. - this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); + if (this.renderer.legacy) + { + this.MAX_TEXTURES = 1; + } + else + { + // step 1: first check max textures the GPU can handle. + this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); - // step 2: check the maximum number of if statements the shader can have too.. - this.MAX_TEXTURES = checkMaxIfStatmentsInShader(this.MAX_TEXTURES, gl); + // step 2: check the maximum number of if statements the shader can have too.. + this.MAX_TEXTURES = checkMaxIfStatmentsInShader(this.MAX_TEXTURES, gl); + } const shader = this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); @@ -130,8 +136,12 @@ .addIndex(this.indexBuffer) .addAttribute(this.vertexBuffers[i], shader.attributes.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) .addAttribute(this.vertexBuffers[i], shader.attributes.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) - .addAttribute(this.vertexBuffers[i], shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4) - .addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + .addAttribute(this.vertexBuffers[i], shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); + + if (shader.attributes.aTextureId) + { + this.vaos[i].addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + } /* eslint-enable max-len */ } @@ -378,8 +388,12 @@ .addIndex(this.indexBuffer) .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) - .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4) - .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); + + if (this.shader.attributes.aTextureId) + { + this.vaos[this.vertexCount].addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + } /* eslint-enable max-len */ } diff --git a/src/core/text/Text.js b/src/core/text/Text.js index 013625a..cec8c32 100644 --- a/src/core/text/Text.js +++ b/src/core/text/Text.js @@ -6,6 +6,7 @@ import { TEXT_GRADIENT } from '../const'; import settings from '../settings'; import TextStyle from './TextStyle'; +import trimCanvas from '../utils/trimCanvas'; const defaultDestroyOptions = { texture: true, @@ -57,7 +58,7 @@ /** * The canvas 2d context that everything is drawn with - * @member {HTMLCanvasElement} + * @member {CanvasRenderingContext2D} */ this.context = this.canvas.getContext('2d'); @@ -189,10 +190,12 @@ if (style.dropShadow) { + this.context.shadowBlur = style.dropShadowBlur; + this.context.globalAlpha = style.dropShadowAlpha; + if (style.dropShadowBlur > 0) { this.context.shadowColor = style.dropShadowColor; - this.context.shadowBlur = style.dropShadowBlur; } else { @@ -237,6 +240,10 @@ } } + // reset the shadow blur and alpha that was set by the drop shadow, for the regular text + this.context.shadowBlur = 0; + this.context.globalAlpha = 1; + // set canvas text styles this.context.fillStyle = this._generateFillStyle(style, lines); @@ -326,6 +333,15 @@ */ updateTexture() { + if (this._style.trim) + { + const trimmed = trimCanvas(this.canvas); + + this.canvas.width = trimmed.width; + this.canvas.height = trimmed.height; + this.context.putImageData(trimmed.data, 0, 0); + } + const texture = this._texture; const style = this._style; @@ -475,6 +491,19 @@ } /** + * Gets the local bounds of the text object. + * + * @param {Rectangle} rect - The output rectangle. + * @return {Rectangle} The bounds. + */ + getLocalBounds(rect) + { + this.updateText(true); + + return super.getLocalBounds.call(this, rect); + } + + /** * calculates the bounds of the Text as a rectangle. The bounds calculation takes the worldTransform into account. */ _calculateBounds() @@ -525,6 +554,29 @@ const width = this.canvas.width / this.resolution; const height = this.canvas.height / this.resolution; + // make a copy of the style settings, so we can manipulate them later + const fill = style.fill.slice(); + const fillGradientStops = style.fillGradientStops.slice(); + + // wanting to evenly distribute the fills. So an array of 4 colours should give fills of 0.25, 0.5 and 0.75 + if (!fillGradientStops.length) + { + const lengthPlus1 = fill.length + 1; + + for (let i = 1; i < lengthPlus1; ++i) + { + fillGradientStops.push(i / lengthPlus1); + } + } + + // stop the bleeding of the last gradient on the line above to the top gradient of the this line + // by hard defining the first gradient colour at point 0, and last gradient colour at point 1 + fill.unshift(style.fill[0]); + fillGradientStops.unshift(0); + + fill.push(style.fill[style.fill.length - 1]); + fillGradientStops.push(1); + if (style.fillGradientType === TEXT_GRADIENT.LINEAR_VERTICAL) { // start the gradient at the top center of the canvas, and end at the bottom middle of the canvas @@ -532,15 +584,22 @@ // we need to repeat the gradient so that each individual line of text has the same vertical gradient effect // ['#FF0000', '#00FF00', '#0000FF'] over 2 lines would create stops at 0.125, 0.25, 0.375, 0.625, 0.75, 0.875 - totalIterations = (style.fill.length + 1) * lines.length; + totalIterations = (fill.length + 1) * lines.length; currentIteration = 0; for (let i = 0; i < lines.length; i++) { currentIteration += 1; - for (let j = 0; j < style.fill.length; j++) + for (let j = 0; j < fill.length; j++) { - stop = (currentIteration / totalIterations); - gradient.addColorStop(stop, style.fill[j]); + if (fillGradientStops[j]) + { + stop = (fillGradientStops[j] / lines.length) + (i / lines.length); + } + else + { + stop = currentIteration / totalIterations; + } + gradient.addColorStop(stop, fill[j]); currentIteration++; } } @@ -552,13 +611,20 @@ // can just evenly space out the gradients in this case, as multiple lines makes no difference // to an even left to right gradient - totalIterations = style.fill.length + 1; + totalIterations = fill.length + 1; currentIteration = 1; - for (let i = 0; i < style.fill.length; i++) + for (let i = 0; i < fill.length; i++) { - stop = currentIteration / totalIterations; - gradient.addColorStop(stop, style.fill[i]); + if (fillGradientStops[i]) + { + stop = fillGradientStops[i]; + } + else + { + stop = currentIteration / totalIterations; + } + gradient.addColorStop(stop, fill[i]); currentIteration++; } } @@ -569,7 +635,7 @@ /** * Destroys this text object. * Note* Unlike a Sprite, a Text object will automatically destroy its baseTexture and texture as - * the majorety of the time the texture will not be shared with any other Sprites. + * the majority of the time the texture will not be shared with any other Sprites. * * @param {object|boolean} [options] - Options parameter. A boolean will act as if all options * have been set to that value @@ -680,7 +746,7 @@ set text(text) // eslint-disable-line require-jsdoc { - text = String(text || ' '); + text = String(text === '' || text === null || text === undefined ? ' ' : text); if (this._text === text) { diff --git a/src/core/text/TextStyle.js b/src/core/text/TextStyle.js index 3b25528..890950b 100644 --- a/src/core/text/TextStyle.js +++ b/src/core/text/TextStyle.js @@ -8,12 +8,14 @@ align: 'left', breakWords: false, dropShadow: false, + dropShadowAlpha: 1, dropShadowAngle: Math.PI / 6, dropShadowBlur: 0, dropShadowColor: '#000000', dropShadowDistance: 5, fill: 'black', fillGradientType: TEXT_GRADIENT.LINEAR_VERTICAL, + fillGradientStops: [], fontFamily: 'Arial', fontSize: 26, fontStyle: 'normal', @@ -27,6 +29,7 @@ stroke: 'black', strokeThickness: 0, textBaseline: 'alphabetic', + trim: false, wordWrap: false, wordWrapWidth: 100, }; @@ -47,6 +50,7 @@ * @param {boolean} [style.breakWords=false] - Indicates if lines can be wrapped within words, it * needs wordWrap to be set to true * @param {boolean} [style.dropShadow=false] - Set a drop shadow for the text + * @param {number} [style.dropShadowAlpha=1] - Set alpha for the drop shadow * @param {number} [style.dropShadowAngle=Math.PI/6] - Set a angle of the drop shadow * @param {number} [style.dropShadowBlur=0] - Set a shadow blur radius * @param {string} [style.dropShadowColor='#000000'] - A fill style to be used on the dropshadow e.g 'red', '#00FF00' @@ -55,8 +59,10 @@ * fillstyle that will be used on the text e.g 'red', '#00FF00'. Can be an array to create a gradient * eg ['#000000','#FFFFFF'] * {@link https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/fillStyle|MDN} - * @param {number} [style.fillGradientType=PIXI.TEXT_GRADIENT.LINEAR_VERTICAL] - If fills styles are - * supplied, this can change the type/direction of the gradient. See {@link PIXI.TEXT_GRADIENT} for possible values + * @param {number} [style.fillGradientType=PIXI.TEXT_GRADIENT.LINEAR_VERTICAL] - If fill is an array of colours + * to create a gradient, this can change the type/direction of the gradient. See {@link PIXI.TEXT_GRADIENT} + * @param {number[]} [style.fillGradientStops] - If fill is an array of colours to create a gradient, this array can set + * the stop points (numbers between 0 and 1) for the color, overriding the default behaviour of evenly spacing them. * @param {string|string[]} [style.fontFamily='Arial'] - The font family * @param {number|string} [style.fontSize=26] - The font size (as a number it converts to px, but as a string, * equivalents are '26px','20pt','160%' or '1.6em') @@ -76,6 +82,7 @@ * e.g 'blue', '#FCFF00' * @param {number} [style.strokeThickness=0] - A number that represents the thickness of the stroke. * Default is 0 (no stroke) + * @param {boolean} [style.trim=false] - Trim transparent borders * @param {string} [style.textBaseline='alphabetic'] - The baseline of the text that is rendered. * @param {boolean} [style.wordWrap=false] - Indicates if word wrap should be used * @param {number} [style.wordWrapWidth=100] - The width at which text will wrap, it needs wordWrap to be set to true @@ -152,6 +159,19 @@ } } + get dropShadowAlpha() + { + return this._dropShadowAlpha; + } + set dropShadowAlpha(dropShadowAlpha) + { + if (this._dropShadowAlpha !== dropShadowAlpha) + { + this._dropShadowAlpha = dropShadowAlpha; + this.styleID++; + } + } + get dropShadowAngle() { return this._dropShadowAngle; @@ -232,6 +252,19 @@ } } + get fillGradientStops() + { + return this._fillGradientStops; + } + set fillGradientStops(fillGradientStops) + { + if (!areArraysEqual(this._fillGradientStops,fillGradientStops)) + { + this._fillGradientStops = fillGradientStops; + this.styleID++; + } + } + get fontFamily() { return this._fontFamily; @@ -402,6 +435,19 @@ } } + get trim() + { + return this._trim; + } + set trim(trim) + { + if (this._trim !== trim) + { + this._trim = trim; + this.styleID++; + } + } + get wordWrap() { return this._wordWrap; @@ -475,3 +521,34 @@ return color; } } + +/** + * Utility function to convert hexadecimal colors to strings, and simply return the color if it's a string. + * This version can also convert array of colors + * + * @param {Array} array1 First array to compare + * @param {Array} array2 Second array to compare + * @return {boolean} Do the arrays contain the same values in the same order + */ +function areArraysEqual(array1, array2) +{ + if (!Array.isArray(array1) || !Array.isArray(array2)) + { + return false; + } + + if (array1.length !== array2.length) + { + return false; + } + + for (let i = 0; i < array1.length; ++i) + { + if (array1[i] !== array2[i]) + { + return false; + } + } + + return true; +} diff --git a/src/core/textures/BaseRenderTexture.js b/src/core/textures/BaseRenderTexture.js index 5d50146..aa727f0 100644 --- a/src/core/textures/BaseRenderTexture.js +++ b/src/core/textures/BaseRenderTexture.js @@ -1,8 +1,6 @@ import BaseTexture from './BaseTexture'; import settings from '../settings'; -const { RESOLUTION, SCALE_MODE } = settings; - /** * A BaseRenderTexture is a special texture that allows any Pixi display object to be rendered to it. * @@ -54,7 +52,7 @@ { super(null, scaleMode); - this.resolution = resolution || RESOLUTION; + this.resolution = resolution || settings.RESOLUTION; this.width = width; this.height = height; @@ -62,7 +60,7 @@ this.realWidth = this.width * this.resolution; this.realHeight = this.height * this.resolution; - this.scaleMode = scaleMode || SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; this.hasLoaded = true; /** diff --git a/src/core/textures/BaseTexture.js b/src/core/textures/BaseTexture.js index 676dca3..aeccf39 100644 --- a/src/core/textures/BaseTexture.js +++ b/src/core/textures/BaseTexture.js @@ -77,7 +77,7 @@ * @default PIXI.settings.SCALE_MODE * @see PIXI.SCALE_MODES */ - this.scaleMode = scaleMode || settings.SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; /** * Set to true once the base texture has successfully loaded. @@ -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/RenderTexture.js b/src/core/textures/RenderTexture.js index 4c5034f..b3c6297 100644 --- a/src/core/textures/RenderTexture.js +++ b/src/core/textures/RenderTexture.js @@ -54,8 +54,8 @@ /* eslint-disable prefer-rest-params, no-console */ const width = arguments[1]; const height = arguments[2]; - const scaleMode = arguments[3] || 0; - const resolution = arguments[4] || 1; + const scaleMode = arguments[3]; + const resolution = arguments[4]; // we have an old render texture.. console.warn(`Please use RenderTexture.create(${width}, ${height}) instead of the ctor directly.`); 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 ca31456..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 @@ -459,7 +497,9 @@ if (frame.x + frame.width > this.baseTexture.width || frame.y + frame.height > this.baseTexture.height) { - throw new Error(`Texture Error: frame does not fit inside the base Texture dimensions ${this}`); + throw new Error('Texture Error: frame does not fit inside the base Texture dimensions: ' + + `X: ${frame.x} + ${frame.width} > ${this.baseTexture.width} ` + + `Y: ${frame.y} + ${frame.height} > ${this.baseTexture.height}`); } // this.valid = frame && frame.width && frame.height && this.baseTexture.source && this.baseTexture.hasLoaded; @@ -520,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. @@ -528,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 2a7497b..99e585a 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)); } /** @@ -360,3 +362,43 @@ * @private */ export const BaseTextureCache = {}; + +/** + * 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/core/utils/trimCanvas.js b/src/core/utils/trimCanvas.js new file mode 100644 index 0000000..5af6de0 --- /dev/null +++ b/src/core/utils/trimCanvas.js @@ -0,0 +1,83 @@ +/** + * Trim transparent borders from a canvas + * + * @memberof PIXI + * @function trimCanvas + * @private + * @param {HTMLCanvasElement} canvas - the canvas to trim + * @returns {object} Trim data + */ +export default function trimCanvas(canvas) +{ + // https://gist.github.com/remy/784508 + + let width = canvas.width; + let height = canvas.height; + + const context = canvas.getContext('2d'); + const imageData = context.getImageData(0, 0, width, height); + const pixels = imageData.data; + const len = pixels.length; + + const bound = { + top: null, + left: null, + right: null, + bottom: null, + }; + let i; + let x; + let y; + + for (i = 0; i < len; i += 4) + { + if (pixels[i + 3] !== 0) + { + x = (i / 4) % width; + y = ~~((i / 4) / width); + + if (bound.top === null) + { + bound.top = y; + } + + if (bound.left === null) + { + bound.left = x; + } + else if (x < bound.left) + { + bound.left = x; + } + + if (bound.right === null) + { + bound.right = x + 1; + } + else if (bound.right < x) + { + bound.right = x + 1; + } + + if (bound.bottom === null) + { + bound.bottom = y; + } + else if (bound.bottom < y) + { + bound.bottom = y; + } + } + } + + width = bound.right - bound.left; + height = bound.bottom - bound.top + 1; + + const data = context.getImageData(bound.left, bound.top, width, height); + + return { + height, + width, + data, + }; +} diff --git a/src/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/extras/TilingSprite.js b/src/extras/TilingSprite.js index f90d248..fe82451 100644 --- a/src/extras/TilingSprite.js +++ b/src/extras/TilingSprite.js @@ -220,20 +220,33 @@ transform.tx * resolution, transform.ty * resolution); - // TODO - this should be rolled into the setTransform above.. - context.scale(this.tileScale.x / baseTextureResolution, this.tileScale.y / baseTextureResolution); - - context.translate(modX + (this.anchor.x * -this._width), - modY + (this.anchor.y * -this._height)); - renderer.setBlendMode(this.blendMode); // fill the pattern! context.fillStyle = this._canvasPattern; - context.fillRect(-modX, - -modY, - this._width / this.tileScale.x * baseTextureResolution, - this._height / this.tileScale.y * baseTextureResolution); + + // TODO - this should be rolled into the setTransform above.. + context.scale(this.tileScale.x / baseTextureResolution, this.tileScale.y / baseTextureResolution); + + const anchorX = this.anchor.x * -this._width; + const anchorY = this.anchor.y * -this._height; + + if (this.uvRespectAnchor) + { + context.translate(modX, modY); + + context.fillRect(-modX + anchorX, -modY + anchorY, + this._width / this.tileScale.x * baseTextureResolution, + this._height / this.tileScale.y * baseTextureResolution); + } + else + { + context.translate(modX + anchorX, modY + anchorY); + + context.fillRect(-modX, -modY, + this._width / this.tileScale.x * baseTextureResolution, + this._height / this.tileScale.y * baseTextureResolution); + } } /** diff --git a/src/extras/cacheAsBitmap.js b/src/extras/cacheAsBitmap.js index 18043a7..5dd5640 100644 --- a/src/extras/cacheAsBitmap.js +++ b/src/extras/cacheAsBitmap.js @@ -41,6 +41,9 @@ * provide a performance benefit for complex static displayObjects. * To remove simply set this property to 'false' * + * IMPORTANT GOTCHA - make sure that all your textures are preloaded BEFORE setting this property to true + * as it will take a snapshot of what is currently there. If the textures have not loaded then they will not appear. + * * @member {boolean} * @memberof PIXI.DisplayObject# */ diff --git a/src/filters/blur/BlurFilter.js b/src/filters/blur/BlurFilter.js index 340fc58..1575db9 100644 --- a/src/filters/blur/BlurFilter.js +++ b/src/filters/blur/BlurFilter.js @@ -24,10 +24,9 @@ this.blurXFilter = new BlurXFilter(strength, quality, resolution, kernelSize); this.blurYFilter = new BlurYFilter(strength, quality, resolution, kernelSize); - this.resolution = 1; this.padding = 0; - this.resolution = resolution || 1; + this.resolution = resolution || core.settings.RESOLUTION; this.quality = quality || 4; this.blur = strength || 8; } diff --git a/src/filters/blur/BlurXFilter.js b/src/filters/blur/BlurXFilter.js index def99c3..0f48889 100644 --- a/src/filters/blur/BlurXFilter.js +++ b/src/filters/blur/BlurXFilter.js @@ -31,7 +31,7 @@ fragSrc ); - this.resolution = resolution || 1; + this.resolution = resolution || core.settings.RESOLUTION; this._quality = 0; diff --git a/src/filters/blur/BlurYFilter.js b/src/filters/blur/BlurYFilter.js index 638e0f0..6d489c7 100644 --- a/src/filters/blur/BlurYFilter.js +++ b/src/filters/blur/BlurYFilter.js @@ -31,7 +31,7 @@ fragSrc ); - this.resolution = resolution || 1; + this.resolution = resolution || core.settings.RESOLUTION; this._quality = 0; diff --git a/src/filters/colormatrix/ColorMatrixFilter.js b/src/filters/colormatrix/ColorMatrixFilter.js index 8509505..679f8ec 100644 --- a/src/filters/colormatrix/ColorMatrixFilter.js +++ b/src/filters/colormatrix/ColorMatrixFilter.js @@ -75,28 +75,28 @@ out[1] = (a[0] * b[1]) + (a[1] * b[6]) + (a[2] * b[11]) + (a[3] * b[16]); out[2] = (a[0] * b[2]) + (a[1] * b[7]) + (a[2] * b[12]) + (a[3] * b[17]); out[3] = (a[0] * b[3]) + (a[1] * b[8]) + (a[2] * b[13]) + (a[3] * b[18]); - out[4] = (a[0] * b[4]) + (a[1] * b[9]) + (a[2] * b[14]) + (a[3] * b[19]); + out[4] = (a[0] * b[4]) + (a[1] * b[9]) + (a[2] * b[14]) + (a[3] * b[19]) + a[4]; // Green Channel out[5] = (a[5] * b[0]) + (a[6] * b[5]) + (a[7] * b[10]) + (a[8] * b[15]); out[6] = (a[5] * b[1]) + (a[6] * b[6]) + (a[7] * b[11]) + (a[8] * b[16]); out[7] = (a[5] * b[2]) + (a[6] * b[7]) + (a[7] * b[12]) + (a[8] * b[17]); out[8] = (a[5] * b[3]) + (a[6] * b[8]) + (a[7] * b[13]) + (a[8] * b[18]); - out[9] = (a[5] * b[4]) + (a[6] * b[9]) + (a[7] * b[14]) + (a[8] * b[19]); + out[9] = (a[5] * b[4]) + (a[6] * b[9]) + (a[7] * b[14]) + (a[8] * b[19]) + a[9]; // Blue Channel out[10] = (a[10] * b[0]) + (a[11] * b[5]) + (a[12] * b[10]) + (a[13] * b[15]); out[11] = (a[10] * b[1]) + (a[11] * b[6]) + (a[12] * b[11]) + (a[13] * b[16]); out[12] = (a[10] * b[2]) + (a[11] * b[7]) + (a[12] * b[12]) + (a[13] * b[17]); out[13] = (a[10] * b[3]) + (a[11] * b[8]) + (a[12] * b[13]) + (a[13] * b[18]); - out[14] = (a[10] * b[4]) + (a[11] * b[9]) + (a[12] * b[14]) + (a[13] * b[19]); + out[14] = (a[10] * b[4]) + (a[11] * b[9]) + (a[12] * b[14]) + (a[13] * b[19]) + a[14]; // Alpha Channel out[15] = (a[15] * b[0]) + (a[16] * b[5]) + (a[17] * b[10]) + (a[18] * b[15]); out[16] = (a[15] * b[1]) + (a[16] * b[6]) + (a[17] * b[11]) + (a[18] * b[16]); out[17] = (a[15] * b[2]) + (a[16] * b[7]) + (a[17] * b[12]) + (a[18] * b[17]); out[18] = (a[15] * b[3]) + (a[16] * b[8]) + (a[17] * b[13]) + (a[18] * b[18]); - out[19] = (a[15] * b[4]) + (a[16] * b[9]) + (a[17] * b[14]) + (a[18] * b[19]); + out[19] = (a[15] * b[4]) + (a[16] * b[9]) + (a[17] * b[14]) + (a[18] * b[19]) + a[19]; return out; } diff --git a/package.json b/package.json index 1271fef..71ade15 100644 --- a/package.json +++ b/package.json @@ -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/scripts/release.js b/scripts/release.js index c4c8e36..156620c 100644 --- a/scripts/release.js +++ b/scripts/release.js @@ -12,7 +12,8 @@ 'dist/**/*', 'lib/**/*', 'src/**/*', - 'scripts/**/*', + 'scripts/*', + 'scripts/renders/*', 'test/**/*', '*.json', '*.md', diff --git a/scripts/renders/client.js b/scripts/renders/client.js index 16c9565..6faee34 100755 --- a/scripts/renders/client.js +++ b/scripts/renders/client.js @@ -1,6 +1,6 @@ 'use strict'; -require('../../bin/pixi'); +require('../../dist/pixi'); PIXI.utils.skipHello(); const fs = require('fs'); 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 bc730db..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. @@ -33,8 +33,11 @@ * need to call toDataUrl on the webgl context * @param {number} [options.resolution=1] - The resolution / device pixel ratio of the renderer, retina would be 2 * @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 @@ -43,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. */ @@ -73,7 +99,7 @@ */ stop() { - this.ticker.stop(); + this._ticker.stop(); } /** @@ -81,7 +107,7 @@ */ start() { - this.ticker.start(); + this._ticker.start(); } /** @@ -95,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 index 81e076b..78b17e6 100644 --- a/src/core/Shader.js +++ b/src/core/Shader.js @@ -1,9 +1,7 @@ import { GLShader } from 'pixi-gl-core'; import settings from './settings'; -const { PRECISION } = settings; - -function checkPrecision(src) +function checkPrecision(src, def) { if (src instanceof Array) { @@ -11,14 +9,14 @@ { const copy = src.slice(0); - copy.unshift(`precision ${PRECISION} float;`); + copy.unshift(`precision ${def} float;`); return copy; } } else if (src.substring(0, 9) !== 'precision') { - return `precision ${PRECISION} float;\n${src}`; + return `precision ${def} float;\n${src}`; } return src; @@ -42,6 +40,7 @@ */ constructor(gl, vertexSrc, fragmentSrc) { - super(gl, checkPrecision(vertexSrc), checkPrecision(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 3425b41..f9235e4 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -181,7 +181,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 b3792ef..17fde8d 100644 --- a/src/core/display/Container.js +++ b/src/core/display/Container.js @@ -73,13 +73,14 @@ } child.parent = this; - - // ensure a transform will be recalculated.. - this.transform._parentID = -1; - this._boundsID++; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.push(child); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(this.children.length - 1); child.emit('added', this); @@ -108,9 +109,14 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.splice(index, 0, child); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(index); child.emit('added', this); @@ -174,6 +180,7 @@ removeItems(this.children, currentIndex, 1); // remove from old position this.children.splice(index, 0, child); // add at new position + this.onChildrenChange(index); } @@ -220,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! @@ -244,9 +252,14 @@ { const child = this.getChildAt(index); + // ensure child transform will be recalculated.. child.parent = null; + child.transform._parentID = -1; removeItems(this.children, index, 1); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(index); child.emit('removed', this); @@ -275,8 +288,14 @@ for (let i = 0; i < removed.length; ++i) { removed[i].parent = null; + if (removed[i].transform) + { + removed[i].transform._parentID = -1; + } } + this._boundsID++; + this.onChildrenChange(beginIndex); for (let i = 0; i < removed.length; ++i) diff --git a/src/core/display/DisplayObject.js b/src/core/display/DisplayObject.js index 3affe36..4e6c77d 100644 --- a/src/core/display/DisplayObject.js +++ b/src/core/display/DisplayObject.js @@ -109,7 +109,7 @@ /** * The original, cached mask of the object * - * @member {PIXI.Rectangle} + * @member {PIXI.Graphics|PIXI.Sprite} * @private */ this._mask = null; diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index 95f2af4..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 ); @@ -1065,10 +1061,15 @@ canvasRenderer = new CanvasRenderer(); } - tempMatrix.tx = -bounds.x; - tempMatrix.ty = -bounds.y; + this.transform.updateLocalTransform(); + this.transform.localTransform.copy(tempMatrix); - canvasRenderer.render(this, canvasBuffer, false, tempMatrix); + tempMatrix.invert(); + + tempMatrix.tx -= bounds.x; + tempMatrix.ty -= bounds.y; + + canvasRenderer.render(this, canvasBuffer, true, tempMatrix); const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode); 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 e67680f..867dde2 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/buildCircle.js b/src/core/graphics/webgl/utils/buildCircle.js index 0400d13..abf5d02 100644 --- a/src/core/graphics/webgl/utils/buildCircle.js +++ b/src/core/graphics/webgl/utils/buildCircle.js @@ -33,6 +33,11 @@ height = circleData.height; } + if (width === 0 || height === 0) + { + return; + } + const totalSegs = Math.floor(30 * Math.sqrt(circleData.radius)) || Math.floor(15 * Math.sqrt(circleData.width + circleData.height)); diff --git a/src/core/graphics/webgl/utils/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 90155f9..f03fe15 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -30,6 +30,7 @@ export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; export { default as CanvasGraphicsRenderer } from './graphics/canvas/CanvasGraphicsRenderer'; +export { default as Spritesheet } from './textures/Spritesheet'; export { default as Texture } from './textures/Texture'; export { default as BaseTexture } from './textures/BaseTexture'; export { default as RenderTexture } from './textures/RenderTexture'; diff --git a/src/core/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 ccce680..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); } /** @@ -162,6 +162,9 @@ if (transform) { transform.copy(tempWt); + + // lets not forget to flag the parent transform as dirty... + this._tempDisplayObjectParent.transform._worldID = -1; } else { @@ -169,6 +172,7 @@ } displayObject.parent = this._tempDisplayObjectParent; + displayObject.updateTransform(); displayObject.parent = cacheParent; // displayObject.hitArea = //TODO add a temp hit area @@ -279,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/canvas/utils/CanvasRenderTarget.js b/src/core/renderers/canvas/utils/CanvasRenderTarget.js index 3de0aef..b409891 100644 --- a/src/core/renderers/canvas/utils/CanvasRenderTarget.js +++ b/src/core/renderers/canvas/utils/CanvasRenderTarget.js @@ -1,5 +1,4 @@ import settings from '../../../settings'; -const { RESOLUTION } = settings; /** * Creates a Canvas element of the given size. @@ -30,7 +29,7 @@ */ this.context = this.canvas.getContext('2d'); - this.resolution = resolution || RESOLUTION; + this.resolution = resolution || settings.RESOLUTION; this.resize(width, height); } 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 ce0e1c4..788ed6c 100644 --- a/src/core/renderers/webgl/WebGLRenderer.js +++ b/src/core/renderers/webgl/WebGLRenderer.js @@ -30,8 +30,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 @@ -49,10 +49,19 @@ * enable this if you need to call toDataUrl on the webgl context. * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when * rendering, stopping pixel interpolation. + * @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; + + if (this.legacy) + { + glCore.VertexArrayObject.FORCE_NATIVE = true; + } /** * The type of this renderer as a standardised const @@ -229,7 +238,7 @@ this.emit('context', gl); // setup the width/height properties and gl viewport - this.resize(this.width, this.height); + this.resize(this.screen.width, this.screen.height); } /** @@ -323,16 +332,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) { @@ -376,6 +385,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 @@ -440,9 +469,10 @@ * Changes the current shader to the one given in parameter * * @param {PIXI.Shader} shader - the new shader + * @param {boolean} [autoProject=true] - Whether automatically set the projection matrix * @return {PIXI.WebGLRenderer} Returns itself. */ - bindShader(shader) + bindShader(shader, autoProject) { // TODO cache if (this._activeShader !== shader) @@ -450,8 +480,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 bbab3b3..944f41d 100644 --- a/src/core/renderers/webgl/filters/Filter.js +++ b/src/core/renderers/webgl/filters/Filter.js @@ -1,6 +1,7 @@ import extractUniformsFromSrc from './extractUniformsFromSrc'; import { uid } from '../../../utils'; import { BLEND_MODES } from '../../../const'; +import settings from '../../../settings'; const SOURCE_KEY_MAP = {}; @@ -10,7 +11,7 @@ * @memberof PIXI * @extends PIXI.Shader */ -class Filter +export default class Filter { /** * @param {string} [vertexSrc] - The source of the vertex shader. @@ -35,8 +36,6 @@ this.blendMode = BLEND_MODES.NORMAL; - // pull out the vertex and shader uniforms if they are not specified.. - // currently this does not extract structs only default types this.uniformData = uniforms || extractUniformsFromSrc(this.vertexSrc, this.fragmentSrc, 'projectionMatrix|uSampler'); /** @@ -80,7 +79,7 @@ * * @member {number} */ - this.resolution = 1; + this.resolution = settings.RESOLUTION; /** * If enabled is true the filter is applied, if false it will not. @@ -97,8 +96,11 @@ * @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 { // --- // // this.uniforms.filterMatrix = filterManager.calculateSpriteMatrix(tempMatrix, window.panda ); @@ -170,5 +172,3 @@ ].join('\n'); } } - -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 dc7072a..6c7422d 100644 --- a/src/core/renderers/webgl/managers/FilterManager.js +++ b/src/core/renderers/webgl/managers/FilterManager.js @@ -154,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 @@ -176,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; @@ -184,7 +184,7 @@ flop = t; } - filters[i].apply(this, flip, lastState.renderTarget, true); + filters[i].apply(this, flip, lastState.renderTarget, false, currentState); this.freePotRenderTarget(flip); this.freePotRenderTarget(flop); @@ -291,9 +291,13 @@ let textureCount = 1; let currentState; - if (shader.uniforms.data.filterArea) + // 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; @@ -306,9 +310,9 @@ // use this to clamp displaced texture coords so they belong to filterArea // see displacementFilter fragment shader for an example - if (shader.uniforms.data.filterClamp) + if (shader.uniforms.filterClamp) { - currentState = this.filterData.stack[this.filterData.index]; + currentState = currentState || this.filterData.stack[this.filterData.index]; const filterClamp = shader.uniforms.filterClamp; @@ -485,7 +489,7 @@ */ destroy() { - this.shaderCache = []; + this.shaderCache = {}; this.emptyPool(); } diff --git a/src/core/renderers/webgl/utils/RenderTarget.js b/src/core/renderers/webgl/utils/RenderTarget.js index afe51ab..6d2467f 100644 --- a/src/core/renderers/webgl/utils/RenderTarget.js +++ b/src/core/renderers/webgl/utils/RenderTarget.js @@ -124,7 +124,7 @@ * @default PIXI.settings.SCALE_MODE * @see PIXI.SCALE_MODES */ - this.scaleMode = scaleMode || settings.SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; /** * Whether this object is the root element or not 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 36ea5bb..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; @@ -346,8 +346,8 @@ /** * Gets the local bounds of the sprite object. * - * @param {Rectangle} rect - The output rectangle. - * @return {Rectangle} The bounds. + * @param {PIXI.Rectangle} rect - The output rectangle. + * @return {PIXI.Rectangle} The bounds. */ getLocalBounds(rect) { @@ -440,7 +440,7 @@ * * @static * @param {number|string|PIXI.BaseTexture|HTMLCanvasElement|HTMLVideoElement} source Source to create texture from - * @return {PIXI.Texture} 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 e77b39a..90b08c0 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++) @@ -103,11 +102,18 @@ { const gl = this.renderer.gl; - // step 1: first check max textures the GPU can handle. - this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); + if (this.renderer.legacy) + { + this.MAX_TEXTURES = 1; + } + else + { + // step 1: first check max textures the GPU can handle. + this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); - // step 2: check the maximum number of if statements the shader can have too.. - this.MAX_TEXTURES = checkMaxIfStatmentsInShader(this.MAX_TEXTURES, gl); + // step 2: check the maximum number of if statements the shader can have too.. + this.MAX_TEXTURES = checkMaxIfStatmentsInShader(this.MAX_TEXTURES, gl); + } const shader = this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); @@ -130,8 +136,12 @@ .addIndex(this.indexBuffer) .addAttribute(this.vertexBuffers[i], shader.attributes.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) .addAttribute(this.vertexBuffers[i], shader.attributes.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) - .addAttribute(this.vertexBuffers[i], shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4) - .addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + .addAttribute(this.vertexBuffers[i], shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); + + if (shader.attributes.aTextureId) + { + this.vaos[i].addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + } /* eslint-enable max-len */ } @@ -378,8 +388,12 @@ .addIndex(this.indexBuffer) .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) - .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4) - .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); + + if (this.shader.attributes.aTextureId) + { + this.vaos[this.vertexCount].addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + } /* eslint-enable max-len */ } diff --git a/src/core/text/Text.js b/src/core/text/Text.js index 013625a..cec8c32 100644 --- a/src/core/text/Text.js +++ b/src/core/text/Text.js @@ -6,6 +6,7 @@ import { TEXT_GRADIENT } from '../const'; import settings from '../settings'; import TextStyle from './TextStyle'; +import trimCanvas from '../utils/trimCanvas'; const defaultDestroyOptions = { texture: true, @@ -57,7 +58,7 @@ /** * The canvas 2d context that everything is drawn with - * @member {HTMLCanvasElement} + * @member {CanvasRenderingContext2D} */ this.context = this.canvas.getContext('2d'); @@ -189,10 +190,12 @@ if (style.dropShadow) { + this.context.shadowBlur = style.dropShadowBlur; + this.context.globalAlpha = style.dropShadowAlpha; + if (style.dropShadowBlur > 0) { this.context.shadowColor = style.dropShadowColor; - this.context.shadowBlur = style.dropShadowBlur; } else { @@ -237,6 +240,10 @@ } } + // reset the shadow blur and alpha that was set by the drop shadow, for the regular text + this.context.shadowBlur = 0; + this.context.globalAlpha = 1; + // set canvas text styles this.context.fillStyle = this._generateFillStyle(style, lines); @@ -326,6 +333,15 @@ */ updateTexture() { + if (this._style.trim) + { + const trimmed = trimCanvas(this.canvas); + + this.canvas.width = trimmed.width; + this.canvas.height = trimmed.height; + this.context.putImageData(trimmed.data, 0, 0); + } + const texture = this._texture; const style = this._style; @@ -475,6 +491,19 @@ } /** + * Gets the local bounds of the text object. + * + * @param {Rectangle} rect - The output rectangle. + * @return {Rectangle} The bounds. + */ + getLocalBounds(rect) + { + this.updateText(true); + + return super.getLocalBounds.call(this, rect); + } + + /** * calculates the bounds of the Text as a rectangle. The bounds calculation takes the worldTransform into account. */ _calculateBounds() @@ -525,6 +554,29 @@ const width = this.canvas.width / this.resolution; const height = this.canvas.height / this.resolution; + // make a copy of the style settings, so we can manipulate them later + const fill = style.fill.slice(); + const fillGradientStops = style.fillGradientStops.slice(); + + // wanting to evenly distribute the fills. So an array of 4 colours should give fills of 0.25, 0.5 and 0.75 + if (!fillGradientStops.length) + { + const lengthPlus1 = fill.length + 1; + + for (let i = 1; i < lengthPlus1; ++i) + { + fillGradientStops.push(i / lengthPlus1); + } + } + + // stop the bleeding of the last gradient on the line above to the top gradient of the this line + // by hard defining the first gradient colour at point 0, and last gradient colour at point 1 + fill.unshift(style.fill[0]); + fillGradientStops.unshift(0); + + fill.push(style.fill[style.fill.length - 1]); + fillGradientStops.push(1); + if (style.fillGradientType === TEXT_GRADIENT.LINEAR_VERTICAL) { // start the gradient at the top center of the canvas, and end at the bottom middle of the canvas @@ -532,15 +584,22 @@ // we need to repeat the gradient so that each individual line of text has the same vertical gradient effect // ['#FF0000', '#00FF00', '#0000FF'] over 2 lines would create stops at 0.125, 0.25, 0.375, 0.625, 0.75, 0.875 - totalIterations = (style.fill.length + 1) * lines.length; + totalIterations = (fill.length + 1) * lines.length; currentIteration = 0; for (let i = 0; i < lines.length; i++) { currentIteration += 1; - for (let j = 0; j < style.fill.length; j++) + for (let j = 0; j < fill.length; j++) { - stop = (currentIteration / totalIterations); - gradient.addColorStop(stop, style.fill[j]); + if (fillGradientStops[j]) + { + stop = (fillGradientStops[j] / lines.length) + (i / lines.length); + } + else + { + stop = currentIteration / totalIterations; + } + gradient.addColorStop(stop, fill[j]); currentIteration++; } } @@ -552,13 +611,20 @@ // can just evenly space out the gradients in this case, as multiple lines makes no difference // to an even left to right gradient - totalIterations = style.fill.length + 1; + totalIterations = fill.length + 1; currentIteration = 1; - for (let i = 0; i < style.fill.length; i++) + for (let i = 0; i < fill.length; i++) { - stop = currentIteration / totalIterations; - gradient.addColorStop(stop, style.fill[i]); + if (fillGradientStops[i]) + { + stop = fillGradientStops[i]; + } + else + { + stop = currentIteration / totalIterations; + } + gradient.addColorStop(stop, fill[i]); currentIteration++; } } @@ -569,7 +635,7 @@ /** * Destroys this text object. * Note* Unlike a Sprite, a Text object will automatically destroy its baseTexture and texture as - * the majorety of the time the texture will not be shared with any other Sprites. + * the majority of the time the texture will not be shared with any other Sprites. * * @param {object|boolean} [options] - Options parameter. A boolean will act as if all options * have been set to that value @@ -680,7 +746,7 @@ set text(text) // eslint-disable-line require-jsdoc { - text = String(text || ' '); + text = String(text === '' || text === null || text === undefined ? ' ' : text); if (this._text === text) { diff --git a/src/core/text/TextStyle.js b/src/core/text/TextStyle.js index 3b25528..890950b 100644 --- a/src/core/text/TextStyle.js +++ b/src/core/text/TextStyle.js @@ -8,12 +8,14 @@ align: 'left', breakWords: false, dropShadow: false, + dropShadowAlpha: 1, dropShadowAngle: Math.PI / 6, dropShadowBlur: 0, dropShadowColor: '#000000', dropShadowDistance: 5, fill: 'black', fillGradientType: TEXT_GRADIENT.LINEAR_VERTICAL, + fillGradientStops: [], fontFamily: 'Arial', fontSize: 26, fontStyle: 'normal', @@ -27,6 +29,7 @@ stroke: 'black', strokeThickness: 0, textBaseline: 'alphabetic', + trim: false, wordWrap: false, wordWrapWidth: 100, }; @@ -47,6 +50,7 @@ * @param {boolean} [style.breakWords=false] - Indicates if lines can be wrapped within words, it * needs wordWrap to be set to true * @param {boolean} [style.dropShadow=false] - Set a drop shadow for the text + * @param {number} [style.dropShadowAlpha=1] - Set alpha for the drop shadow * @param {number} [style.dropShadowAngle=Math.PI/6] - Set a angle of the drop shadow * @param {number} [style.dropShadowBlur=0] - Set a shadow blur radius * @param {string} [style.dropShadowColor='#000000'] - A fill style to be used on the dropshadow e.g 'red', '#00FF00' @@ -55,8 +59,10 @@ * fillstyle that will be used on the text e.g 'red', '#00FF00'. Can be an array to create a gradient * eg ['#000000','#FFFFFF'] * {@link https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/fillStyle|MDN} - * @param {number} [style.fillGradientType=PIXI.TEXT_GRADIENT.LINEAR_VERTICAL] - If fills styles are - * supplied, this can change the type/direction of the gradient. See {@link PIXI.TEXT_GRADIENT} for possible values + * @param {number} [style.fillGradientType=PIXI.TEXT_GRADIENT.LINEAR_VERTICAL] - If fill is an array of colours + * to create a gradient, this can change the type/direction of the gradient. See {@link PIXI.TEXT_GRADIENT} + * @param {number[]} [style.fillGradientStops] - If fill is an array of colours to create a gradient, this array can set + * the stop points (numbers between 0 and 1) for the color, overriding the default behaviour of evenly spacing them. * @param {string|string[]} [style.fontFamily='Arial'] - The font family * @param {number|string} [style.fontSize=26] - The font size (as a number it converts to px, but as a string, * equivalents are '26px','20pt','160%' or '1.6em') @@ -76,6 +82,7 @@ * e.g 'blue', '#FCFF00' * @param {number} [style.strokeThickness=0] - A number that represents the thickness of the stroke. * Default is 0 (no stroke) + * @param {boolean} [style.trim=false] - Trim transparent borders * @param {string} [style.textBaseline='alphabetic'] - The baseline of the text that is rendered. * @param {boolean} [style.wordWrap=false] - Indicates if word wrap should be used * @param {number} [style.wordWrapWidth=100] - The width at which text will wrap, it needs wordWrap to be set to true @@ -152,6 +159,19 @@ } } + get dropShadowAlpha() + { + return this._dropShadowAlpha; + } + set dropShadowAlpha(dropShadowAlpha) + { + if (this._dropShadowAlpha !== dropShadowAlpha) + { + this._dropShadowAlpha = dropShadowAlpha; + this.styleID++; + } + } + get dropShadowAngle() { return this._dropShadowAngle; @@ -232,6 +252,19 @@ } } + get fillGradientStops() + { + return this._fillGradientStops; + } + set fillGradientStops(fillGradientStops) + { + if (!areArraysEqual(this._fillGradientStops,fillGradientStops)) + { + this._fillGradientStops = fillGradientStops; + this.styleID++; + } + } + get fontFamily() { return this._fontFamily; @@ -402,6 +435,19 @@ } } + get trim() + { + return this._trim; + } + set trim(trim) + { + if (this._trim !== trim) + { + this._trim = trim; + this.styleID++; + } + } + get wordWrap() { return this._wordWrap; @@ -475,3 +521,34 @@ return color; } } + +/** + * Utility function to convert hexadecimal colors to strings, and simply return the color if it's a string. + * This version can also convert array of colors + * + * @param {Array} array1 First array to compare + * @param {Array} array2 Second array to compare + * @return {boolean} Do the arrays contain the same values in the same order + */ +function areArraysEqual(array1, array2) +{ + if (!Array.isArray(array1) || !Array.isArray(array2)) + { + return false; + } + + if (array1.length !== array2.length) + { + return false; + } + + for (let i = 0; i < array1.length; ++i) + { + if (array1[i] !== array2[i]) + { + return false; + } + } + + return true; +} diff --git a/src/core/textures/BaseRenderTexture.js b/src/core/textures/BaseRenderTexture.js index 5d50146..aa727f0 100644 --- a/src/core/textures/BaseRenderTexture.js +++ b/src/core/textures/BaseRenderTexture.js @@ -1,8 +1,6 @@ import BaseTexture from './BaseTexture'; import settings from '../settings'; -const { RESOLUTION, SCALE_MODE } = settings; - /** * A BaseRenderTexture is a special texture that allows any Pixi display object to be rendered to it. * @@ -54,7 +52,7 @@ { super(null, scaleMode); - this.resolution = resolution || RESOLUTION; + this.resolution = resolution || settings.RESOLUTION; this.width = width; this.height = height; @@ -62,7 +60,7 @@ this.realWidth = this.width * this.resolution; this.realHeight = this.height * this.resolution; - this.scaleMode = scaleMode || SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; this.hasLoaded = true; /** diff --git a/src/core/textures/BaseTexture.js b/src/core/textures/BaseTexture.js index 676dca3..aeccf39 100644 --- a/src/core/textures/BaseTexture.js +++ b/src/core/textures/BaseTexture.js @@ -77,7 +77,7 @@ * @default PIXI.settings.SCALE_MODE * @see PIXI.SCALE_MODES */ - this.scaleMode = scaleMode || settings.SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; /** * Set to true once the base texture has successfully loaded. @@ -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/RenderTexture.js b/src/core/textures/RenderTexture.js index 4c5034f..b3c6297 100644 --- a/src/core/textures/RenderTexture.js +++ b/src/core/textures/RenderTexture.js @@ -54,8 +54,8 @@ /* eslint-disable prefer-rest-params, no-console */ const width = arguments[1]; const height = arguments[2]; - const scaleMode = arguments[3] || 0; - const resolution = arguments[4] || 1; + const scaleMode = arguments[3]; + const resolution = arguments[4]; // we have an old render texture.. console.warn(`Please use RenderTexture.create(${width}, ${height}) instead of the ctor directly.`); 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 ca31456..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 @@ -459,7 +497,9 @@ if (frame.x + frame.width > this.baseTexture.width || frame.y + frame.height > this.baseTexture.height) { - throw new Error(`Texture Error: frame does not fit inside the base Texture dimensions ${this}`); + throw new Error('Texture Error: frame does not fit inside the base Texture dimensions: ' + + `X: ${frame.x} + ${frame.width} > ${this.baseTexture.width} ` + + `Y: ${frame.y} + ${frame.height} > ${this.baseTexture.height}`); } // this.valid = frame && frame.width && frame.height && this.baseTexture.source && this.baseTexture.hasLoaded; @@ -520,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. @@ -528,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 2a7497b..99e585a 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)); } /** @@ -360,3 +362,43 @@ * @private */ export const BaseTextureCache = {}; + +/** + * 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/core/utils/trimCanvas.js b/src/core/utils/trimCanvas.js new file mode 100644 index 0000000..5af6de0 --- /dev/null +++ b/src/core/utils/trimCanvas.js @@ -0,0 +1,83 @@ +/** + * Trim transparent borders from a canvas + * + * @memberof PIXI + * @function trimCanvas + * @private + * @param {HTMLCanvasElement} canvas - the canvas to trim + * @returns {object} Trim data + */ +export default function trimCanvas(canvas) +{ + // https://gist.github.com/remy/784508 + + let width = canvas.width; + let height = canvas.height; + + const context = canvas.getContext('2d'); + const imageData = context.getImageData(0, 0, width, height); + const pixels = imageData.data; + const len = pixels.length; + + const bound = { + top: null, + left: null, + right: null, + bottom: null, + }; + let i; + let x; + let y; + + for (i = 0; i < len; i += 4) + { + if (pixels[i + 3] !== 0) + { + x = (i / 4) % width; + y = ~~((i / 4) / width); + + if (bound.top === null) + { + bound.top = y; + } + + if (bound.left === null) + { + bound.left = x; + } + else if (x < bound.left) + { + bound.left = x; + } + + if (bound.right === null) + { + bound.right = x + 1; + } + else if (bound.right < x) + { + bound.right = x + 1; + } + + if (bound.bottom === null) + { + bound.bottom = y; + } + else if (bound.bottom < y) + { + bound.bottom = y; + } + } + } + + width = bound.right - bound.left; + height = bound.bottom - bound.top + 1; + + const data = context.getImageData(bound.left, bound.top, width, height); + + return { + height, + width, + data, + }; +} diff --git a/src/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/extras/TilingSprite.js b/src/extras/TilingSprite.js index f90d248..fe82451 100644 --- a/src/extras/TilingSprite.js +++ b/src/extras/TilingSprite.js @@ -220,20 +220,33 @@ transform.tx * resolution, transform.ty * resolution); - // TODO - this should be rolled into the setTransform above.. - context.scale(this.tileScale.x / baseTextureResolution, this.tileScale.y / baseTextureResolution); - - context.translate(modX + (this.anchor.x * -this._width), - modY + (this.anchor.y * -this._height)); - renderer.setBlendMode(this.blendMode); // fill the pattern! context.fillStyle = this._canvasPattern; - context.fillRect(-modX, - -modY, - this._width / this.tileScale.x * baseTextureResolution, - this._height / this.tileScale.y * baseTextureResolution); + + // TODO - this should be rolled into the setTransform above.. + context.scale(this.tileScale.x / baseTextureResolution, this.tileScale.y / baseTextureResolution); + + const anchorX = this.anchor.x * -this._width; + const anchorY = this.anchor.y * -this._height; + + if (this.uvRespectAnchor) + { + context.translate(modX, modY); + + context.fillRect(-modX + anchorX, -modY + anchorY, + this._width / this.tileScale.x * baseTextureResolution, + this._height / this.tileScale.y * baseTextureResolution); + } + else + { + context.translate(modX + anchorX, modY + anchorY); + + context.fillRect(-modX, -modY, + this._width / this.tileScale.x * baseTextureResolution, + this._height / this.tileScale.y * baseTextureResolution); + } } /** diff --git a/src/extras/cacheAsBitmap.js b/src/extras/cacheAsBitmap.js index 18043a7..5dd5640 100644 --- a/src/extras/cacheAsBitmap.js +++ b/src/extras/cacheAsBitmap.js @@ -41,6 +41,9 @@ * provide a performance benefit for complex static displayObjects. * To remove simply set this property to 'false' * + * IMPORTANT GOTCHA - make sure that all your textures are preloaded BEFORE setting this property to true + * as it will take a snapshot of what is currently there. If the textures have not loaded then they will not appear. + * * @member {boolean} * @memberof PIXI.DisplayObject# */ diff --git a/src/filters/blur/BlurFilter.js b/src/filters/blur/BlurFilter.js index 340fc58..1575db9 100644 --- a/src/filters/blur/BlurFilter.js +++ b/src/filters/blur/BlurFilter.js @@ -24,10 +24,9 @@ this.blurXFilter = new BlurXFilter(strength, quality, resolution, kernelSize); this.blurYFilter = new BlurYFilter(strength, quality, resolution, kernelSize); - this.resolution = 1; this.padding = 0; - this.resolution = resolution || 1; + this.resolution = resolution || core.settings.RESOLUTION; this.quality = quality || 4; this.blur = strength || 8; } diff --git a/src/filters/blur/BlurXFilter.js b/src/filters/blur/BlurXFilter.js index def99c3..0f48889 100644 --- a/src/filters/blur/BlurXFilter.js +++ b/src/filters/blur/BlurXFilter.js @@ -31,7 +31,7 @@ fragSrc ); - this.resolution = resolution || 1; + this.resolution = resolution || core.settings.RESOLUTION; this._quality = 0; diff --git a/src/filters/blur/BlurYFilter.js b/src/filters/blur/BlurYFilter.js index 638e0f0..6d489c7 100644 --- a/src/filters/blur/BlurYFilter.js +++ b/src/filters/blur/BlurYFilter.js @@ -31,7 +31,7 @@ fragSrc ); - this.resolution = resolution || 1; + this.resolution = resolution || core.settings.RESOLUTION; this._quality = 0; diff --git a/src/filters/colormatrix/ColorMatrixFilter.js b/src/filters/colormatrix/ColorMatrixFilter.js index 8509505..679f8ec 100644 --- a/src/filters/colormatrix/ColorMatrixFilter.js +++ b/src/filters/colormatrix/ColorMatrixFilter.js @@ -75,28 +75,28 @@ out[1] = (a[0] * b[1]) + (a[1] * b[6]) + (a[2] * b[11]) + (a[3] * b[16]); out[2] = (a[0] * b[2]) + (a[1] * b[7]) + (a[2] * b[12]) + (a[3] * b[17]); out[3] = (a[0] * b[3]) + (a[1] * b[8]) + (a[2] * b[13]) + (a[3] * b[18]); - out[4] = (a[0] * b[4]) + (a[1] * b[9]) + (a[2] * b[14]) + (a[3] * b[19]); + out[4] = (a[0] * b[4]) + (a[1] * b[9]) + (a[2] * b[14]) + (a[3] * b[19]) + a[4]; // Green Channel out[5] = (a[5] * b[0]) + (a[6] * b[5]) + (a[7] * b[10]) + (a[8] * b[15]); out[6] = (a[5] * b[1]) + (a[6] * b[6]) + (a[7] * b[11]) + (a[8] * b[16]); out[7] = (a[5] * b[2]) + (a[6] * b[7]) + (a[7] * b[12]) + (a[8] * b[17]); out[8] = (a[5] * b[3]) + (a[6] * b[8]) + (a[7] * b[13]) + (a[8] * b[18]); - out[9] = (a[5] * b[4]) + (a[6] * b[9]) + (a[7] * b[14]) + (a[8] * b[19]); + out[9] = (a[5] * b[4]) + (a[6] * b[9]) + (a[7] * b[14]) + (a[8] * b[19]) + a[9]; // Blue Channel out[10] = (a[10] * b[0]) + (a[11] * b[5]) + (a[12] * b[10]) + (a[13] * b[15]); out[11] = (a[10] * b[1]) + (a[11] * b[6]) + (a[12] * b[11]) + (a[13] * b[16]); out[12] = (a[10] * b[2]) + (a[11] * b[7]) + (a[12] * b[12]) + (a[13] * b[17]); out[13] = (a[10] * b[3]) + (a[11] * b[8]) + (a[12] * b[13]) + (a[13] * b[18]); - out[14] = (a[10] * b[4]) + (a[11] * b[9]) + (a[12] * b[14]) + (a[13] * b[19]); + out[14] = (a[10] * b[4]) + (a[11] * b[9]) + (a[12] * b[14]) + (a[13] * b[19]) + a[14]; // Alpha Channel out[15] = (a[15] * b[0]) + (a[16] * b[5]) + (a[17] * b[10]) + (a[18] * b[15]); out[16] = (a[15] * b[1]) + (a[16] * b[6]) + (a[17] * b[11]) + (a[18] * b[16]); out[17] = (a[15] * b[2]) + (a[16] * b[7]) + (a[17] * b[12]) + (a[18] * b[17]); out[18] = (a[15] * b[3]) + (a[16] * b[8]) + (a[17] * b[13]) + (a[18] * b[18]); - out[19] = (a[15] * b[4]) + (a[16] * b[9]) + (a[17] * b[14]) + (a[18] * b[19]); + out[19] = (a[15] * b[4]) + (a[16] * b[9]) + (a[17] * b[14]) + (a[18] * b[19]) + a[19]; return out; } diff --git a/src/filters/colormatrix/colorMatrix.frag b/src/filters/colormatrix/colorMatrix.frag index 3aaf2b3..c73c0e9 100644 --- a/src/filters/colormatrix/colorMatrix.frag +++ b/src/filters/colormatrix/colorMatrix.frag @@ -4,32 +4,38 @@ void main(void) { - vec4 c = texture2D(uSampler, vTextureCoord); + // Un-premultiply alpha before applying the color matrix. See issue #3539. + if (c.a > 0.0) { + c.rgb /= c.a; + } + vec4 result; + result.r = (m[0] * c.r); + result.r += (m[1] * c.g); + result.r += (m[2] * c.b); + result.r += (m[3] * c.a); + result.r += m[4]; - gl_FragColor.r = (m[0] * c.r); - gl_FragColor.r += (m[1] * c.g); - gl_FragColor.r += (m[2] * c.b); - gl_FragColor.r += (m[3] * c.a); - gl_FragColor.r += m[4] * c.a; + result.g = (m[5] * c.r); + result.g += (m[6] * c.g); + result.g += (m[7] * c.b); + result.g += (m[8] * c.a); + result.g += m[9]; - gl_FragColor.g = (m[5] * c.r); - gl_FragColor.g += (m[6] * c.g); - gl_FragColor.g += (m[7] * c.b); - gl_FragColor.g += (m[8] * c.a); - gl_FragColor.g += m[9] * c.a; + result.b = (m[10] * c.r); + result.b += (m[11] * c.g); + result.b += (m[12] * c.b); + result.b += (m[13] * c.a); + result.b += m[14]; - gl_FragColor.b = (m[10] * c.r); - gl_FragColor.b += (m[11] * c.g); - gl_FragColor.b += (m[12] * c.b); - gl_FragColor.b += (m[13] * c.a); - gl_FragColor.b += m[14] * c.a; + result.a = (m[15] * c.r); + result.a += (m[16] * c.g); + result.a += (m[17] * c.b); + result.a += (m[18] * c.a); + result.a += m[19]; - gl_FragColor.a = (m[15] * c.r); - gl_FragColor.a += (m[16] * c.g); - gl_FragColor.a += (m[17] * c.b); - gl_FragColor.a += (m[18] * c.a); - gl_FragColor.a += m[19] * c.a; + // Premultiply alpha again. + result.rgb *= result.a; -// gl_FragColor = vec4(m[0]); + gl_FragColor = result; } diff --git a/package.json b/package.json index 1271fef..71ade15 100644 --- a/package.json +++ b/package.json @@ -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/scripts/release.js b/scripts/release.js index c4c8e36..156620c 100644 --- a/scripts/release.js +++ b/scripts/release.js @@ -12,7 +12,8 @@ 'dist/**/*', 'lib/**/*', 'src/**/*', - 'scripts/**/*', + 'scripts/*', + 'scripts/renders/*', 'test/**/*', '*.json', '*.md', diff --git a/scripts/renders/client.js b/scripts/renders/client.js index 16c9565..6faee34 100755 --- a/scripts/renders/client.js +++ b/scripts/renders/client.js @@ -1,6 +1,6 @@ 'use strict'; -require('../../bin/pixi'); +require('../../dist/pixi'); PIXI.utils.skipHello(); const fs = require('fs'); 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 bc730db..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. @@ -33,8 +33,11 @@ * need to call toDataUrl on the webgl context * @param {number} [options.resolution=1] - The resolution / device pixel ratio of the renderer, retina would be 2 * @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 @@ -43,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. */ @@ -73,7 +99,7 @@ */ stop() { - this.ticker.stop(); + this._ticker.stop(); } /** @@ -81,7 +107,7 @@ */ start() { - this.ticker.start(); + this._ticker.start(); } /** @@ -95,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 index 81e076b..78b17e6 100644 --- a/src/core/Shader.js +++ b/src/core/Shader.js @@ -1,9 +1,7 @@ import { GLShader } from 'pixi-gl-core'; import settings from './settings'; -const { PRECISION } = settings; - -function checkPrecision(src) +function checkPrecision(src, def) { if (src instanceof Array) { @@ -11,14 +9,14 @@ { const copy = src.slice(0); - copy.unshift(`precision ${PRECISION} float;`); + copy.unshift(`precision ${def} float;`); return copy; } } else if (src.substring(0, 9) !== 'precision') { - return `precision ${PRECISION} float;\n${src}`; + return `precision ${def} float;\n${src}`; } return src; @@ -42,6 +40,7 @@ */ constructor(gl, vertexSrc, fragmentSrc) { - super(gl, checkPrecision(vertexSrc), checkPrecision(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 3425b41..f9235e4 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -181,7 +181,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 b3792ef..17fde8d 100644 --- a/src/core/display/Container.js +++ b/src/core/display/Container.js @@ -73,13 +73,14 @@ } child.parent = this; - - // ensure a transform will be recalculated.. - this.transform._parentID = -1; - this._boundsID++; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.push(child); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(this.children.length - 1); child.emit('added', this); @@ -108,9 +109,14 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.splice(index, 0, child); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(index); child.emit('added', this); @@ -174,6 +180,7 @@ removeItems(this.children, currentIndex, 1); // remove from old position this.children.splice(index, 0, child); // add at new position + this.onChildrenChange(index); } @@ -220,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! @@ -244,9 +252,14 @@ { const child = this.getChildAt(index); + // ensure child transform will be recalculated.. child.parent = null; + child.transform._parentID = -1; removeItems(this.children, index, 1); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(index); child.emit('removed', this); @@ -275,8 +288,14 @@ for (let i = 0; i < removed.length; ++i) { removed[i].parent = null; + if (removed[i].transform) + { + removed[i].transform._parentID = -1; + } } + this._boundsID++; + this.onChildrenChange(beginIndex); for (let i = 0; i < removed.length; ++i) diff --git a/src/core/display/DisplayObject.js b/src/core/display/DisplayObject.js index 3affe36..4e6c77d 100644 --- a/src/core/display/DisplayObject.js +++ b/src/core/display/DisplayObject.js @@ -109,7 +109,7 @@ /** * The original, cached mask of the object * - * @member {PIXI.Rectangle} + * @member {PIXI.Graphics|PIXI.Sprite} * @private */ this._mask = null; diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index 95f2af4..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 ); @@ -1065,10 +1061,15 @@ canvasRenderer = new CanvasRenderer(); } - tempMatrix.tx = -bounds.x; - tempMatrix.ty = -bounds.y; + this.transform.updateLocalTransform(); + this.transform.localTransform.copy(tempMatrix); - canvasRenderer.render(this, canvasBuffer, false, tempMatrix); + tempMatrix.invert(); + + tempMatrix.tx -= bounds.x; + tempMatrix.ty -= bounds.y; + + canvasRenderer.render(this, canvasBuffer, true, tempMatrix); const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode); 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 e67680f..867dde2 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/buildCircle.js b/src/core/graphics/webgl/utils/buildCircle.js index 0400d13..abf5d02 100644 --- a/src/core/graphics/webgl/utils/buildCircle.js +++ b/src/core/graphics/webgl/utils/buildCircle.js @@ -33,6 +33,11 @@ height = circleData.height; } + if (width === 0 || height === 0) + { + return; + } + const totalSegs = Math.floor(30 * Math.sqrt(circleData.radius)) || Math.floor(15 * Math.sqrt(circleData.width + circleData.height)); diff --git a/src/core/graphics/webgl/utils/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 90155f9..f03fe15 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -30,6 +30,7 @@ export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; export { default as CanvasGraphicsRenderer } from './graphics/canvas/CanvasGraphicsRenderer'; +export { default as Spritesheet } from './textures/Spritesheet'; export { default as Texture } from './textures/Texture'; export { default as BaseTexture } from './textures/BaseTexture'; export { default as RenderTexture } from './textures/RenderTexture'; diff --git a/src/core/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 ccce680..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); } /** @@ -162,6 +162,9 @@ if (transform) { transform.copy(tempWt); + + // lets not forget to flag the parent transform as dirty... + this._tempDisplayObjectParent.transform._worldID = -1; } else { @@ -169,6 +172,7 @@ } displayObject.parent = this._tempDisplayObjectParent; + displayObject.updateTransform(); displayObject.parent = cacheParent; // displayObject.hitArea = //TODO add a temp hit area @@ -279,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/canvas/utils/CanvasRenderTarget.js b/src/core/renderers/canvas/utils/CanvasRenderTarget.js index 3de0aef..b409891 100644 --- a/src/core/renderers/canvas/utils/CanvasRenderTarget.js +++ b/src/core/renderers/canvas/utils/CanvasRenderTarget.js @@ -1,5 +1,4 @@ import settings from '../../../settings'; -const { RESOLUTION } = settings; /** * Creates a Canvas element of the given size. @@ -30,7 +29,7 @@ */ this.context = this.canvas.getContext('2d'); - this.resolution = resolution || RESOLUTION; + this.resolution = resolution || settings.RESOLUTION; this.resize(width, height); } 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 ce0e1c4..788ed6c 100644 --- a/src/core/renderers/webgl/WebGLRenderer.js +++ b/src/core/renderers/webgl/WebGLRenderer.js @@ -30,8 +30,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 @@ -49,10 +49,19 @@ * enable this if you need to call toDataUrl on the webgl context. * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when * rendering, stopping pixel interpolation. + * @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; + + if (this.legacy) + { + glCore.VertexArrayObject.FORCE_NATIVE = true; + } /** * The type of this renderer as a standardised const @@ -229,7 +238,7 @@ this.emit('context', gl); // setup the width/height properties and gl viewport - this.resize(this.width, this.height); + this.resize(this.screen.width, this.screen.height); } /** @@ -323,16 +332,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) { @@ -376,6 +385,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 @@ -440,9 +469,10 @@ * Changes the current shader to the one given in parameter * * @param {PIXI.Shader} shader - the new shader + * @param {boolean} [autoProject=true] - Whether automatically set the projection matrix * @return {PIXI.WebGLRenderer} Returns itself. */ - bindShader(shader) + bindShader(shader, autoProject) { // TODO cache if (this._activeShader !== shader) @@ -450,8 +480,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 bbab3b3..944f41d 100644 --- a/src/core/renderers/webgl/filters/Filter.js +++ b/src/core/renderers/webgl/filters/Filter.js @@ -1,6 +1,7 @@ import extractUniformsFromSrc from './extractUniformsFromSrc'; import { uid } from '../../../utils'; import { BLEND_MODES } from '../../../const'; +import settings from '../../../settings'; const SOURCE_KEY_MAP = {}; @@ -10,7 +11,7 @@ * @memberof PIXI * @extends PIXI.Shader */ -class Filter +export default class Filter { /** * @param {string} [vertexSrc] - The source of the vertex shader. @@ -35,8 +36,6 @@ this.blendMode = BLEND_MODES.NORMAL; - // pull out the vertex and shader uniforms if they are not specified.. - // currently this does not extract structs only default types this.uniformData = uniforms || extractUniformsFromSrc(this.vertexSrc, this.fragmentSrc, 'projectionMatrix|uSampler'); /** @@ -80,7 +79,7 @@ * * @member {number} */ - this.resolution = 1; + this.resolution = settings.RESOLUTION; /** * If enabled is true the filter is applied, if false it will not. @@ -97,8 +96,11 @@ * @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 { // --- // // this.uniforms.filterMatrix = filterManager.calculateSpriteMatrix(tempMatrix, window.panda ); @@ -170,5 +172,3 @@ ].join('\n'); } } - -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 dc7072a..6c7422d 100644 --- a/src/core/renderers/webgl/managers/FilterManager.js +++ b/src/core/renderers/webgl/managers/FilterManager.js @@ -154,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 @@ -176,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; @@ -184,7 +184,7 @@ flop = t; } - filters[i].apply(this, flip, lastState.renderTarget, true); + filters[i].apply(this, flip, lastState.renderTarget, false, currentState); this.freePotRenderTarget(flip); this.freePotRenderTarget(flop); @@ -291,9 +291,13 @@ let textureCount = 1; let currentState; - if (shader.uniforms.data.filterArea) + // 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; @@ -306,9 +310,9 @@ // use this to clamp displaced texture coords so they belong to filterArea // see displacementFilter fragment shader for an example - if (shader.uniforms.data.filterClamp) + if (shader.uniforms.filterClamp) { - currentState = this.filterData.stack[this.filterData.index]; + currentState = currentState || this.filterData.stack[this.filterData.index]; const filterClamp = shader.uniforms.filterClamp; @@ -485,7 +489,7 @@ */ destroy() { - this.shaderCache = []; + this.shaderCache = {}; this.emptyPool(); } diff --git a/src/core/renderers/webgl/utils/RenderTarget.js b/src/core/renderers/webgl/utils/RenderTarget.js index afe51ab..6d2467f 100644 --- a/src/core/renderers/webgl/utils/RenderTarget.js +++ b/src/core/renderers/webgl/utils/RenderTarget.js @@ -124,7 +124,7 @@ * @default PIXI.settings.SCALE_MODE * @see PIXI.SCALE_MODES */ - this.scaleMode = scaleMode || settings.SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; /** * Whether this object is the root element or not 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 36ea5bb..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; @@ -346,8 +346,8 @@ /** * Gets the local bounds of the sprite object. * - * @param {Rectangle} rect - The output rectangle. - * @return {Rectangle} The bounds. + * @param {PIXI.Rectangle} rect - The output rectangle. + * @return {PIXI.Rectangle} The bounds. */ getLocalBounds(rect) { @@ -440,7 +440,7 @@ * * @static * @param {number|string|PIXI.BaseTexture|HTMLCanvasElement|HTMLVideoElement} source Source to create texture from - * @return {PIXI.Texture} 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 e77b39a..90b08c0 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++) @@ -103,11 +102,18 @@ { const gl = this.renderer.gl; - // step 1: first check max textures the GPU can handle. - this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); + if (this.renderer.legacy) + { + this.MAX_TEXTURES = 1; + } + else + { + // step 1: first check max textures the GPU can handle. + this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); - // step 2: check the maximum number of if statements the shader can have too.. - this.MAX_TEXTURES = checkMaxIfStatmentsInShader(this.MAX_TEXTURES, gl); + // step 2: check the maximum number of if statements the shader can have too.. + this.MAX_TEXTURES = checkMaxIfStatmentsInShader(this.MAX_TEXTURES, gl); + } const shader = this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); @@ -130,8 +136,12 @@ .addIndex(this.indexBuffer) .addAttribute(this.vertexBuffers[i], shader.attributes.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) .addAttribute(this.vertexBuffers[i], shader.attributes.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) - .addAttribute(this.vertexBuffers[i], shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4) - .addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + .addAttribute(this.vertexBuffers[i], shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); + + if (shader.attributes.aTextureId) + { + this.vaos[i].addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + } /* eslint-enable max-len */ } @@ -378,8 +388,12 @@ .addIndex(this.indexBuffer) .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) - .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4) - .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); + + if (this.shader.attributes.aTextureId) + { + this.vaos[this.vertexCount].addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + } /* eslint-enable max-len */ } diff --git a/src/core/text/Text.js b/src/core/text/Text.js index 013625a..cec8c32 100644 --- a/src/core/text/Text.js +++ b/src/core/text/Text.js @@ -6,6 +6,7 @@ import { TEXT_GRADIENT } from '../const'; import settings from '../settings'; import TextStyle from './TextStyle'; +import trimCanvas from '../utils/trimCanvas'; const defaultDestroyOptions = { texture: true, @@ -57,7 +58,7 @@ /** * The canvas 2d context that everything is drawn with - * @member {HTMLCanvasElement} + * @member {CanvasRenderingContext2D} */ this.context = this.canvas.getContext('2d'); @@ -189,10 +190,12 @@ if (style.dropShadow) { + this.context.shadowBlur = style.dropShadowBlur; + this.context.globalAlpha = style.dropShadowAlpha; + if (style.dropShadowBlur > 0) { this.context.shadowColor = style.dropShadowColor; - this.context.shadowBlur = style.dropShadowBlur; } else { @@ -237,6 +240,10 @@ } } + // reset the shadow blur and alpha that was set by the drop shadow, for the regular text + this.context.shadowBlur = 0; + this.context.globalAlpha = 1; + // set canvas text styles this.context.fillStyle = this._generateFillStyle(style, lines); @@ -326,6 +333,15 @@ */ updateTexture() { + if (this._style.trim) + { + const trimmed = trimCanvas(this.canvas); + + this.canvas.width = trimmed.width; + this.canvas.height = trimmed.height; + this.context.putImageData(trimmed.data, 0, 0); + } + const texture = this._texture; const style = this._style; @@ -475,6 +491,19 @@ } /** + * Gets the local bounds of the text object. + * + * @param {Rectangle} rect - The output rectangle. + * @return {Rectangle} The bounds. + */ + getLocalBounds(rect) + { + this.updateText(true); + + return super.getLocalBounds.call(this, rect); + } + + /** * calculates the bounds of the Text as a rectangle. The bounds calculation takes the worldTransform into account. */ _calculateBounds() @@ -525,6 +554,29 @@ const width = this.canvas.width / this.resolution; const height = this.canvas.height / this.resolution; + // make a copy of the style settings, so we can manipulate them later + const fill = style.fill.slice(); + const fillGradientStops = style.fillGradientStops.slice(); + + // wanting to evenly distribute the fills. So an array of 4 colours should give fills of 0.25, 0.5 and 0.75 + if (!fillGradientStops.length) + { + const lengthPlus1 = fill.length + 1; + + for (let i = 1; i < lengthPlus1; ++i) + { + fillGradientStops.push(i / lengthPlus1); + } + } + + // stop the bleeding of the last gradient on the line above to the top gradient of the this line + // by hard defining the first gradient colour at point 0, and last gradient colour at point 1 + fill.unshift(style.fill[0]); + fillGradientStops.unshift(0); + + fill.push(style.fill[style.fill.length - 1]); + fillGradientStops.push(1); + if (style.fillGradientType === TEXT_GRADIENT.LINEAR_VERTICAL) { // start the gradient at the top center of the canvas, and end at the bottom middle of the canvas @@ -532,15 +584,22 @@ // we need to repeat the gradient so that each individual line of text has the same vertical gradient effect // ['#FF0000', '#00FF00', '#0000FF'] over 2 lines would create stops at 0.125, 0.25, 0.375, 0.625, 0.75, 0.875 - totalIterations = (style.fill.length + 1) * lines.length; + totalIterations = (fill.length + 1) * lines.length; currentIteration = 0; for (let i = 0; i < lines.length; i++) { currentIteration += 1; - for (let j = 0; j < style.fill.length; j++) + for (let j = 0; j < fill.length; j++) { - stop = (currentIteration / totalIterations); - gradient.addColorStop(stop, style.fill[j]); + if (fillGradientStops[j]) + { + stop = (fillGradientStops[j] / lines.length) + (i / lines.length); + } + else + { + stop = currentIteration / totalIterations; + } + gradient.addColorStop(stop, fill[j]); currentIteration++; } } @@ -552,13 +611,20 @@ // can just evenly space out the gradients in this case, as multiple lines makes no difference // to an even left to right gradient - totalIterations = style.fill.length + 1; + totalIterations = fill.length + 1; currentIteration = 1; - for (let i = 0; i < style.fill.length; i++) + for (let i = 0; i < fill.length; i++) { - stop = currentIteration / totalIterations; - gradient.addColorStop(stop, style.fill[i]); + if (fillGradientStops[i]) + { + stop = fillGradientStops[i]; + } + else + { + stop = currentIteration / totalIterations; + } + gradient.addColorStop(stop, fill[i]); currentIteration++; } } @@ -569,7 +635,7 @@ /** * Destroys this text object. * Note* Unlike a Sprite, a Text object will automatically destroy its baseTexture and texture as - * the majorety of the time the texture will not be shared with any other Sprites. + * the majority of the time the texture will not be shared with any other Sprites. * * @param {object|boolean} [options] - Options parameter. A boolean will act as if all options * have been set to that value @@ -680,7 +746,7 @@ set text(text) // eslint-disable-line require-jsdoc { - text = String(text || ' '); + text = String(text === '' || text === null || text === undefined ? ' ' : text); if (this._text === text) { diff --git a/src/core/text/TextStyle.js b/src/core/text/TextStyle.js index 3b25528..890950b 100644 --- a/src/core/text/TextStyle.js +++ b/src/core/text/TextStyle.js @@ -8,12 +8,14 @@ align: 'left', breakWords: false, dropShadow: false, + dropShadowAlpha: 1, dropShadowAngle: Math.PI / 6, dropShadowBlur: 0, dropShadowColor: '#000000', dropShadowDistance: 5, fill: 'black', fillGradientType: TEXT_GRADIENT.LINEAR_VERTICAL, + fillGradientStops: [], fontFamily: 'Arial', fontSize: 26, fontStyle: 'normal', @@ -27,6 +29,7 @@ stroke: 'black', strokeThickness: 0, textBaseline: 'alphabetic', + trim: false, wordWrap: false, wordWrapWidth: 100, }; @@ -47,6 +50,7 @@ * @param {boolean} [style.breakWords=false] - Indicates if lines can be wrapped within words, it * needs wordWrap to be set to true * @param {boolean} [style.dropShadow=false] - Set a drop shadow for the text + * @param {number} [style.dropShadowAlpha=1] - Set alpha for the drop shadow * @param {number} [style.dropShadowAngle=Math.PI/6] - Set a angle of the drop shadow * @param {number} [style.dropShadowBlur=0] - Set a shadow blur radius * @param {string} [style.dropShadowColor='#000000'] - A fill style to be used on the dropshadow e.g 'red', '#00FF00' @@ -55,8 +59,10 @@ * fillstyle that will be used on the text e.g 'red', '#00FF00'. Can be an array to create a gradient * eg ['#000000','#FFFFFF'] * {@link https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/fillStyle|MDN} - * @param {number} [style.fillGradientType=PIXI.TEXT_GRADIENT.LINEAR_VERTICAL] - If fills styles are - * supplied, this can change the type/direction of the gradient. See {@link PIXI.TEXT_GRADIENT} for possible values + * @param {number} [style.fillGradientType=PIXI.TEXT_GRADIENT.LINEAR_VERTICAL] - If fill is an array of colours + * to create a gradient, this can change the type/direction of the gradient. See {@link PIXI.TEXT_GRADIENT} + * @param {number[]} [style.fillGradientStops] - If fill is an array of colours to create a gradient, this array can set + * the stop points (numbers between 0 and 1) for the color, overriding the default behaviour of evenly spacing them. * @param {string|string[]} [style.fontFamily='Arial'] - The font family * @param {number|string} [style.fontSize=26] - The font size (as a number it converts to px, but as a string, * equivalents are '26px','20pt','160%' or '1.6em') @@ -76,6 +82,7 @@ * e.g 'blue', '#FCFF00' * @param {number} [style.strokeThickness=0] - A number that represents the thickness of the stroke. * Default is 0 (no stroke) + * @param {boolean} [style.trim=false] - Trim transparent borders * @param {string} [style.textBaseline='alphabetic'] - The baseline of the text that is rendered. * @param {boolean} [style.wordWrap=false] - Indicates if word wrap should be used * @param {number} [style.wordWrapWidth=100] - The width at which text will wrap, it needs wordWrap to be set to true @@ -152,6 +159,19 @@ } } + get dropShadowAlpha() + { + return this._dropShadowAlpha; + } + set dropShadowAlpha(dropShadowAlpha) + { + if (this._dropShadowAlpha !== dropShadowAlpha) + { + this._dropShadowAlpha = dropShadowAlpha; + this.styleID++; + } + } + get dropShadowAngle() { return this._dropShadowAngle; @@ -232,6 +252,19 @@ } } + get fillGradientStops() + { + return this._fillGradientStops; + } + set fillGradientStops(fillGradientStops) + { + if (!areArraysEqual(this._fillGradientStops,fillGradientStops)) + { + this._fillGradientStops = fillGradientStops; + this.styleID++; + } + } + get fontFamily() { return this._fontFamily; @@ -402,6 +435,19 @@ } } + get trim() + { + return this._trim; + } + set trim(trim) + { + if (this._trim !== trim) + { + this._trim = trim; + this.styleID++; + } + } + get wordWrap() { return this._wordWrap; @@ -475,3 +521,34 @@ return color; } } + +/** + * Utility function to convert hexadecimal colors to strings, and simply return the color if it's a string. + * This version can also convert array of colors + * + * @param {Array} array1 First array to compare + * @param {Array} array2 Second array to compare + * @return {boolean} Do the arrays contain the same values in the same order + */ +function areArraysEqual(array1, array2) +{ + if (!Array.isArray(array1) || !Array.isArray(array2)) + { + return false; + } + + if (array1.length !== array2.length) + { + return false; + } + + for (let i = 0; i < array1.length; ++i) + { + if (array1[i] !== array2[i]) + { + return false; + } + } + + return true; +} diff --git a/src/core/textures/BaseRenderTexture.js b/src/core/textures/BaseRenderTexture.js index 5d50146..aa727f0 100644 --- a/src/core/textures/BaseRenderTexture.js +++ b/src/core/textures/BaseRenderTexture.js @@ -1,8 +1,6 @@ import BaseTexture from './BaseTexture'; import settings from '../settings'; -const { RESOLUTION, SCALE_MODE } = settings; - /** * A BaseRenderTexture is a special texture that allows any Pixi display object to be rendered to it. * @@ -54,7 +52,7 @@ { super(null, scaleMode); - this.resolution = resolution || RESOLUTION; + this.resolution = resolution || settings.RESOLUTION; this.width = width; this.height = height; @@ -62,7 +60,7 @@ this.realWidth = this.width * this.resolution; this.realHeight = this.height * this.resolution; - this.scaleMode = scaleMode || SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; this.hasLoaded = true; /** diff --git a/src/core/textures/BaseTexture.js b/src/core/textures/BaseTexture.js index 676dca3..aeccf39 100644 --- a/src/core/textures/BaseTexture.js +++ b/src/core/textures/BaseTexture.js @@ -77,7 +77,7 @@ * @default PIXI.settings.SCALE_MODE * @see PIXI.SCALE_MODES */ - this.scaleMode = scaleMode || settings.SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; /** * Set to true once the base texture has successfully loaded. @@ -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/RenderTexture.js b/src/core/textures/RenderTexture.js index 4c5034f..b3c6297 100644 --- a/src/core/textures/RenderTexture.js +++ b/src/core/textures/RenderTexture.js @@ -54,8 +54,8 @@ /* eslint-disable prefer-rest-params, no-console */ const width = arguments[1]; const height = arguments[2]; - const scaleMode = arguments[3] || 0; - const resolution = arguments[4] || 1; + const scaleMode = arguments[3]; + const resolution = arguments[4]; // we have an old render texture.. console.warn(`Please use RenderTexture.create(${width}, ${height}) instead of the ctor directly.`); 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 ca31456..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 @@ -459,7 +497,9 @@ if (frame.x + frame.width > this.baseTexture.width || frame.y + frame.height > this.baseTexture.height) { - throw new Error(`Texture Error: frame does not fit inside the base Texture dimensions ${this}`); + throw new Error('Texture Error: frame does not fit inside the base Texture dimensions: ' + + `X: ${frame.x} + ${frame.width} > ${this.baseTexture.width} ` + + `Y: ${frame.y} + ${frame.height} > ${this.baseTexture.height}`); } // this.valid = frame && frame.width && frame.height && this.baseTexture.source && this.baseTexture.hasLoaded; @@ -520,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. @@ -528,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 2a7497b..99e585a 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)); } /** @@ -360,3 +362,43 @@ * @private */ export const BaseTextureCache = {}; + +/** + * 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/core/utils/trimCanvas.js b/src/core/utils/trimCanvas.js new file mode 100644 index 0000000..5af6de0 --- /dev/null +++ b/src/core/utils/trimCanvas.js @@ -0,0 +1,83 @@ +/** + * Trim transparent borders from a canvas + * + * @memberof PIXI + * @function trimCanvas + * @private + * @param {HTMLCanvasElement} canvas - the canvas to trim + * @returns {object} Trim data + */ +export default function trimCanvas(canvas) +{ + // https://gist.github.com/remy/784508 + + let width = canvas.width; + let height = canvas.height; + + const context = canvas.getContext('2d'); + const imageData = context.getImageData(0, 0, width, height); + const pixels = imageData.data; + const len = pixels.length; + + const bound = { + top: null, + left: null, + right: null, + bottom: null, + }; + let i; + let x; + let y; + + for (i = 0; i < len; i += 4) + { + if (pixels[i + 3] !== 0) + { + x = (i / 4) % width; + y = ~~((i / 4) / width); + + if (bound.top === null) + { + bound.top = y; + } + + if (bound.left === null) + { + bound.left = x; + } + else if (x < bound.left) + { + bound.left = x; + } + + if (bound.right === null) + { + bound.right = x + 1; + } + else if (bound.right < x) + { + bound.right = x + 1; + } + + if (bound.bottom === null) + { + bound.bottom = y; + } + else if (bound.bottom < y) + { + bound.bottom = y; + } + } + } + + width = bound.right - bound.left; + height = bound.bottom - bound.top + 1; + + const data = context.getImageData(bound.left, bound.top, width, height); + + return { + height, + width, + data, + }; +} diff --git a/src/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/extras/TilingSprite.js b/src/extras/TilingSprite.js index f90d248..fe82451 100644 --- a/src/extras/TilingSprite.js +++ b/src/extras/TilingSprite.js @@ -220,20 +220,33 @@ transform.tx * resolution, transform.ty * resolution); - // TODO - this should be rolled into the setTransform above.. - context.scale(this.tileScale.x / baseTextureResolution, this.tileScale.y / baseTextureResolution); - - context.translate(modX + (this.anchor.x * -this._width), - modY + (this.anchor.y * -this._height)); - renderer.setBlendMode(this.blendMode); // fill the pattern! context.fillStyle = this._canvasPattern; - context.fillRect(-modX, - -modY, - this._width / this.tileScale.x * baseTextureResolution, - this._height / this.tileScale.y * baseTextureResolution); + + // TODO - this should be rolled into the setTransform above.. + context.scale(this.tileScale.x / baseTextureResolution, this.tileScale.y / baseTextureResolution); + + const anchorX = this.anchor.x * -this._width; + const anchorY = this.anchor.y * -this._height; + + if (this.uvRespectAnchor) + { + context.translate(modX, modY); + + context.fillRect(-modX + anchorX, -modY + anchorY, + this._width / this.tileScale.x * baseTextureResolution, + this._height / this.tileScale.y * baseTextureResolution); + } + else + { + context.translate(modX + anchorX, modY + anchorY); + + context.fillRect(-modX, -modY, + this._width / this.tileScale.x * baseTextureResolution, + this._height / this.tileScale.y * baseTextureResolution); + } } /** diff --git a/src/extras/cacheAsBitmap.js b/src/extras/cacheAsBitmap.js index 18043a7..5dd5640 100644 --- a/src/extras/cacheAsBitmap.js +++ b/src/extras/cacheAsBitmap.js @@ -41,6 +41,9 @@ * provide a performance benefit for complex static displayObjects. * To remove simply set this property to 'false' * + * IMPORTANT GOTCHA - make sure that all your textures are preloaded BEFORE setting this property to true + * as it will take a snapshot of what is currently there. If the textures have not loaded then they will not appear. + * * @member {boolean} * @memberof PIXI.DisplayObject# */ diff --git a/src/filters/blur/BlurFilter.js b/src/filters/blur/BlurFilter.js index 340fc58..1575db9 100644 --- a/src/filters/blur/BlurFilter.js +++ b/src/filters/blur/BlurFilter.js @@ -24,10 +24,9 @@ this.blurXFilter = new BlurXFilter(strength, quality, resolution, kernelSize); this.blurYFilter = new BlurYFilter(strength, quality, resolution, kernelSize); - this.resolution = 1; this.padding = 0; - this.resolution = resolution || 1; + this.resolution = resolution || core.settings.RESOLUTION; this.quality = quality || 4; this.blur = strength || 8; } diff --git a/src/filters/blur/BlurXFilter.js b/src/filters/blur/BlurXFilter.js index def99c3..0f48889 100644 --- a/src/filters/blur/BlurXFilter.js +++ b/src/filters/blur/BlurXFilter.js @@ -31,7 +31,7 @@ fragSrc ); - this.resolution = resolution || 1; + this.resolution = resolution || core.settings.RESOLUTION; this._quality = 0; diff --git a/src/filters/blur/BlurYFilter.js b/src/filters/blur/BlurYFilter.js index 638e0f0..6d489c7 100644 --- a/src/filters/blur/BlurYFilter.js +++ b/src/filters/blur/BlurYFilter.js @@ -31,7 +31,7 @@ fragSrc ); - this.resolution = resolution || 1; + this.resolution = resolution || core.settings.RESOLUTION; this._quality = 0; diff --git a/src/filters/colormatrix/ColorMatrixFilter.js b/src/filters/colormatrix/ColorMatrixFilter.js index 8509505..679f8ec 100644 --- a/src/filters/colormatrix/ColorMatrixFilter.js +++ b/src/filters/colormatrix/ColorMatrixFilter.js @@ -75,28 +75,28 @@ out[1] = (a[0] * b[1]) + (a[1] * b[6]) + (a[2] * b[11]) + (a[3] * b[16]); out[2] = (a[0] * b[2]) + (a[1] * b[7]) + (a[2] * b[12]) + (a[3] * b[17]); out[3] = (a[0] * b[3]) + (a[1] * b[8]) + (a[2] * b[13]) + (a[3] * b[18]); - out[4] = (a[0] * b[4]) + (a[1] * b[9]) + (a[2] * b[14]) + (a[3] * b[19]); + out[4] = (a[0] * b[4]) + (a[1] * b[9]) + (a[2] * b[14]) + (a[3] * b[19]) + a[4]; // Green Channel out[5] = (a[5] * b[0]) + (a[6] * b[5]) + (a[7] * b[10]) + (a[8] * b[15]); out[6] = (a[5] * b[1]) + (a[6] * b[6]) + (a[7] * b[11]) + (a[8] * b[16]); out[7] = (a[5] * b[2]) + (a[6] * b[7]) + (a[7] * b[12]) + (a[8] * b[17]); out[8] = (a[5] * b[3]) + (a[6] * b[8]) + (a[7] * b[13]) + (a[8] * b[18]); - out[9] = (a[5] * b[4]) + (a[6] * b[9]) + (a[7] * b[14]) + (a[8] * b[19]); + out[9] = (a[5] * b[4]) + (a[6] * b[9]) + (a[7] * b[14]) + (a[8] * b[19]) + a[9]; // Blue Channel out[10] = (a[10] * b[0]) + (a[11] * b[5]) + (a[12] * b[10]) + (a[13] * b[15]); out[11] = (a[10] * b[1]) + (a[11] * b[6]) + (a[12] * b[11]) + (a[13] * b[16]); out[12] = (a[10] * b[2]) + (a[11] * b[7]) + (a[12] * b[12]) + (a[13] * b[17]); out[13] = (a[10] * b[3]) + (a[11] * b[8]) + (a[12] * b[13]) + (a[13] * b[18]); - out[14] = (a[10] * b[4]) + (a[11] * b[9]) + (a[12] * b[14]) + (a[13] * b[19]); + out[14] = (a[10] * b[4]) + (a[11] * b[9]) + (a[12] * b[14]) + (a[13] * b[19]) + a[14]; // Alpha Channel out[15] = (a[15] * b[0]) + (a[16] * b[5]) + (a[17] * b[10]) + (a[18] * b[15]); out[16] = (a[15] * b[1]) + (a[16] * b[6]) + (a[17] * b[11]) + (a[18] * b[16]); out[17] = (a[15] * b[2]) + (a[16] * b[7]) + (a[17] * b[12]) + (a[18] * b[17]); out[18] = (a[15] * b[3]) + (a[16] * b[8]) + (a[17] * b[13]) + (a[18] * b[18]); - out[19] = (a[15] * b[4]) + (a[16] * b[9]) + (a[17] * b[14]) + (a[18] * b[19]); + out[19] = (a[15] * b[4]) + (a[16] * b[9]) + (a[17] * b[14]) + (a[18] * b[19]) + a[19]; return out; } diff --git a/src/filters/colormatrix/colorMatrix.frag b/src/filters/colormatrix/colorMatrix.frag index 3aaf2b3..c73c0e9 100644 --- a/src/filters/colormatrix/colorMatrix.frag +++ b/src/filters/colormatrix/colorMatrix.frag @@ -4,32 +4,38 @@ void main(void) { - vec4 c = texture2D(uSampler, vTextureCoord); + // Un-premultiply alpha before applying the color matrix. See issue #3539. + if (c.a > 0.0) { + c.rgb /= c.a; + } + vec4 result; + result.r = (m[0] * c.r); + result.r += (m[1] * c.g); + result.r += (m[2] * c.b); + result.r += (m[3] * c.a); + result.r += m[4]; - gl_FragColor.r = (m[0] * c.r); - gl_FragColor.r += (m[1] * c.g); - gl_FragColor.r += (m[2] * c.b); - gl_FragColor.r += (m[3] * c.a); - gl_FragColor.r += m[4] * c.a; + result.g = (m[5] * c.r); + result.g += (m[6] * c.g); + result.g += (m[7] * c.b); + result.g += (m[8] * c.a); + result.g += m[9]; - gl_FragColor.g = (m[5] * c.r); - gl_FragColor.g += (m[6] * c.g); - gl_FragColor.g += (m[7] * c.b); - gl_FragColor.g += (m[8] * c.a); - gl_FragColor.g += m[9] * c.a; + result.b = (m[10] * c.r); + result.b += (m[11] * c.g); + result.b += (m[12] * c.b); + result.b += (m[13] * c.a); + result.b += m[14]; - gl_FragColor.b = (m[10] * c.r); - gl_FragColor.b += (m[11] * c.g); - gl_FragColor.b += (m[12] * c.b); - gl_FragColor.b += (m[13] * c.a); - gl_FragColor.b += m[14] * c.a; + result.a = (m[15] * c.r); + result.a += (m[16] * c.g); + result.a += (m[17] * c.b); + result.a += (m[18] * c.a); + result.a += m[19]; - gl_FragColor.a = (m[15] * c.r); - gl_FragColor.a += (m[16] * c.g); - gl_FragColor.a += (m[17] * c.b); - gl_FragColor.a += (m[18] * c.a); - gl_FragColor.a += m[19] * c.a; + // Premultiply alpha again. + result.rgb *= result.a; -// gl_FragColor = vec4(m[0]); + gl_FragColor = result; } diff --git a/src/filters/displacement/DisplacementFilter.js b/src/filters/displacement/DisplacementFilter.js index 112de0b..d5355c3 100644 --- a/src/filters/displacement/DisplacementFilter.js +++ b/src/filters/displacement/DisplacementFilter.js @@ -36,7 +36,7 @@ this.maskMatrix = maskMatrix; this.uniforms.mapSampler = sprite.texture; - this.uniforms.filterMatrix = maskMatrix.toArray(true); + this.uniforms.filterMatrix = maskMatrix; this.uniforms.scale = { x: 1, y: 1 }; if (scale === null || scale === undefined) diff --git a/package.json b/package.json index 1271fef..71ade15 100644 --- a/package.json +++ b/package.json @@ -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/scripts/release.js b/scripts/release.js index c4c8e36..156620c 100644 --- a/scripts/release.js +++ b/scripts/release.js @@ -12,7 +12,8 @@ 'dist/**/*', 'lib/**/*', 'src/**/*', - 'scripts/**/*', + 'scripts/*', + 'scripts/renders/*', 'test/**/*', '*.json', '*.md', diff --git a/scripts/renders/client.js b/scripts/renders/client.js index 16c9565..6faee34 100755 --- a/scripts/renders/client.js +++ b/scripts/renders/client.js @@ -1,6 +1,6 @@ 'use strict'; -require('../../bin/pixi'); +require('../../dist/pixi'); PIXI.utils.skipHello(); const fs = require('fs'); 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 bc730db..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. @@ -33,8 +33,11 @@ * need to call toDataUrl on the webgl context * @param {number} [options.resolution=1] - The resolution / device pixel ratio of the renderer, retina would be 2 * @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 @@ -43,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. */ @@ -73,7 +99,7 @@ */ stop() { - this.ticker.stop(); + this._ticker.stop(); } /** @@ -81,7 +107,7 @@ */ start() { - this.ticker.start(); + this._ticker.start(); } /** @@ -95,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 index 81e076b..78b17e6 100644 --- a/src/core/Shader.js +++ b/src/core/Shader.js @@ -1,9 +1,7 @@ import { GLShader } from 'pixi-gl-core'; import settings from './settings'; -const { PRECISION } = settings; - -function checkPrecision(src) +function checkPrecision(src, def) { if (src instanceof Array) { @@ -11,14 +9,14 @@ { const copy = src.slice(0); - copy.unshift(`precision ${PRECISION} float;`); + copy.unshift(`precision ${def} float;`); return copy; } } else if (src.substring(0, 9) !== 'precision') { - return `precision ${PRECISION} float;\n${src}`; + return `precision ${def} float;\n${src}`; } return src; @@ -42,6 +40,7 @@ */ constructor(gl, vertexSrc, fragmentSrc) { - super(gl, checkPrecision(vertexSrc), checkPrecision(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 3425b41..f9235e4 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -181,7 +181,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 b3792ef..17fde8d 100644 --- a/src/core/display/Container.js +++ b/src/core/display/Container.js @@ -73,13 +73,14 @@ } child.parent = this; - - // ensure a transform will be recalculated.. - this.transform._parentID = -1; - this._boundsID++; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.push(child); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(this.children.length - 1); child.emit('added', this); @@ -108,9 +109,14 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.splice(index, 0, child); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(index); child.emit('added', this); @@ -174,6 +180,7 @@ removeItems(this.children, currentIndex, 1); // remove from old position this.children.splice(index, 0, child); // add at new position + this.onChildrenChange(index); } @@ -220,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! @@ -244,9 +252,14 @@ { const child = this.getChildAt(index); + // ensure child transform will be recalculated.. child.parent = null; + child.transform._parentID = -1; removeItems(this.children, index, 1); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(index); child.emit('removed', this); @@ -275,8 +288,14 @@ for (let i = 0; i < removed.length; ++i) { removed[i].parent = null; + if (removed[i].transform) + { + removed[i].transform._parentID = -1; + } } + this._boundsID++; + this.onChildrenChange(beginIndex); for (let i = 0; i < removed.length; ++i) diff --git a/src/core/display/DisplayObject.js b/src/core/display/DisplayObject.js index 3affe36..4e6c77d 100644 --- a/src/core/display/DisplayObject.js +++ b/src/core/display/DisplayObject.js @@ -109,7 +109,7 @@ /** * The original, cached mask of the object * - * @member {PIXI.Rectangle} + * @member {PIXI.Graphics|PIXI.Sprite} * @private */ this._mask = null; diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index 95f2af4..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 ); @@ -1065,10 +1061,15 @@ canvasRenderer = new CanvasRenderer(); } - tempMatrix.tx = -bounds.x; - tempMatrix.ty = -bounds.y; + this.transform.updateLocalTransform(); + this.transform.localTransform.copy(tempMatrix); - canvasRenderer.render(this, canvasBuffer, false, tempMatrix); + tempMatrix.invert(); + + tempMatrix.tx -= bounds.x; + tempMatrix.ty -= bounds.y; + + canvasRenderer.render(this, canvasBuffer, true, tempMatrix); const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode); 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 e67680f..867dde2 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/buildCircle.js b/src/core/graphics/webgl/utils/buildCircle.js index 0400d13..abf5d02 100644 --- a/src/core/graphics/webgl/utils/buildCircle.js +++ b/src/core/graphics/webgl/utils/buildCircle.js @@ -33,6 +33,11 @@ height = circleData.height; } + if (width === 0 || height === 0) + { + return; + } + const totalSegs = Math.floor(30 * Math.sqrt(circleData.radius)) || Math.floor(15 * Math.sqrt(circleData.width + circleData.height)); diff --git a/src/core/graphics/webgl/utils/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 90155f9..f03fe15 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -30,6 +30,7 @@ export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; export { default as CanvasGraphicsRenderer } from './graphics/canvas/CanvasGraphicsRenderer'; +export { default as Spritesheet } from './textures/Spritesheet'; export { default as Texture } from './textures/Texture'; export { default as BaseTexture } from './textures/BaseTexture'; export { default as RenderTexture } from './textures/RenderTexture'; diff --git a/src/core/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 ccce680..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); } /** @@ -162,6 +162,9 @@ if (transform) { transform.copy(tempWt); + + // lets not forget to flag the parent transform as dirty... + this._tempDisplayObjectParent.transform._worldID = -1; } else { @@ -169,6 +172,7 @@ } displayObject.parent = this._tempDisplayObjectParent; + displayObject.updateTransform(); displayObject.parent = cacheParent; // displayObject.hitArea = //TODO add a temp hit area @@ -279,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/canvas/utils/CanvasRenderTarget.js b/src/core/renderers/canvas/utils/CanvasRenderTarget.js index 3de0aef..b409891 100644 --- a/src/core/renderers/canvas/utils/CanvasRenderTarget.js +++ b/src/core/renderers/canvas/utils/CanvasRenderTarget.js @@ -1,5 +1,4 @@ import settings from '../../../settings'; -const { RESOLUTION } = settings; /** * Creates a Canvas element of the given size. @@ -30,7 +29,7 @@ */ this.context = this.canvas.getContext('2d'); - this.resolution = resolution || RESOLUTION; + this.resolution = resolution || settings.RESOLUTION; this.resize(width, height); } 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 ce0e1c4..788ed6c 100644 --- a/src/core/renderers/webgl/WebGLRenderer.js +++ b/src/core/renderers/webgl/WebGLRenderer.js @@ -30,8 +30,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 @@ -49,10 +49,19 @@ * enable this if you need to call toDataUrl on the webgl context. * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when * rendering, stopping pixel interpolation. + * @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; + + if (this.legacy) + { + glCore.VertexArrayObject.FORCE_NATIVE = true; + } /** * The type of this renderer as a standardised const @@ -229,7 +238,7 @@ this.emit('context', gl); // setup the width/height properties and gl viewport - this.resize(this.width, this.height); + this.resize(this.screen.width, this.screen.height); } /** @@ -323,16 +332,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) { @@ -376,6 +385,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 @@ -440,9 +469,10 @@ * Changes the current shader to the one given in parameter * * @param {PIXI.Shader} shader - the new shader + * @param {boolean} [autoProject=true] - Whether automatically set the projection matrix * @return {PIXI.WebGLRenderer} Returns itself. */ - bindShader(shader) + bindShader(shader, autoProject) { // TODO cache if (this._activeShader !== shader) @@ -450,8 +480,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 bbab3b3..944f41d 100644 --- a/src/core/renderers/webgl/filters/Filter.js +++ b/src/core/renderers/webgl/filters/Filter.js @@ -1,6 +1,7 @@ import extractUniformsFromSrc from './extractUniformsFromSrc'; import { uid } from '../../../utils'; import { BLEND_MODES } from '../../../const'; +import settings from '../../../settings'; const SOURCE_KEY_MAP = {}; @@ -10,7 +11,7 @@ * @memberof PIXI * @extends PIXI.Shader */ -class Filter +export default class Filter { /** * @param {string} [vertexSrc] - The source of the vertex shader. @@ -35,8 +36,6 @@ this.blendMode = BLEND_MODES.NORMAL; - // pull out the vertex and shader uniforms if they are not specified.. - // currently this does not extract structs only default types this.uniformData = uniforms || extractUniformsFromSrc(this.vertexSrc, this.fragmentSrc, 'projectionMatrix|uSampler'); /** @@ -80,7 +79,7 @@ * * @member {number} */ - this.resolution = 1; + this.resolution = settings.RESOLUTION; /** * If enabled is true the filter is applied, if false it will not. @@ -97,8 +96,11 @@ * @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 { // --- // // this.uniforms.filterMatrix = filterManager.calculateSpriteMatrix(tempMatrix, window.panda ); @@ -170,5 +172,3 @@ ].join('\n'); } } - -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 dc7072a..6c7422d 100644 --- a/src/core/renderers/webgl/managers/FilterManager.js +++ b/src/core/renderers/webgl/managers/FilterManager.js @@ -154,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 @@ -176,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; @@ -184,7 +184,7 @@ flop = t; } - filters[i].apply(this, flip, lastState.renderTarget, true); + filters[i].apply(this, flip, lastState.renderTarget, false, currentState); this.freePotRenderTarget(flip); this.freePotRenderTarget(flop); @@ -291,9 +291,13 @@ let textureCount = 1; let currentState; - if (shader.uniforms.data.filterArea) + // 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; @@ -306,9 +310,9 @@ // use this to clamp displaced texture coords so they belong to filterArea // see displacementFilter fragment shader for an example - if (shader.uniforms.data.filterClamp) + if (shader.uniforms.filterClamp) { - currentState = this.filterData.stack[this.filterData.index]; + currentState = currentState || this.filterData.stack[this.filterData.index]; const filterClamp = shader.uniforms.filterClamp; @@ -485,7 +489,7 @@ */ destroy() { - this.shaderCache = []; + this.shaderCache = {}; this.emptyPool(); } diff --git a/src/core/renderers/webgl/utils/RenderTarget.js b/src/core/renderers/webgl/utils/RenderTarget.js index afe51ab..6d2467f 100644 --- a/src/core/renderers/webgl/utils/RenderTarget.js +++ b/src/core/renderers/webgl/utils/RenderTarget.js @@ -124,7 +124,7 @@ * @default PIXI.settings.SCALE_MODE * @see PIXI.SCALE_MODES */ - this.scaleMode = scaleMode || settings.SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; /** * Whether this object is the root element or not 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 36ea5bb..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; @@ -346,8 +346,8 @@ /** * Gets the local bounds of the sprite object. * - * @param {Rectangle} rect - The output rectangle. - * @return {Rectangle} The bounds. + * @param {PIXI.Rectangle} rect - The output rectangle. + * @return {PIXI.Rectangle} The bounds. */ getLocalBounds(rect) { @@ -440,7 +440,7 @@ * * @static * @param {number|string|PIXI.BaseTexture|HTMLCanvasElement|HTMLVideoElement} source Source to create texture from - * @return {PIXI.Texture} 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 e77b39a..90b08c0 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++) @@ -103,11 +102,18 @@ { const gl = this.renderer.gl; - // step 1: first check max textures the GPU can handle. - this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); + if (this.renderer.legacy) + { + this.MAX_TEXTURES = 1; + } + else + { + // step 1: first check max textures the GPU can handle. + this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); - // step 2: check the maximum number of if statements the shader can have too.. - this.MAX_TEXTURES = checkMaxIfStatmentsInShader(this.MAX_TEXTURES, gl); + // step 2: check the maximum number of if statements the shader can have too.. + this.MAX_TEXTURES = checkMaxIfStatmentsInShader(this.MAX_TEXTURES, gl); + } const shader = this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); @@ -130,8 +136,12 @@ .addIndex(this.indexBuffer) .addAttribute(this.vertexBuffers[i], shader.attributes.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) .addAttribute(this.vertexBuffers[i], shader.attributes.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) - .addAttribute(this.vertexBuffers[i], shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4) - .addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + .addAttribute(this.vertexBuffers[i], shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); + + if (shader.attributes.aTextureId) + { + this.vaos[i].addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + } /* eslint-enable max-len */ } @@ -378,8 +388,12 @@ .addIndex(this.indexBuffer) .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) - .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4) - .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); + + if (this.shader.attributes.aTextureId) + { + this.vaos[this.vertexCount].addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + } /* eslint-enable max-len */ } diff --git a/src/core/text/Text.js b/src/core/text/Text.js index 013625a..cec8c32 100644 --- a/src/core/text/Text.js +++ b/src/core/text/Text.js @@ -6,6 +6,7 @@ import { TEXT_GRADIENT } from '../const'; import settings from '../settings'; import TextStyle from './TextStyle'; +import trimCanvas from '../utils/trimCanvas'; const defaultDestroyOptions = { texture: true, @@ -57,7 +58,7 @@ /** * The canvas 2d context that everything is drawn with - * @member {HTMLCanvasElement} + * @member {CanvasRenderingContext2D} */ this.context = this.canvas.getContext('2d'); @@ -189,10 +190,12 @@ if (style.dropShadow) { + this.context.shadowBlur = style.dropShadowBlur; + this.context.globalAlpha = style.dropShadowAlpha; + if (style.dropShadowBlur > 0) { this.context.shadowColor = style.dropShadowColor; - this.context.shadowBlur = style.dropShadowBlur; } else { @@ -237,6 +240,10 @@ } } + // reset the shadow blur and alpha that was set by the drop shadow, for the regular text + this.context.shadowBlur = 0; + this.context.globalAlpha = 1; + // set canvas text styles this.context.fillStyle = this._generateFillStyle(style, lines); @@ -326,6 +333,15 @@ */ updateTexture() { + if (this._style.trim) + { + const trimmed = trimCanvas(this.canvas); + + this.canvas.width = trimmed.width; + this.canvas.height = trimmed.height; + this.context.putImageData(trimmed.data, 0, 0); + } + const texture = this._texture; const style = this._style; @@ -475,6 +491,19 @@ } /** + * Gets the local bounds of the text object. + * + * @param {Rectangle} rect - The output rectangle. + * @return {Rectangle} The bounds. + */ + getLocalBounds(rect) + { + this.updateText(true); + + return super.getLocalBounds.call(this, rect); + } + + /** * calculates the bounds of the Text as a rectangle. The bounds calculation takes the worldTransform into account. */ _calculateBounds() @@ -525,6 +554,29 @@ const width = this.canvas.width / this.resolution; const height = this.canvas.height / this.resolution; + // make a copy of the style settings, so we can manipulate them later + const fill = style.fill.slice(); + const fillGradientStops = style.fillGradientStops.slice(); + + // wanting to evenly distribute the fills. So an array of 4 colours should give fills of 0.25, 0.5 and 0.75 + if (!fillGradientStops.length) + { + const lengthPlus1 = fill.length + 1; + + for (let i = 1; i < lengthPlus1; ++i) + { + fillGradientStops.push(i / lengthPlus1); + } + } + + // stop the bleeding of the last gradient on the line above to the top gradient of the this line + // by hard defining the first gradient colour at point 0, and last gradient colour at point 1 + fill.unshift(style.fill[0]); + fillGradientStops.unshift(0); + + fill.push(style.fill[style.fill.length - 1]); + fillGradientStops.push(1); + if (style.fillGradientType === TEXT_GRADIENT.LINEAR_VERTICAL) { // start the gradient at the top center of the canvas, and end at the bottom middle of the canvas @@ -532,15 +584,22 @@ // we need to repeat the gradient so that each individual line of text has the same vertical gradient effect // ['#FF0000', '#00FF00', '#0000FF'] over 2 lines would create stops at 0.125, 0.25, 0.375, 0.625, 0.75, 0.875 - totalIterations = (style.fill.length + 1) * lines.length; + totalIterations = (fill.length + 1) * lines.length; currentIteration = 0; for (let i = 0; i < lines.length; i++) { currentIteration += 1; - for (let j = 0; j < style.fill.length; j++) + for (let j = 0; j < fill.length; j++) { - stop = (currentIteration / totalIterations); - gradient.addColorStop(stop, style.fill[j]); + if (fillGradientStops[j]) + { + stop = (fillGradientStops[j] / lines.length) + (i / lines.length); + } + else + { + stop = currentIteration / totalIterations; + } + gradient.addColorStop(stop, fill[j]); currentIteration++; } } @@ -552,13 +611,20 @@ // can just evenly space out the gradients in this case, as multiple lines makes no difference // to an even left to right gradient - totalIterations = style.fill.length + 1; + totalIterations = fill.length + 1; currentIteration = 1; - for (let i = 0; i < style.fill.length; i++) + for (let i = 0; i < fill.length; i++) { - stop = currentIteration / totalIterations; - gradient.addColorStop(stop, style.fill[i]); + if (fillGradientStops[i]) + { + stop = fillGradientStops[i]; + } + else + { + stop = currentIteration / totalIterations; + } + gradient.addColorStop(stop, fill[i]); currentIteration++; } } @@ -569,7 +635,7 @@ /** * Destroys this text object. * Note* Unlike a Sprite, a Text object will automatically destroy its baseTexture and texture as - * the majorety of the time the texture will not be shared with any other Sprites. + * the majority of the time the texture will not be shared with any other Sprites. * * @param {object|boolean} [options] - Options parameter. A boolean will act as if all options * have been set to that value @@ -680,7 +746,7 @@ set text(text) // eslint-disable-line require-jsdoc { - text = String(text || ' '); + text = String(text === '' || text === null || text === undefined ? ' ' : text); if (this._text === text) { diff --git a/src/core/text/TextStyle.js b/src/core/text/TextStyle.js index 3b25528..890950b 100644 --- a/src/core/text/TextStyle.js +++ b/src/core/text/TextStyle.js @@ -8,12 +8,14 @@ align: 'left', breakWords: false, dropShadow: false, + dropShadowAlpha: 1, dropShadowAngle: Math.PI / 6, dropShadowBlur: 0, dropShadowColor: '#000000', dropShadowDistance: 5, fill: 'black', fillGradientType: TEXT_GRADIENT.LINEAR_VERTICAL, + fillGradientStops: [], fontFamily: 'Arial', fontSize: 26, fontStyle: 'normal', @@ -27,6 +29,7 @@ stroke: 'black', strokeThickness: 0, textBaseline: 'alphabetic', + trim: false, wordWrap: false, wordWrapWidth: 100, }; @@ -47,6 +50,7 @@ * @param {boolean} [style.breakWords=false] - Indicates if lines can be wrapped within words, it * needs wordWrap to be set to true * @param {boolean} [style.dropShadow=false] - Set a drop shadow for the text + * @param {number} [style.dropShadowAlpha=1] - Set alpha for the drop shadow * @param {number} [style.dropShadowAngle=Math.PI/6] - Set a angle of the drop shadow * @param {number} [style.dropShadowBlur=0] - Set a shadow blur radius * @param {string} [style.dropShadowColor='#000000'] - A fill style to be used on the dropshadow e.g 'red', '#00FF00' @@ -55,8 +59,10 @@ * fillstyle that will be used on the text e.g 'red', '#00FF00'. Can be an array to create a gradient * eg ['#000000','#FFFFFF'] * {@link https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/fillStyle|MDN} - * @param {number} [style.fillGradientType=PIXI.TEXT_GRADIENT.LINEAR_VERTICAL] - If fills styles are - * supplied, this can change the type/direction of the gradient. See {@link PIXI.TEXT_GRADIENT} for possible values + * @param {number} [style.fillGradientType=PIXI.TEXT_GRADIENT.LINEAR_VERTICAL] - If fill is an array of colours + * to create a gradient, this can change the type/direction of the gradient. See {@link PIXI.TEXT_GRADIENT} + * @param {number[]} [style.fillGradientStops] - If fill is an array of colours to create a gradient, this array can set + * the stop points (numbers between 0 and 1) for the color, overriding the default behaviour of evenly spacing them. * @param {string|string[]} [style.fontFamily='Arial'] - The font family * @param {number|string} [style.fontSize=26] - The font size (as a number it converts to px, but as a string, * equivalents are '26px','20pt','160%' or '1.6em') @@ -76,6 +82,7 @@ * e.g 'blue', '#FCFF00' * @param {number} [style.strokeThickness=0] - A number that represents the thickness of the stroke. * Default is 0 (no stroke) + * @param {boolean} [style.trim=false] - Trim transparent borders * @param {string} [style.textBaseline='alphabetic'] - The baseline of the text that is rendered. * @param {boolean} [style.wordWrap=false] - Indicates if word wrap should be used * @param {number} [style.wordWrapWidth=100] - The width at which text will wrap, it needs wordWrap to be set to true @@ -152,6 +159,19 @@ } } + get dropShadowAlpha() + { + return this._dropShadowAlpha; + } + set dropShadowAlpha(dropShadowAlpha) + { + if (this._dropShadowAlpha !== dropShadowAlpha) + { + this._dropShadowAlpha = dropShadowAlpha; + this.styleID++; + } + } + get dropShadowAngle() { return this._dropShadowAngle; @@ -232,6 +252,19 @@ } } + get fillGradientStops() + { + return this._fillGradientStops; + } + set fillGradientStops(fillGradientStops) + { + if (!areArraysEqual(this._fillGradientStops,fillGradientStops)) + { + this._fillGradientStops = fillGradientStops; + this.styleID++; + } + } + get fontFamily() { return this._fontFamily; @@ -402,6 +435,19 @@ } } + get trim() + { + return this._trim; + } + set trim(trim) + { + if (this._trim !== trim) + { + this._trim = trim; + this.styleID++; + } + } + get wordWrap() { return this._wordWrap; @@ -475,3 +521,34 @@ return color; } } + +/** + * Utility function to convert hexadecimal colors to strings, and simply return the color if it's a string. + * This version can also convert array of colors + * + * @param {Array} array1 First array to compare + * @param {Array} array2 Second array to compare + * @return {boolean} Do the arrays contain the same values in the same order + */ +function areArraysEqual(array1, array2) +{ + if (!Array.isArray(array1) || !Array.isArray(array2)) + { + return false; + } + + if (array1.length !== array2.length) + { + return false; + } + + for (let i = 0; i < array1.length; ++i) + { + if (array1[i] !== array2[i]) + { + return false; + } + } + + return true; +} diff --git a/src/core/textures/BaseRenderTexture.js b/src/core/textures/BaseRenderTexture.js index 5d50146..aa727f0 100644 --- a/src/core/textures/BaseRenderTexture.js +++ b/src/core/textures/BaseRenderTexture.js @@ -1,8 +1,6 @@ import BaseTexture from './BaseTexture'; import settings from '../settings'; -const { RESOLUTION, SCALE_MODE } = settings; - /** * A BaseRenderTexture is a special texture that allows any Pixi display object to be rendered to it. * @@ -54,7 +52,7 @@ { super(null, scaleMode); - this.resolution = resolution || RESOLUTION; + this.resolution = resolution || settings.RESOLUTION; this.width = width; this.height = height; @@ -62,7 +60,7 @@ this.realWidth = this.width * this.resolution; this.realHeight = this.height * this.resolution; - this.scaleMode = scaleMode || SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; this.hasLoaded = true; /** diff --git a/src/core/textures/BaseTexture.js b/src/core/textures/BaseTexture.js index 676dca3..aeccf39 100644 --- a/src/core/textures/BaseTexture.js +++ b/src/core/textures/BaseTexture.js @@ -77,7 +77,7 @@ * @default PIXI.settings.SCALE_MODE * @see PIXI.SCALE_MODES */ - this.scaleMode = scaleMode || settings.SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; /** * Set to true once the base texture has successfully loaded. @@ -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/RenderTexture.js b/src/core/textures/RenderTexture.js index 4c5034f..b3c6297 100644 --- a/src/core/textures/RenderTexture.js +++ b/src/core/textures/RenderTexture.js @@ -54,8 +54,8 @@ /* eslint-disable prefer-rest-params, no-console */ const width = arguments[1]; const height = arguments[2]; - const scaleMode = arguments[3] || 0; - const resolution = arguments[4] || 1; + const scaleMode = arguments[3]; + const resolution = arguments[4]; // we have an old render texture.. console.warn(`Please use RenderTexture.create(${width}, ${height}) instead of the ctor directly.`); 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 ca31456..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 @@ -459,7 +497,9 @@ if (frame.x + frame.width > this.baseTexture.width || frame.y + frame.height > this.baseTexture.height) { - throw new Error(`Texture Error: frame does not fit inside the base Texture dimensions ${this}`); + throw new Error('Texture Error: frame does not fit inside the base Texture dimensions: ' + + `X: ${frame.x} + ${frame.width} > ${this.baseTexture.width} ` + + `Y: ${frame.y} + ${frame.height} > ${this.baseTexture.height}`); } // this.valid = frame && frame.width && frame.height && this.baseTexture.source && this.baseTexture.hasLoaded; @@ -520,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. @@ -528,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 2a7497b..99e585a 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)); } /** @@ -360,3 +362,43 @@ * @private */ export const BaseTextureCache = {}; + +/** + * 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/core/utils/trimCanvas.js b/src/core/utils/trimCanvas.js new file mode 100644 index 0000000..5af6de0 --- /dev/null +++ b/src/core/utils/trimCanvas.js @@ -0,0 +1,83 @@ +/** + * Trim transparent borders from a canvas + * + * @memberof PIXI + * @function trimCanvas + * @private + * @param {HTMLCanvasElement} canvas - the canvas to trim + * @returns {object} Trim data + */ +export default function trimCanvas(canvas) +{ + // https://gist.github.com/remy/784508 + + let width = canvas.width; + let height = canvas.height; + + const context = canvas.getContext('2d'); + const imageData = context.getImageData(0, 0, width, height); + const pixels = imageData.data; + const len = pixels.length; + + const bound = { + top: null, + left: null, + right: null, + bottom: null, + }; + let i; + let x; + let y; + + for (i = 0; i < len; i += 4) + { + if (pixels[i + 3] !== 0) + { + x = (i / 4) % width; + y = ~~((i / 4) / width); + + if (bound.top === null) + { + bound.top = y; + } + + if (bound.left === null) + { + bound.left = x; + } + else if (x < bound.left) + { + bound.left = x; + } + + if (bound.right === null) + { + bound.right = x + 1; + } + else if (bound.right < x) + { + bound.right = x + 1; + } + + if (bound.bottom === null) + { + bound.bottom = y; + } + else if (bound.bottom < y) + { + bound.bottom = y; + } + } + } + + width = bound.right - bound.left; + height = bound.bottom - bound.top + 1; + + const data = context.getImageData(bound.left, bound.top, width, height); + + return { + height, + width, + data, + }; +} diff --git a/src/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/extras/TilingSprite.js b/src/extras/TilingSprite.js index f90d248..fe82451 100644 --- a/src/extras/TilingSprite.js +++ b/src/extras/TilingSprite.js @@ -220,20 +220,33 @@ transform.tx * resolution, transform.ty * resolution); - // TODO - this should be rolled into the setTransform above.. - context.scale(this.tileScale.x / baseTextureResolution, this.tileScale.y / baseTextureResolution); - - context.translate(modX + (this.anchor.x * -this._width), - modY + (this.anchor.y * -this._height)); - renderer.setBlendMode(this.blendMode); // fill the pattern! context.fillStyle = this._canvasPattern; - context.fillRect(-modX, - -modY, - this._width / this.tileScale.x * baseTextureResolution, - this._height / this.tileScale.y * baseTextureResolution); + + // TODO - this should be rolled into the setTransform above.. + context.scale(this.tileScale.x / baseTextureResolution, this.tileScale.y / baseTextureResolution); + + const anchorX = this.anchor.x * -this._width; + const anchorY = this.anchor.y * -this._height; + + if (this.uvRespectAnchor) + { + context.translate(modX, modY); + + context.fillRect(-modX + anchorX, -modY + anchorY, + this._width / this.tileScale.x * baseTextureResolution, + this._height / this.tileScale.y * baseTextureResolution); + } + else + { + context.translate(modX + anchorX, modY + anchorY); + + context.fillRect(-modX, -modY, + this._width / this.tileScale.x * baseTextureResolution, + this._height / this.tileScale.y * baseTextureResolution); + } } /** diff --git a/src/extras/cacheAsBitmap.js b/src/extras/cacheAsBitmap.js index 18043a7..5dd5640 100644 --- a/src/extras/cacheAsBitmap.js +++ b/src/extras/cacheAsBitmap.js @@ -41,6 +41,9 @@ * provide a performance benefit for complex static displayObjects. * To remove simply set this property to 'false' * + * IMPORTANT GOTCHA - make sure that all your textures are preloaded BEFORE setting this property to true + * as it will take a snapshot of what is currently there. If the textures have not loaded then they will not appear. + * * @member {boolean} * @memberof PIXI.DisplayObject# */ diff --git a/src/filters/blur/BlurFilter.js b/src/filters/blur/BlurFilter.js index 340fc58..1575db9 100644 --- a/src/filters/blur/BlurFilter.js +++ b/src/filters/blur/BlurFilter.js @@ -24,10 +24,9 @@ this.blurXFilter = new BlurXFilter(strength, quality, resolution, kernelSize); this.blurYFilter = new BlurYFilter(strength, quality, resolution, kernelSize); - this.resolution = 1; this.padding = 0; - this.resolution = resolution || 1; + this.resolution = resolution || core.settings.RESOLUTION; this.quality = quality || 4; this.blur = strength || 8; } diff --git a/src/filters/blur/BlurXFilter.js b/src/filters/blur/BlurXFilter.js index def99c3..0f48889 100644 --- a/src/filters/blur/BlurXFilter.js +++ b/src/filters/blur/BlurXFilter.js @@ -31,7 +31,7 @@ fragSrc ); - this.resolution = resolution || 1; + this.resolution = resolution || core.settings.RESOLUTION; this._quality = 0; diff --git a/src/filters/blur/BlurYFilter.js b/src/filters/blur/BlurYFilter.js index 638e0f0..6d489c7 100644 --- a/src/filters/blur/BlurYFilter.js +++ b/src/filters/blur/BlurYFilter.js @@ -31,7 +31,7 @@ fragSrc ); - this.resolution = resolution || 1; + this.resolution = resolution || core.settings.RESOLUTION; this._quality = 0; diff --git a/src/filters/colormatrix/ColorMatrixFilter.js b/src/filters/colormatrix/ColorMatrixFilter.js index 8509505..679f8ec 100644 --- a/src/filters/colormatrix/ColorMatrixFilter.js +++ b/src/filters/colormatrix/ColorMatrixFilter.js @@ -75,28 +75,28 @@ out[1] = (a[0] * b[1]) + (a[1] * b[6]) + (a[2] * b[11]) + (a[3] * b[16]); out[2] = (a[0] * b[2]) + (a[1] * b[7]) + (a[2] * b[12]) + (a[3] * b[17]); out[3] = (a[0] * b[3]) + (a[1] * b[8]) + (a[2] * b[13]) + (a[3] * b[18]); - out[4] = (a[0] * b[4]) + (a[1] * b[9]) + (a[2] * b[14]) + (a[3] * b[19]); + out[4] = (a[0] * b[4]) + (a[1] * b[9]) + (a[2] * b[14]) + (a[3] * b[19]) + a[4]; // Green Channel out[5] = (a[5] * b[0]) + (a[6] * b[5]) + (a[7] * b[10]) + (a[8] * b[15]); out[6] = (a[5] * b[1]) + (a[6] * b[6]) + (a[7] * b[11]) + (a[8] * b[16]); out[7] = (a[5] * b[2]) + (a[6] * b[7]) + (a[7] * b[12]) + (a[8] * b[17]); out[8] = (a[5] * b[3]) + (a[6] * b[8]) + (a[7] * b[13]) + (a[8] * b[18]); - out[9] = (a[5] * b[4]) + (a[6] * b[9]) + (a[7] * b[14]) + (a[8] * b[19]); + out[9] = (a[5] * b[4]) + (a[6] * b[9]) + (a[7] * b[14]) + (a[8] * b[19]) + a[9]; // Blue Channel out[10] = (a[10] * b[0]) + (a[11] * b[5]) + (a[12] * b[10]) + (a[13] * b[15]); out[11] = (a[10] * b[1]) + (a[11] * b[6]) + (a[12] * b[11]) + (a[13] * b[16]); out[12] = (a[10] * b[2]) + (a[11] * b[7]) + (a[12] * b[12]) + (a[13] * b[17]); out[13] = (a[10] * b[3]) + (a[11] * b[8]) + (a[12] * b[13]) + (a[13] * b[18]); - out[14] = (a[10] * b[4]) + (a[11] * b[9]) + (a[12] * b[14]) + (a[13] * b[19]); + out[14] = (a[10] * b[4]) + (a[11] * b[9]) + (a[12] * b[14]) + (a[13] * b[19]) + a[14]; // Alpha Channel out[15] = (a[15] * b[0]) + (a[16] * b[5]) + (a[17] * b[10]) + (a[18] * b[15]); out[16] = (a[15] * b[1]) + (a[16] * b[6]) + (a[17] * b[11]) + (a[18] * b[16]); out[17] = (a[15] * b[2]) + (a[16] * b[7]) + (a[17] * b[12]) + (a[18] * b[17]); out[18] = (a[15] * b[3]) + (a[16] * b[8]) + (a[17] * b[13]) + (a[18] * b[18]); - out[19] = (a[15] * b[4]) + (a[16] * b[9]) + (a[17] * b[14]) + (a[18] * b[19]); + out[19] = (a[15] * b[4]) + (a[16] * b[9]) + (a[17] * b[14]) + (a[18] * b[19]) + a[19]; return out; } diff --git a/src/filters/colormatrix/colorMatrix.frag b/src/filters/colormatrix/colorMatrix.frag index 3aaf2b3..c73c0e9 100644 --- a/src/filters/colormatrix/colorMatrix.frag +++ b/src/filters/colormatrix/colorMatrix.frag @@ -4,32 +4,38 @@ void main(void) { - vec4 c = texture2D(uSampler, vTextureCoord); + // Un-premultiply alpha before applying the color matrix. See issue #3539. + if (c.a > 0.0) { + c.rgb /= c.a; + } + vec4 result; + result.r = (m[0] * c.r); + result.r += (m[1] * c.g); + result.r += (m[2] * c.b); + result.r += (m[3] * c.a); + result.r += m[4]; - gl_FragColor.r = (m[0] * c.r); - gl_FragColor.r += (m[1] * c.g); - gl_FragColor.r += (m[2] * c.b); - gl_FragColor.r += (m[3] * c.a); - gl_FragColor.r += m[4] * c.a; + result.g = (m[5] * c.r); + result.g += (m[6] * c.g); + result.g += (m[7] * c.b); + result.g += (m[8] * c.a); + result.g += m[9]; - gl_FragColor.g = (m[5] * c.r); - gl_FragColor.g += (m[6] * c.g); - gl_FragColor.g += (m[7] * c.b); - gl_FragColor.g += (m[8] * c.a); - gl_FragColor.g += m[9] * c.a; + result.b = (m[10] * c.r); + result.b += (m[11] * c.g); + result.b += (m[12] * c.b); + result.b += (m[13] * c.a); + result.b += m[14]; - gl_FragColor.b = (m[10] * c.r); - gl_FragColor.b += (m[11] * c.g); - gl_FragColor.b += (m[12] * c.b); - gl_FragColor.b += (m[13] * c.a); - gl_FragColor.b += m[14] * c.a; + result.a = (m[15] * c.r); + result.a += (m[16] * c.g); + result.a += (m[17] * c.b); + result.a += (m[18] * c.a); + result.a += m[19]; - gl_FragColor.a = (m[15] * c.r); - gl_FragColor.a += (m[16] * c.g); - gl_FragColor.a += (m[17] * c.b); - gl_FragColor.a += (m[18] * c.a); - gl_FragColor.a += m[19] * c.a; + // Premultiply alpha again. + result.rgb *= result.a; -// gl_FragColor = vec4(m[0]); + gl_FragColor = result; } diff --git a/src/filters/displacement/DisplacementFilter.js b/src/filters/displacement/DisplacementFilter.js index 112de0b..d5355c3 100644 --- a/src/filters/displacement/DisplacementFilter.js +++ b/src/filters/displacement/DisplacementFilter.js @@ -36,7 +36,7 @@ this.maskMatrix = maskMatrix; this.uniforms.mapSampler = sprite.texture; - this.uniforms.filterMatrix = maskMatrix.toArray(true); + this.uniforms.filterMatrix = maskMatrix; this.uniforms.scale = { x: 1, y: 1 }; if (scale === null || scale === undefined) diff --git a/src/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 1271fef..71ade15 100644 --- a/package.json +++ b/package.json @@ -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/scripts/release.js b/scripts/release.js index c4c8e36..156620c 100644 --- a/scripts/release.js +++ b/scripts/release.js @@ -12,7 +12,8 @@ 'dist/**/*', 'lib/**/*', 'src/**/*', - 'scripts/**/*', + 'scripts/*', + 'scripts/renders/*', 'test/**/*', '*.json', '*.md', diff --git a/scripts/renders/client.js b/scripts/renders/client.js index 16c9565..6faee34 100755 --- a/scripts/renders/client.js +++ b/scripts/renders/client.js @@ -1,6 +1,6 @@ 'use strict'; -require('../../bin/pixi'); +require('../../dist/pixi'); PIXI.utils.skipHello(); const fs = require('fs'); 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 bc730db..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. @@ -33,8 +33,11 @@ * need to call toDataUrl on the webgl context * @param {number} [options.resolution=1] - The resolution / device pixel ratio of the renderer, retina would be 2 * @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 @@ -43,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. */ @@ -73,7 +99,7 @@ */ stop() { - this.ticker.stop(); + this._ticker.stop(); } /** @@ -81,7 +107,7 @@ */ start() { - this.ticker.start(); + this._ticker.start(); } /** @@ -95,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 index 81e076b..78b17e6 100644 --- a/src/core/Shader.js +++ b/src/core/Shader.js @@ -1,9 +1,7 @@ import { GLShader } from 'pixi-gl-core'; import settings from './settings'; -const { PRECISION } = settings; - -function checkPrecision(src) +function checkPrecision(src, def) { if (src instanceof Array) { @@ -11,14 +9,14 @@ { const copy = src.slice(0); - copy.unshift(`precision ${PRECISION} float;`); + copy.unshift(`precision ${def} float;`); return copy; } } else if (src.substring(0, 9) !== 'precision') { - return `precision ${PRECISION} float;\n${src}`; + return `precision ${def} float;\n${src}`; } return src; @@ -42,6 +40,7 @@ */ constructor(gl, vertexSrc, fragmentSrc) { - super(gl, checkPrecision(vertexSrc), checkPrecision(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 3425b41..f9235e4 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -181,7 +181,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 b3792ef..17fde8d 100644 --- a/src/core/display/Container.js +++ b/src/core/display/Container.js @@ -73,13 +73,14 @@ } child.parent = this; - - // ensure a transform will be recalculated.. - this.transform._parentID = -1; - this._boundsID++; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.push(child); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(this.children.length - 1); child.emit('added', this); @@ -108,9 +109,14 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.splice(index, 0, child); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(index); child.emit('added', this); @@ -174,6 +180,7 @@ removeItems(this.children, currentIndex, 1); // remove from old position this.children.splice(index, 0, child); // add at new position + this.onChildrenChange(index); } @@ -220,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! @@ -244,9 +252,14 @@ { const child = this.getChildAt(index); + // ensure child transform will be recalculated.. child.parent = null; + child.transform._parentID = -1; removeItems(this.children, index, 1); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(index); child.emit('removed', this); @@ -275,8 +288,14 @@ for (let i = 0; i < removed.length; ++i) { removed[i].parent = null; + if (removed[i].transform) + { + removed[i].transform._parentID = -1; + } } + this._boundsID++; + this.onChildrenChange(beginIndex); for (let i = 0; i < removed.length; ++i) diff --git a/src/core/display/DisplayObject.js b/src/core/display/DisplayObject.js index 3affe36..4e6c77d 100644 --- a/src/core/display/DisplayObject.js +++ b/src/core/display/DisplayObject.js @@ -109,7 +109,7 @@ /** * The original, cached mask of the object * - * @member {PIXI.Rectangle} + * @member {PIXI.Graphics|PIXI.Sprite} * @private */ this._mask = null; diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index 95f2af4..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 ); @@ -1065,10 +1061,15 @@ canvasRenderer = new CanvasRenderer(); } - tempMatrix.tx = -bounds.x; - tempMatrix.ty = -bounds.y; + this.transform.updateLocalTransform(); + this.transform.localTransform.copy(tempMatrix); - canvasRenderer.render(this, canvasBuffer, false, tempMatrix); + tempMatrix.invert(); + + tempMatrix.tx -= bounds.x; + tempMatrix.ty -= bounds.y; + + canvasRenderer.render(this, canvasBuffer, true, tempMatrix); const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode); 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 e67680f..867dde2 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/buildCircle.js b/src/core/graphics/webgl/utils/buildCircle.js index 0400d13..abf5d02 100644 --- a/src/core/graphics/webgl/utils/buildCircle.js +++ b/src/core/graphics/webgl/utils/buildCircle.js @@ -33,6 +33,11 @@ height = circleData.height; } + if (width === 0 || height === 0) + { + return; + } + const totalSegs = Math.floor(30 * Math.sqrt(circleData.radius)) || Math.floor(15 * Math.sqrt(circleData.width + circleData.height)); diff --git a/src/core/graphics/webgl/utils/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 90155f9..f03fe15 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -30,6 +30,7 @@ export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; export { default as CanvasGraphicsRenderer } from './graphics/canvas/CanvasGraphicsRenderer'; +export { default as Spritesheet } from './textures/Spritesheet'; export { default as Texture } from './textures/Texture'; export { default as BaseTexture } from './textures/BaseTexture'; export { default as RenderTexture } from './textures/RenderTexture'; diff --git a/src/core/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 ccce680..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); } /** @@ -162,6 +162,9 @@ if (transform) { transform.copy(tempWt); + + // lets not forget to flag the parent transform as dirty... + this._tempDisplayObjectParent.transform._worldID = -1; } else { @@ -169,6 +172,7 @@ } displayObject.parent = this._tempDisplayObjectParent; + displayObject.updateTransform(); displayObject.parent = cacheParent; // displayObject.hitArea = //TODO add a temp hit area @@ -279,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/canvas/utils/CanvasRenderTarget.js b/src/core/renderers/canvas/utils/CanvasRenderTarget.js index 3de0aef..b409891 100644 --- a/src/core/renderers/canvas/utils/CanvasRenderTarget.js +++ b/src/core/renderers/canvas/utils/CanvasRenderTarget.js @@ -1,5 +1,4 @@ import settings from '../../../settings'; -const { RESOLUTION } = settings; /** * Creates a Canvas element of the given size. @@ -30,7 +29,7 @@ */ this.context = this.canvas.getContext('2d'); - this.resolution = resolution || RESOLUTION; + this.resolution = resolution || settings.RESOLUTION; this.resize(width, height); } 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 ce0e1c4..788ed6c 100644 --- a/src/core/renderers/webgl/WebGLRenderer.js +++ b/src/core/renderers/webgl/WebGLRenderer.js @@ -30,8 +30,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 @@ -49,10 +49,19 @@ * enable this if you need to call toDataUrl on the webgl context. * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when * rendering, stopping pixel interpolation. + * @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; + + if (this.legacy) + { + glCore.VertexArrayObject.FORCE_NATIVE = true; + } /** * The type of this renderer as a standardised const @@ -229,7 +238,7 @@ this.emit('context', gl); // setup the width/height properties and gl viewport - this.resize(this.width, this.height); + this.resize(this.screen.width, this.screen.height); } /** @@ -323,16 +332,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) { @@ -376,6 +385,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 @@ -440,9 +469,10 @@ * Changes the current shader to the one given in parameter * * @param {PIXI.Shader} shader - the new shader + * @param {boolean} [autoProject=true] - Whether automatically set the projection matrix * @return {PIXI.WebGLRenderer} Returns itself. */ - bindShader(shader) + bindShader(shader, autoProject) { // TODO cache if (this._activeShader !== shader) @@ -450,8 +480,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 bbab3b3..944f41d 100644 --- a/src/core/renderers/webgl/filters/Filter.js +++ b/src/core/renderers/webgl/filters/Filter.js @@ -1,6 +1,7 @@ import extractUniformsFromSrc from './extractUniformsFromSrc'; import { uid } from '../../../utils'; import { BLEND_MODES } from '../../../const'; +import settings from '../../../settings'; const SOURCE_KEY_MAP = {}; @@ -10,7 +11,7 @@ * @memberof PIXI * @extends PIXI.Shader */ -class Filter +export default class Filter { /** * @param {string} [vertexSrc] - The source of the vertex shader. @@ -35,8 +36,6 @@ this.blendMode = BLEND_MODES.NORMAL; - // pull out the vertex and shader uniforms if they are not specified.. - // currently this does not extract structs only default types this.uniformData = uniforms || extractUniformsFromSrc(this.vertexSrc, this.fragmentSrc, 'projectionMatrix|uSampler'); /** @@ -80,7 +79,7 @@ * * @member {number} */ - this.resolution = 1; + this.resolution = settings.RESOLUTION; /** * If enabled is true the filter is applied, if false it will not. @@ -97,8 +96,11 @@ * @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 { // --- // // this.uniforms.filterMatrix = filterManager.calculateSpriteMatrix(tempMatrix, window.panda ); @@ -170,5 +172,3 @@ ].join('\n'); } } - -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 dc7072a..6c7422d 100644 --- a/src/core/renderers/webgl/managers/FilterManager.js +++ b/src/core/renderers/webgl/managers/FilterManager.js @@ -154,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 @@ -176,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; @@ -184,7 +184,7 @@ flop = t; } - filters[i].apply(this, flip, lastState.renderTarget, true); + filters[i].apply(this, flip, lastState.renderTarget, false, currentState); this.freePotRenderTarget(flip); this.freePotRenderTarget(flop); @@ -291,9 +291,13 @@ let textureCount = 1; let currentState; - if (shader.uniforms.data.filterArea) + // 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; @@ -306,9 +310,9 @@ // use this to clamp displaced texture coords so they belong to filterArea // see displacementFilter fragment shader for an example - if (shader.uniforms.data.filterClamp) + if (shader.uniforms.filterClamp) { - currentState = this.filterData.stack[this.filterData.index]; + currentState = currentState || this.filterData.stack[this.filterData.index]; const filterClamp = shader.uniforms.filterClamp; @@ -485,7 +489,7 @@ */ destroy() { - this.shaderCache = []; + this.shaderCache = {}; this.emptyPool(); } diff --git a/src/core/renderers/webgl/utils/RenderTarget.js b/src/core/renderers/webgl/utils/RenderTarget.js index afe51ab..6d2467f 100644 --- a/src/core/renderers/webgl/utils/RenderTarget.js +++ b/src/core/renderers/webgl/utils/RenderTarget.js @@ -124,7 +124,7 @@ * @default PIXI.settings.SCALE_MODE * @see PIXI.SCALE_MODES */ - this.scaleMode = scaleMode || settings.SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; /** * Whether this object is the root element or not 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 36ea5bb..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; @@ -346,8 +346,8 @@ /** * Gets the local bounds of the sprite object. * - * @param {Rectangle} rect - The output rectangle. - * @return {Rectangle} The bounds. + * @param {PIXI.Rectangle} rect - The output rectangle. + * @return {PIXI.Rectangle} The bounds. */ getLocalBounds(rect) { @@ -440,7 +440,7 @@ * * @static * @param {number|string|PIXI.BaseTexture|HTMLCanvasElement|HTMLVideoElement} source Source to create texture from - * @return {PIXI.Texture} 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 e77b39a..90b08c0 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++) @@ -103,11 +102,18 @@ { const gl = this.renderer.gl; - // step 1: first check max textures the GPU can handle. - this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); + if (this.renderer.legacy) + { + this.MAX_TEXTURES = 1; + } + else + { + // step 1: first check max textures the GPU can handle. + this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); - // step 2: check the maximum number of if statements the shader can have too.. - this.MAX_TEXTURES = checkMaxIfStatmentsInShader(this.MAX_TEXTURES, gl); + // step 2: check the maximum number of if statements the shader can have too.. + this.MAX_TEXTURES = checkMaxIfStatmentsInShader(this.MAX_TEXTURES, gl); + } const shader = this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); @@ -130,8 +136,12 @@ .addIndex(this.indexBuffer) .addAttribute(this.vertexBuffers[i], shader.attributes.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) .addAttribute(this.vertexBuffers[i], shader.attributes.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) - .addAttribute(this.vertexBuffers[i], shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4) - .addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + .addAttribute(this.vertexBuffers[i], shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); + + if (shader.attributes.aTextureId) + { + this.vaos[i].addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + } /* eslint-enable max-len */ } @@ -378,8 +388,12 @@ .addIndex(this.indexBuffer) .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) - .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4) - .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); + + if (this.shader.attributes.aTextureId) + { + this.vaos[this.vertexCount].addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + } /* eslint-enable max-len */ } diff --git a/src/core/text/Text.js b/src/core/text/Text.js index 013625a..cec8c32 100644 --- a/src/core/text/Text.js +++ b/src/core/text/Text.js @@ -6,6 +6,7 @@ import { TEXT_GRADIENT } from '../const'; import settings from '../settings'; import TextStyle from './TextStyle'; +import trimCanvas from '../utils/trimCanvas'; const defaultDestroyOptions = { texture: true, @@ -57,7 +58,7 @@ /** * The canvas 2d context that everything is drawn with - * @member {HTMLCanvasElement} + * @member {CanvasRenderingContext2D} */ this.context = this.canvas.getContext('2d'); @@ -189,10 +190,12 @@ if (style.dropShadow) { + this.context.shadowBlur = style.dropShadowBlur; + this.context.globalAlpha = style.dropShadowAlpha; + if (style.dropShadowBlur > 0) { this.context.shadowColor = style.dropShadowColor; - this.context.shadowBlur = style.dropShadowBlur; } else { @@ -237,6 +240,10 @@ } } + // reset the shadow blur and alpha that was set by the drop shadow, for the regular text + this.context.shadowBlur = 0; + this.context.globalAlpha = 1; + // set canvas text styles this.context.fillStyle = this._generateFillStyle(style, lines); @@ -326,6 +333,15 @@ */ updateTexture() { + if (this._style.trim) + { + const trimmed = trimCanvas(this.canvas); + + this.canvas.width = trimmed.width; + this.canvas.height = trimmed.height; + this.context.putImageData(trimmed.data, 0, 0); + } + const texture = this._texture; const style = this._style; @@ -475,6 +491,19 @@ } /** + * Gets the local bounds of the text object. + * + * @param {Rectangle} rect - The output rectangle. + * @return {Rectangle} The bounds. + */ + getLocalBounds(rect) + { + this.updateText(true); + + return super.getLocalBounds.call(this, rect); + } + + /** * calculates the bounds of the Text as a rectangle. The bounds calculation takes the worldTransform into account. */ _calculateBounds() @@ -525,6 +554,29 @@ const width = this.canvas.width / this.resolution; const height = this.canvas.height / this.resolution; + // make a copy of the style settings, so we can manipulate them later + const fill = style.fill.slice(); + const fillGradientStops = style.fillGradientStops.slice(); + + // wanting to evenly distribute the fills. So an array of 4 colours should give fills of 0.25, 0.5 and 0.75 + if (!fillGradientStops.length) + { + const lengthPlus1 = fill.length + 1; + + for (let i = 1; i < lengthPlus1; ++i) + { + fillGradientStops.push(i / lengthPlus1); + } + } + + // stop the bleeding of the last gradient on the line above to the top gradient of the this line + // by hard defining the first gradient colour at point 0, and last gradient colour at point 1 + fill.unshift(style.fill[0]); + fillGradientStops.unshift(0); + + fill.push(style.fill[style.fill.length - 1]); + fillGradientStops.push(1); + if (style.fillGradientType === TEXT_GRADIENT.LINEAR_VERTICAL) { // start the gradient at the top center of the canvas, and end at the bottom middle of the canvas @@ -532,15 +584,22 @@ // we need to repeat the gradient so that each individual line of text has the same vertical gradient effect // ['#FF0000', '#00FF00', '#0000FF'] over 2 lines would create stops at 0.125, 0.25, 0.375, 0.625, 0.75, 0.875 - totalIterations = (style.fill.length + 1) * lines.length; + totalIterations = (fill.length + 1) * lines.length; currentIteration = 0; for (let i = 0; i < lines.length; i++) { currentIteration += 1; - for (let j = 0; j < style.fill.length; j++) + for (let j = 0; j < fill.length; j++) { - stop = (currentIteration / totalIterations); - gradient.addColorStop(stop, style.fill[j]); + if (fillGradientStops[j]) + { + stop = (fillGradientStops[j] / lines.length) + (i / lines.length); + } + else + { + stop = currentIteration / totalIterations; + } + gradient.addColorStop(stop, fill[j]); currentIteration++; } } @@ -552,13 +611,20 @@ // can just evenly space out the gradients in this case, as multiple lines makes no difference // to an even left to right gradient - totalIterations = style.fill.length + 1; + totalIterations = fill.length + 1; currentIteration = 1; - for (let i = 0; i < style.fill.length; i++) + for (let i = 0; i < fill.length; i++) { - stop = currentIteration / totalIterations; - gradient.addColorStop(stop, style.fill[i]); + if (fillGradientStops[i]) + { + stop = fillGradientStops[i]; + } + else + { + stop = currentIteration / totalIterations; + } + gradient.addColorStop(stop, fill[i]); currentIteration++; } } @@ -569,7 +635,7 @@ /** * Destroys this text object. * Note* Unlike a Sprite, a Text object will automatically destroy its baseTexture and texture as - * the majorety of the time the texture will not be shared with any other Sprites. + * the majority of the time the texture will not be shared with any other Sprites. * * @param {object|boolean} [options] - Options parameter. A boolean will act as if all options * have been set to that value @@ -680,7 +746,7 @@ set text(text) // eslint-disable-line require-jsdoc { - text = String(text || ' '); + text = String(text === '' || text === null || text === undefined ? ' ' : text); if (this._text === text) { diff --git a/src/core/text/TextStyle.js b/src/core/text/TextStyle.js index 3b25528..890950b 100644 --- a/src/core/text/TextStyle.js +++ b/src/core/text/TextStyle.js @@ -8,12 +8,14 @@ align: 'left', breakWords: false, dropShadow: false, + dropShadowAlpha: 1, dropShadowAngle: Math.PI / 6, dropShadowBlur: 0, dropShadowColor: '#000000', dropShadowDistance: 5, fill: 'black', fillGradientType: TEXT_GRADIENT.LINEAR_VERTICAL, + fillGradientStops: [], fontFamily: 'Arial', fontSize: 26, fontStyle: 'normal', @@ -27,6 +29,7 @@ stroke: 'black', strokeThickness: 0, textBaseline: 'alphabetic', + trim: false, wordWrap: false, wordWrapWidth: 100, }; @@ -47,6 +50,7 @@ * @param {boolean} [style.breakWords=false] - Indicates if lines can be wrapped within words, it * needs wordWrap to be set to true * @param {boolean} [style.dropShadow=false] - Set a drop shadow for the text + * @param {number} [style.dropShadowAlpha=1] - Set alpha for the drop shadow * @param {number} [style.dropShadowAngle=Math.PI/6] - Set a angle of the drop shadow * @param {number} [style.dropShadowBlur=0] - Set a shadow blur radius * @param {string} [style.dropShadowColor='#000000'] - A fill style to be used on the dropshadow e.g 'red', '#00FF00' @@ -55,8 +59,10 @@ * fillstyle that will be used on the text e.g 'red', '#00FF00'. Can be an array to create a gradient * eg ['#000000','#FFFFFF'] * {@link https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/fillStyle|MDN} - * @param {number} [style.fillGradientType=PIXI.TEXT_GRADIENT.LINEAR_VERTICAL] - If fills styles are - * supplied, this can change the type/direction of the gradient. See {@link PIXI.TEXT_GRADIENT} for possible values + * @param {number} [style.fillGradientType=PIXI.TEXT_GRADIENT.LINEAR_VERTICAL] - If fill is an array of colours + * to create a gradient, this can change the type/direction of the gradient. See {@link PIXI.TEXT_GRADIENT} + * @param {number[]} [style.fillGradientStops] - If fill is an array of colours to create a gradient, this array can set + * the stop points (numbers between 0 and 1) for the color, overriding the default behaviour of evenly spacing them. * @param {string|string[]} [style.fontFamily='Arial'] - The font family * @param {number|string} [style.fontSize=26] - The font size (as a number it converts to px, but as a string, * equivalents are '26px','20pt','160%' or '1.6em') @@ -76,6 +82,7 @@ * e.g 'blue', '#FCFF00' * @param {number} [style.strokeThickness=0] - A number that represents the thickness of the stroke. * Default is 0 (no stroke) + * @param {boolean} [style.trim=false] - Trim transparent borders * @param {string} [style.textBaseline='alphabetic'] - The baseline of the text that is rendered. * @param {boolean} [style.wordWrap=false] - Indicates if word wrap should be used * @param {number} [style.wordWrapWidth=100] - The width at which text will wrap, it needs wordWrap to be set to true @@ -152,6 +159,19 @@ } } + get dropShadowAlpha() + { + return this._dropShadowAlpha; + } + set dropShadowAlpha(dropShadowAlpha) + { + if (this._dropShadowAlpha !== dropShadowAlpha) + { + this._dropShadowAlpha = dropShadowAlpha; + this.styleID++; + } + } + get dropShadowAngle() { return this._dropShadowAngle; @@ -232,6 +252,19 @@ } } + get fillGradientStops() + { + return this._fillGradientStops; + } + set fillGradientStops(fillGradientStops) + { + if (!areArraysEqual(this._fillGradientStops,fillGradientStops)) + { + this._fillGradientStops = fillGradientStops; + this.styleID++; + } + } + get fontFamily() { return this._fontFamily; @@ -402,6 +435,19 @@ } } + get trim() + { + return this._trim; + } + set trim(trim) + { + if (this._trim !== trim) + { + this._trim = trim; + this.styleID++; + } + } + get wordWrap() { return this._wordWrap; @@ -475,3 +521,34 @@ return color; } } + +/** + * Utility function to convert hexadecimal colors to strings, and simply return the color if it's a string. + * This version can also convert array of colors + * + * @param {Array} array1 First array to compare + * @param {Array} array2 Second array to compare + * @return {boolean} Do the arrays contain the same values in the same order + */ +function areArraysEqual(array1, array2) +{ + if (!Array.isArray(array1) || !Array.isArray(array2)) + { + return false; + } + + if (array1.length !== array2.length) + { + return false; + } + + for (let i = 0; i < array1.length; ++i) + { + if (array1[i] !== array2[i]) + { + return false; + } + } + + return true; +} diff --git a/src/core/textures/BaseRenderTexture.js b/src/core/textures/BaseRenderTexture.js index 5d50146..aa727f0 100644 --- a/src/core/textures/BaseRenderTexture.js +++ b/src/core/textures/BaseRenderTexture.js @@ -1,8 +1,6 @@ import BaseTexture from './BaseTexture'; import settings from '../settings'; -const { RESOLUTION, SCALE_MODE } = settings; - /** * A BaseRenderTexture is a special texture that allows any Pixi display object to be rendered to it. * @@ -54,7 +52,7 @@ { super(null, scaleMode); - this.resolution = resolution || RESOLUTION; + this.resolution = resolution || settings.RESOLUTION; this.width = width; this.height = height; @@ -62,7 +60,7 @@ this.realWidth = this.width * this.resolution; this.realHeight = this.height * this.resolution; - this.scaleMode = scaleMode || SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; this.hasLoaded = true; /** diff --git a/src/core/textures/BaseTexture.js b/src/core/textures/BaseTexture.js index 676dca3..aeccf39 100644 --- a/src/core/textures/BaseTexture.js +++ b/src/core/textures/BaseTexture.js @@ -77,7 +77,7 @@ * @default PIXI.settings.SCALE_MODE * @see PIXI.SCALE_MODES */ - this.scaleMode = scaleMode || settings.SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; /** * Set to true once the base texture has successfully loaded. @@ -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/RenderTexture.js b/src/core/textures/RenderTexture.js index 4c5034f..b3c6297 100644 --- a/src/core/textures/RenderTexture.js +++ b/src/core/textures/RenderTexture.js @@ -54,8 +54,8 @@ /* eslint-disable prefer-rest-params, no-console */ const width = arguments[1]; const height = arguments[2]; - const scaleMode = arguments[3] || 0; - const resolution = arguments[4] || 1; + const scaleMode = arguments[3]; + const resolution = arguments[4]; // we have an old render texture.. console.warn(`Please use RenderTexture.create(${width}, ${height}) instead of the ctor directly.`); 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 ca31456..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 @@ -459,7 +497,9 @@ if (frame.x + frame.width > this.baseTexture.width || frame.y + frame.height > this.baseTexture.height) { - throw new Error(`Texture Error: frame does not fit inside the base Texture dimensions ${this}`); + throw new Error('Texture Error: frame does not fit inside the base Texture dimensions: ' + + `X: ${frame.x} + ${frame.width} > ${this.baseTexture.width} ` + + `Y: ${frame.y} + ${frame.height} > ${this.baseTexture.height}`); } // this.valid = frame && frame.width && frame.height && this.baseTexture.source && this.baseTexture.hasLoaded; @@ -520,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. @@ -528,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 2a7497b..99e585a 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)); } /** @@ -360,3 +362,43 @@ * @private */ export const BaseTextureCache = {}; + +/** + * 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/core/utils/trimCanvas.js b/src/core/utils/trimCanvas.js new file mode 100644 index 0000000..5af6de0 --- /dev/null +++ b/src/core/utils/trimCanvas.js @@ -0,0 +1,83 @@ +/** + * Trim transparent borders from a canvas + * + * @memberof PIXI + * @function trimCanvas + * @private + * @param {HTMLCanvasElement} canvas - the canvas to trim + * @returns {object} Trim data + */ +export default function trimCanvas(canvas) +{ + // https://gist.github.com/remy/784508 + + let width = canvas.width; + let height = canvas.height; + + const context = canvas.getContext('2d'); + const imageData = context.getImageData(0, 0, width, height); + const pixels = imageData.data; + const len = pixels.length; + + const bound = { + top: null, + left: null, + right: null, + bottom: null, + }; + let i; + let x; + let y; + + for (i = 0; i < len; i += 4) + { + if (pixels[i + 3] !== 0) + { + x = (i / 4) % width; + y = ~~((i / 4) / width); + + if (bound.top === null) + { + bound.top = y; + } + + if (bound.left === null) + { + bound.left = x; + } + else if (x < bound.left) + { + bound.left = x; + } + + if (bound.right === null) + { + bound.right = x + 1; + } + else if (bound.right < x) + { + bound.right = x + 1; + } + + if (bound.bottom === null) + { + bound.bottom = y; + } + else if (bound.bottom < y) + { + bound.bottom = y; + } + } + } + + width = bound.right - bound.left; + height = bound.bottom - bound.top + 1; + + const data = context.getImageData(bound.left, bound.top, width, height); + + return { + height, + width, + data, + }; +} diff --git a/src/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/extras/TilingSprite.js b/src/extras/TilingSprite.js index f90d248..fe82451 100644 --- a/src/extras/TilingSprite.js +++ b/src/extras/TilingSprite.js @@ -220,20 +220,33 @@ transform.tx * resolution, transform.ty * resolution); - // TODO - this should be rolled into the setTransform above.. - context.scale(this.tileScale.x / baseTextureResolution, this.tileScale.y / baseTextureResolution); - - context.translate(modX + (this.anchor.x * -this._width), - modY + (this.anchor.y * -this._height)); - renderer.setBlendMode(this.blendMode); // fill the pattern! context.fillStyle = this._canvasPattern; - context.fillRect(-modX, - -modY, - this._width / this.tileScale.x * baseTextureResolution, - this._height / this.tileScale.y * baseTextureResolution); + + // TODO - this should be rolled into the setTransform above.. + context.scale(this.tileScale.x / baseTextureResolution, this.tileScale.y / baseTextureResolution); + + const anchorX = this.anchor.x * -this._width; + const anchorY = this.anchor.y * -this._height; + + if (this.uvRespectAnchor) + { + context.translate(modX, modY); + + context.fillRect(-modX + anchorX, -modY + anchorY, + this._width / this.tileScale.x * baseTextureResolution, + this._height / this.tileScale.y * baseTextureResolution); + } + else + { + context.translate(modX + anchorX, modY + anchorY); + + context.fillRect(-modX, -modY, + this._width / this.tileScale.x * baseTextureResolution, + this._height / this.tileScale.y * baseTextureResolution); + } } /** diff --git a/src/extras/cacheAsBitmap.js b/src/extras/cacheAsBitmap.js index 18043a7..5dd5640 100644 --- a/src/extras/cacheAsBitmap.js +++ b/src/extras/cacheAsBitmap.js @@ -41,6 +41,9 @@ * provide a performance benefit for complex static displayObjects. * To remove simply set this property to 'false' * + * IMPORTANT GOTCHA - make sure that all your textures are preloaded BEFORE setting this property to true + * as it will take a snapshot of what is currently there. If the textures have not loaded then they will not appear. + * * @member {boolean} * @memberof PIXI.DisplayObject# */ diff --git a/src/filters/blur/BlurFilter.js b/src/filters/blur/BlurFilter.js index 340fc58..1575db9 100644 --- a/src/filters/blur/BlurFilter.js +++ b/src/filters/blur/BlurFilter.js @@ -24,10 +24,9 @@ this.blurXFilter = new BlurXFilter(strength, quality, resolution, kernelSize); this.blurYFilter = new BlurYFilter(strength, quality, resolution, kernelSize); - this.resolution = 1; this.padding = 0; - this.resolution = resolution || 1; + this.resolution = resolution || core.settings.RESOLUTION; this.quality = quality || 4; this.blur = strength || 8; } diff --git a/src/filters/blur/BlurXFilter.js b/src/filters/blur/BlurXFilter.js index def99c3..0f48889 100644 --- a/src/filters/blur/BlurXFilter.js +++ b/src/filters/blur/BlurXFilter.js @@ -31,7 +31,7 @@ fragSrc ); - this.resolution = resolution || 1; + this.resolution = resolution || core.settings.RESOLUTION; this._quality = 0; diff --git a/src/filters/blur/BlurYFilter.js b/src/filters/blur/BlurYFilter.js index 638e0f0..6d489c7 100644 --- a/src/filters/blur/BlurYFilter.js +++ b/src/filters/blur/BlurYFilter.js @@ -31,7 +31,7 @@ fragSrc ); - this.resolution = resolution || 1; + this.resolution = resolution || core.settings.RESOLUTION; this._quality = 0; diff --git a/src/filters/colormatrix/ColorMatrixFilter.js b/src/filters/colormatrix/ColorMatrixFilter.js index 8509505..679f8ec 100644 --- a/src/filters/colormatrix/ColorMatrixFilter.js +++ b/src/filters/colormatrix/ColorMatrixFilter.js @@ -75,28 +75,28 @@ out[1] = (a[0] * b[1]) + (a[1] * b[6]) + (a[2] * b[11]) + (a[3] * b[16]); out[2] = (a[0] * b[2]) + (a[1] * b[7]) + (a[2] * b[12]) + (a[3] * b[17]); out[3] = (a[0] * b[3]) + (a[1] * b[8]) + (a[2] * b[13]) + (a[3] * b[18]); - out[4] = (a[0] * b[4]) + (a[1] * b[9]) + (a[2] * b[14]) + (a[3] * b[19]); + out[4] = (a[0] * b[4]) + (a[1] * b[9]) + (a[2] * b[14]) + (a[3] * b[19]) + a[4]; // Green Channel out[5] = (a[5] * b[0]) + (a[6] * b[5]) + (a[7] * b[10]) + (a[8] * b[15]); out[6] = (a[5] * b[1]) + (a[6] * b[6]) + (a[7] * b[11]) + (a[8] * b[16]); out[7] = (a[5] * b[2]) + (a[6] * b[7]) + (a[7] * b[12]) + (a[8] * b[17]); out[8] = (a[5] * b[3]) + (a[6] * b[8]) + (a[7] * b[13]) + (a[8] * b[18]); - out[9] = (a[5] * b[4]) + (a[6] * b[9]) + (a[7] * b[14]) + (a[8] * b[19]); + out[9] = (a[5] * b[4]) + (a[6] * b[9]) + (a[7] * b[14]) + (a[8] * b[19]) + a[9]; // Blue Channel out[10] = (a[10] * b[0]) + (a[11] * b[5]) + (a[12] * b[10]) + (a[13] * b[15]); out[11] = (a[10] * b[1]) + (a[11] * b[6]) + (a[12] * b[11]) + (a[13] * b[16]); out[12] = (a[10] * b[2]) + (a[11] * b[7]) + (a[12] * b[12]) + (a[13] * b[17]); out[13] = (a[10] * b[3]) + (a[11] * b[8]) + (a[12] * b[13]) + (a[13] * b[18]); - out[14] = (a[10] * b[4]) + (a[11] * b[9]) + (a[12] * b[14]) + (a[13] * b[19]); + out[14] = (a[10] * b[4]) + (a[11] * b[9]) + (a[12] * b[14]) + (a[13] * b[19]) + a[14]; // Alpha Channel out[15] = (a[15] * b[0]) + (a[16] * b[5]) + (a[17] * b[10]) + (a[18] * b[15]); out[16] = (a[15] * b[1]) + (a[16] * b[6]) + (a[17] * b[11]) + (a[18] * b[16]); out[17] = (a[15] * b[2]) + (a[16] * b[7]) + (a[17] * b[12]) + (a[18] * b[17]); out[18] = (a[15] * b[3]) + (a[16] * b[8]) + (a[17] * b[13]) + (a[18] * b[18]); - out[19] = (a[15] * b[4]) + (a[16] * b[9]) + (a[17] * b[14]) + (a[18] * b[19]); + out[19] = (a[15] * b[4]) + (a[16] * b[9]) + (a[17] * b[14]) + (a[18] * b[19]) + a[19]; return out; } diff --git a/src/filters/colormatrix/colorMatrix.frag b/src/filters/colormatrix/colorMatrix.frag index 3aaf2b3..c73c0e9 100644 --- a/src/filters/colormatrix/colorMatrix.frag +++ b/src/filters/colormatrix/colorMatrix.frag @@ -4,32 +4,38 @@ void main(void) { - vec4 c = texture2D(uSampler, vTextureCoord); + // Un-premultiply alpha before applying the color matrix. See issue #3539. + if (c.a > 0.0) { + c.rgb /= c.a; + } + vec4 result; + result.r = (m[0] * c.r); + result.r += (m[1] * c.g); + result.r += (m[2] * c.b); + result.r += (m[3] * c.a); + result.r += m[4]; - gl_FragColor.r = (m[0] * c.r); - gl_FragColor.r += (m[1] * c.g); - gl_FragColor.r += (m[2] * c.b); - gl_FragColor.r += (m[3] * c.a); - gl_FragColor.r += m[4] * c.a; + result.g = (m[5] * c.r); + result.g += (m[6] * c.g); + result.g += (m[7] * c.b); + result.g += (m[8] * c.a); + result.g += m[9]; - gl_FragColor.g = (m[5] * c.r); - gl_FragColor.g += (m[6] * c.g); - gl_FragColor.g += (m[7] * c.b); - gl_FragColor.g += (m[8] * c.a); - gl_FragColor.g += m[9] * c.a; + result.b = (m[10] * c.r); + result.b += (m[11] * c.g); + result.b += (m[12] * c.b); + result.b += (m[13] * c.a); + result.b += m[14]; - gl_FragColor.b = (m[10] * c.r); - gl_FragColor.b += (m[11] * c.g); - gl_FragColor.b += (m[12] * c.b); - gl_FragColor.b += (m[13] * c.a); - gl_FragColor.b += m[14] * c.a; + result.a = (m[15] * c.r); + result.a += (m[16] * c.g); + result.a += (m[17] * c.b); + result.a += (m[18] * c.a); + result.a += m[19]; - gl_FragColor.a = (m[15] * c.r); - gl_FragColor.a += (m[16] * c.g); - gl_FragColor.a += (m[17] * c.b); - gl_FragColor.a += (m[18] * c.a); - gl_FragColor.a += m[19] * c.a; + // Premultiply alpha again. + result.rgb *= result.a; -// gl_FragColor = vec4(m[0]); + gl_FragColor = result; } diff --git a/src/filters/displacement/DisplacementFilter.js b/src/filters/displacement/DisplacementFilter.js index 112de0b..d5355c3 100644 --- a/src/filters/displacement/DisplacementFilter.js +++ b/src/filters/displacement/DisplacementFilter.js @@ -36,7 +36,7 @@ this.maskMatrix = maskMatrix; this.uniforms.mapSampler = sprite.texture; - this.uniforms.filterMatrix = maskMatrix.toArray(true); + this.uniforms.filterMatrix = maskMatrix; this.uniforms.scale = { x: 1, y: 1 }; if (scale === null || scale === undefined) diff --git a/src/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/InteractionData.js b/src/interaction/InteractionData.js index 02d53a3..818da35 100644 --- a/src/interaction/InteractionData.js +++ b/src/interaction/InteractionData.js @@ -21,9 +21,9 @@ this.global = new core.Point(); /** - * The target Sprite that was interacted with + * The target DisplayObject that was interacted with * - * @member {PIXI.Sprite} + * @member {PIXI.DisplayObject} */ this.target = null; @@ -33,6 +33,13 @@ * @member {Event} */ this.originalEvent = null; + + /** + * Unique identifier for this interaction + * + * @member {number} + */ + this.identifier = null; } /** diff --git a/package.json b/package.json index 1271fef..71ade15 100644 --- a/package.json +++ b/package.json @@ -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/scripts/release.js b/scripts/release.js index c4c8e36..156620c 100644 --- a/scripts/release.js +++ b/scripts/release.js @@ -12,7 +12,8 @@ 'dist/**/*', 'lib/**/*', 'src/**/*', - 'scripts/**/*', + 'scripts/*', + 'scripts/renders/*', 'test/**/*', '*.json', '*.md', diff --git a/scripts/renders/client.js b/scripts/renders/client.js index 16c9565..6faee34 100755 --- a/scripts/renders/client.js +++ b/scripts/renders/client.js @@ -1,6 +1,6 @@ 'use strict'; -require('../../bin/pixi'); +require('../../dist/pixi'); PIXI.utils.skipHello(); const fs = require('fs'); 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 bc730db..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. @@ -33,8 +33,11 @@ * need to call toDataUrl on the webgl context * @param {number} [options.resolution=1] - The resolution / device pixel ratio of the renderer, retina would be 2 * @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 @@ -43,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. */ @@ -73,7 +99,7 @@ */ stop() { - this.ticker.stop(); + this._ticker.stop(); } /** @@ -81,7 +107,7 @@ */ start() { - this.ticker.start(); + this._ticker.start(); } /** @@ -95,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 index 81e076b..78b17e6 100644 --- a/src/core/Shader.js +++ b/src/core/Shader.js @@ -1,9 +1,7 @@ import { GLShader } from 'pixi-gl-core'; import settings from './settings'; -const { PRECISION } = settings; - -function checkPrecision(src) +function checkPrecision(src, def) { if (src instanceof Array) { @@ -11,14 +9,14 @@ { const copy = src.slice(0); - copy.unshift(`precision ${PRECISION} float;`); + copy.unshift(`precision ${def} float;`); return copy; } } else if (src.substring(0, 9) !== 'precision') { - return `precision ${PRECISION} float;\n${src}`; + return `precision ${def} float;\n${src}`; } return src; @@ -42,6 +40,7 @@ */ constructor(gl, vertexSrc, fragmentSrc) { - super(gl, checkPrecision(vertexSrc), checkPrecision(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 3425b41..f9235e4 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -181,7 +181,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 b3792ef..17fde8d 100644 --- a/src/core/display/Container.js +++ b/src/core/display/Container.js @@ -73,13 +73,14 @@ } child.parent = this; - - // ensure a transform will be recalculated.. - this.transform._parentID = -1; - this._boundsID++; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.push(child); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(this.children.length - 1); child.emit('added', this); @@ -108,9 +109,14 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.splice(index, 0, child); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(index); child.emit('added', this); @@ -174,6 +180,7 @@ removeItems(this.children, currentIndex, 1); // remove from old position this.children.splice(index, 0, child); // add at new position + this.onChildrenChange(index); } @@ -220,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! @@ -244,9 +252,14 @@ { const child = this.getChildAt(index); + // ensure child transform will be recalculated.. child.parent = null; + child.transform._parentID = -1; removeItems(this.children, index, 1); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(index); child.emit('removed', this); @@ -275,8 +288,14 @@ for (let i = 0; i < removed.length; ++i) { removed[i].parent = null; + if (removed[i].transform) + { + removed[i].transform._parentID = -1; + } } + this._boundsID++; + this.onChildrenChange(beginIndex); for (let i = 0; i < removed.length; ++i) diff --git a/src/core/display/DisplayObject.js b/src/core/display/DisplayObject.js index 3affe36..4e6c77d 100644 --- a/src/core/display/DisplayObject.js +++ b/src/core/display/DisplayObject.js @@ -109,7 +109,7 @@ /** * The original, cached mask of the object * - * @member {PIXI.Rectangle} + * @member {PIXI.Graphics|PIXI.Sprite} * @private */ this._mask = null; diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index 95f2af4..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 ); @@ -1065,10 +1061,15 @@ canvasRenderer = new CanvasRenderer(); } - tempMatrix.tx = -bounds.x; - tempMatrix.ty = -bounds.y; + this.transform.updateLocalTransform(); + this.transform.localTransform.copy(tempMatrix); - canvasRenderer.render(this, canvasBuffer, false, tempMatrix); + tempMatrix.invert(); + + tempMatrix.tx -= bounds.x; + tempMatrix.ty -= bounds.y; + + canvasRenderer.render(this, canvasBuffer, true, tempMatrix); const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode); 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 e67680f..867dde2 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/buildCircle.js b/src/core/graphics/webgl/utils/buildCircle.js index 0400d13..abf5d02 100644 --- a/src/core/graphics/webgl/utils/buildCircle.js +++ b/src/core/graphics/webgl/utils/buildCircle.js @@ -33,6 +33,11 @@ height = circleData.height; } + if (width === 0 || height === 0) + { + return; + } + const totalSegs = Math.floor(30 * Math.sqrt(circleData.radius)) || Math.floor(15 * Math.sqrt(circleData.width + circleData.height)); diff --git a/src/core/graphics/webgl/utils/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 90155f9..f03fe15 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -30,6 +30,7 @@ export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; export { default as CanvasGraphicsRenderer } from './graphics/canvas/CanvasGraphicsRenderer'; +export { default as Spritesheet } from './textures/Spritesheet'; export { default as Texture } from './textures/Texture'; export { default as BaseTexture } from './textures/BaseTexture'; export { default as RenderTexture } from './textures/RenderTexture'; diff --git a/src/core/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 ccce680..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); } /** @@ -162,6 +162,9 @@ if (transform) { transform.copy(tempWt); + + // lets not forget to flag the parent transform as dirty... + this._tempDisplayObjectParent.transform._worldID = -1; } else { @@ -169,6 +172,7 @@ } displayObject.parent = this._tempDisplayObjectParent; + displayObject.updateTransform(); displayObject.parent = cacheParent; // displayObject.hitArea = //TODO add a temp hit area @@ -279,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/canvas/utils/CanvasRenderTarget.js b/src/core/renderers/canvas/utils/CanvasRenderTarget.js index 3de0aef..b409891 100644 --- a/src/core/renderers/canvas/utils/CanvasRenderTarget.js +++ b/src/core/renderers/canvas/utils/CanvasRenderTarget.js @@ -1,5 +1,4 @@ import settings from '../../../settings'; -const { RESOLUTION } = settings; /** * Creates a Canvas element of the given size. @@ -30,7 +29,7 @@ */ this.context = this.canvas.getContext('2d'); - this.resolution = resolution || RESOLUTION; + this.resolution = resolution || settings.RESOLUTION; this.resize(width, height); } 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 ce0e1c4..788ed6c 100644 --- a/src/core/renderers/webgl/WebGLRenderer.js +++ b/src/core/renderers/webgl/WebGLRenderer.js @@ -30,8 +30,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 @@ -49,10 +49,19 @@ * enable this if you need to call toDataUrl on the webgl context. * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when * rendering, stopping pixel interpolation. + * @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; + + if (this.legacy) + { + glCore.VertexArrayObject.FORCE_NATIVE = true; + } /** * The type of this renderer as a standardised const @@ -229,7 +238,7 @@ this.emit('context', gl); // setup the width/height properties and gl viewport - this.resize(this.width, this.height); + this.resize(this.screen.width, this.screen.height); } /** @@ -323,16 +332,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) { @@ -376,6 +385,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 @@ -440,9 +469,10 @@ * Changes the current shader to the one given in parameter * * @param {PIXI.Shader} shader - the new shader + * @param {boolean} [autoProject=true] - Whether automatically set the projection matrix * @return {PIXI.WebGLRenderer} Returns itself. */ - bindShader(shader) + bindShader(shader, autoProject) { // TODO cache if (this._activeShader !== shader) @@ -450,8 +480,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 bbab3b3..944f41d 100644 --- a/src/core/renderers/webgl/filters/Filter.js +++ b/src/core/renderers/webgl/filters/Filter.js @@ -1,6 +1,7 @@ import extractUniformsFromSrc from './extractUniformsFromSrc'; import { uid } from '../../../utils'; import { BLEND_MODES } from '../../../const'; +import settings from '../../../settings'; const SOURCE_KEY_MAP = {}; @@ -10,7 +11,7 @@ * @memberof PIXI * @extends PIXI.Shader */ -class Filter +export default class Filter { /** * @param {string} [vertexSrc] - The source of the vertex shader. @@ -35,8 +36,6 @@ this.blendMode = BLEND_MODES.NORMAL; - // pull out the vertex and shader uniforms if they are not specified.. - // currently this does not extract structs only default types this.uniformData = uniforms || extractUniformsFromSrc(this.vertexSrc, this.fragmentSrc, 'projectionMatrix|uSampler'); /** @@ -80,7 +79,7 @@ * * @member {number} */ - this.resolution = 1; + this.resolution = settings.RESOLUTION; /** * If enabled is true the filter is applied, if false it will not. @@ -97,8 +96,11 @@ * @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 { // --- // // this.uniforms.filterMatrix = filterManager.calculateSpriteMatrix(tempMatrix, window.panda ); @@ -170,5 +172,3 @@ ].join('\n'); } } - -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 dc7072a..6c7422d 100644 --- a/src/core/renderers/webgl/managers/FilterManager.js +++ b/src/core/renderers/webgl/managers/FilterManager.js @@ -154,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 @@ -176,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; @@ -184,7 +184,7 @@ flop = t; } - filters[i].apply(this, flip, lastState.renderTarget, true); + filters[i].apply(this, flip, lastState.renderTarget, false, currentState); this.freePotRenderTarget(flip); this.freePotRenderTarget(flop); @@ -291,9 +291,13 @@ let textureCount = 1; let currentState; - if (shader.uniforms.data.filterArea) + // 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; @@ -306,9 +310,9 @@ // use this to clamp displaced texture coords so they belong to filterArea // see displacementFilter fragment shader for an example - if (shader.uniforms.data.filterClamp) + if (shader.uniforms.filterClamp) { - currentState = this.filterData.stack[this.filterData.index]; + currentState = currentState || this.filterData.stack[this.filterData.index]; const filterClamp = shader.uniforms.filterClamp; @@ -485,7 +489,7 @@ */ destroy() { - this.shaderCache = []; + this.shaderCache = {}; this.emptyPool(); } diff --git a/src/core/renderers/webgl/utils/RenderTarget.js b/src/core/renderers/webgl/utils/RenderTarget.js index afe51ab..6d2467f 100644 --- a/src/core/renderers/webgl/utils/RenderTarget.js +++ b/src/core/renderers/webgl/utils/RenderTarget.js @@ -124,7 +124,7 @@ * @default PIXI.settings.SCALE_MODE * @see PIXI.SCALE_MODES */ - this.scaleMode = scaleMode || settings.SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; /** * Whether this object is the root element or not 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 36ea5bb..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; @@ -346,8 +346,8 @@ /** * Gets the local bounds of the sprite object. * - * @param {Rectangle} rect - The output rectangle. - * @return {Rectangle} The bounds. + * @param {PIXI.Rectangle} rect - The output rectangle. + * @return {PIXI.Rectangle} The bounds. */ getLocalBounds(rect) { @@ -440,7 +440,7 @@ * * @static * @param {number|string|PIXI.BaseTexture|HTMLCanvasElement|HTMLVideoElement} source Source to create texture from - * @return {PIXI.Texture} 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 e77b39a..90b08c0 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++) @@ -103,11 +102,18 @@ { const gl = this.renderer.gl; - // step 1: first check max textures the GPU can handle. - this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); + if (this.renderer.legacy) + { + this.MAX_TEXTURES = 1; + } + else + { + // step 1: first check max textures the GPU can handle. + this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); - // step 2: check the maximum number of if statements the shader can have too.. - this.MAX_TEXTURES = checkMaxIfStatmentsInShader(this.MAX_TEXTURES, gl); + // step 2: check the maximum number of if statements the shader can have too.. + this.MAX_TEXTURES = checkMaxIfStatmentsInShader(this.MAX_TEXTURES, gl); + } const shader = this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); @@ -130,8 +136,12 @@ .addIndex(this.indexBuffer) .addAttribute(this.vertexBuffers[i], shader.attributes.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) .addAttribute(this.vertexBuffers[i], shader.attributes.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) - .addAttribute(this.vertexBuffers[i], shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4) - .addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + .addAttribute(this.vertexBuffers[i], shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); + + if (shader.attributes.aTextureId) + { + this.vaos[i].addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + } /* eslint-enable max-len */ } @@ -378,8 +388,12 @@ .addIndex(this.indexBuffer) .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) - .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4) - .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); + + if (this.shader.attributes.aTextureId) + { + this.vaos[this.vertexCount].addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + } /* eslint-enable max-len */ } diff --git a/src/core/text/Text.js b/src/core/text/Text.js index 013625a..cec8c32 100644 --- a/src/core/text/Text.js +++ b/src/core/text/Text.js @@ -6,6 +6,7 @@ import { TEXT_GRADIENT } from '../const'; import settings from '../settings'; import TextStyle from './TextStyle'; +import trimCanvas from '../utils/trimCanvas'; const defaultDestroyOptions = { texture: true, @@ -57,7 +58,7 @@ /** * The canvas 2d context that everything is drawn with - * @member {HTMLCanvasElement} + * @member {CanvasRenderingContext2D} */ this.context = this.canvas.getContext('2d'); @@ -189,10 +190,12 @@ if (style.dropShadow) { + this.context.shadowBlur = style.dropShadowBlur; + this.context.globalAlpha = style.dropShadowAlpha; + if (style.dropShadowBlur > 0) { this.context.shadowColor = style.dropShadowColor; - this.context.shadowBlur = style.dropShadowBlur; } else { @@ -237,6 +240,10 @@ } } + // reset the shadow blur and alpha that was set by the drop shadow, for the regular text + this.context.shadowBlur = 0; + this.context.globalAlpha = 1; + // set canvas text styles this.context.fillStyle = this._generateFillStyle(style, lines); @@ -326,6 +333,15 @@ */ updateTexture() { + if (this._style.trim) + { + const trimmed = trimCanvas(this.canvas); + + this.canvas.width = trimmed.width; + this.canvas.height = trimmed.height; + this.context.putImageData(trimmed.data, 0, 0); + } + const texture = this._texture; const style = this._style; @@ -475,6 +491,19 @@ } /** + * Gets the local bounds of the text object. + * + * @param {Rectangle} rect - The output rectangle. + * @return {Rectangle} The bounds. + */ + getLocalBounds(rect) + { + this.updateText(true); + + return super.getLocalBounds.call(this, rect); + } + + /** * calculates the bounds of the Text as a rectangle. The bounds calculation takes the worldTransform into account. */ _calculateBounds() @@ -525,6 +554,29 @@ const width = this.canvas.width / this.resolution; const height = this.canvas.height / this.resolution; + // make a copy of the style settings, so we can manipulate them later + const fill = style.fill.slice(); + const fillGradientStops = style.fillGradientStops.slice(); + + // wanting to evenly distribute the fills. So an array of 4 colours should give fills of 0.25, 0.5 and 0.75 + if (!fillGradientStops.length) + { + const lengthPlus1 = fill.length + 1; + + for (let i = 1; i < lengthPlus1; ++i) + { + fillGradientStops.push(i / lengthPlus1); + } + } + + // stop the bleeding of the last gradient on the line above to the top gradient of the this line + // by hard defining the first gradient colour at point 0, and last gradient colour at point 1 + fill.unshift(style.fill[0]); + fillGradientStops.unshift(0); + + fill.push(style.fill[style.fill.length - 1]); + fillGradientStops.push(1); + if (style.fillGradientType === TEXT_GRADIENT.LINEAR_VERTICAL) { // start the gradient at the top center of the canvas, and end at the bottom middle of the canvas @@ -532,15 +584,22 @@ // we need to repeat the gradient so that each individual line of text has the same vertical gradient effect // ['#FF0000', '#00FF00', '#0000FF'] over 2 lines would create stops at 0.125, 0.25, 0.375, 0.625, 0.75, 0.875 - totalIterations = (style.fill.length + 1) * lines.length; + totalIterations = (fill.length + 1) * lines.length; currentIteration = 0; for (let i = 0; i < lines.length; i++) { currentIteration += 1; - for (let j = 0; j < style.fill.length; j++) + for (let j = 0; j < fill.length; j++) { - stop = (currentIteration / totalIterations); - gradient.addColorStop(stop, style.fill[j]); + if (fillGradientStops[j]) + { + stop = (fillGradientStops[j] / lines.length) + (i / lines.length); + } + else + { + stop = currentIteration / totalIterations; + } + gradient.addColorStop(stop, fill[j]); currentIteration++; } } @@ -552,13 +611,20 @@ // can just evenly space out the gradients in this case, as multiple lines makes no difference // to an even left to right gradient - totalIterations = style.fill.length + 1; + totalIterations = fill.length + 1; currentIteration = 1; - for (let i = 0; i < style.fill.length; i++) + for (let i = 0; i < fill.length; i++) { - stop = currentIteration / totalIterations; - gradient.addColorStop(stop, style.fill[i]); + if (fillGradientStops[i]) + { + stop = fillGradientStops[i]; + } + else + { + stop = currentIteration / totalIterations; + } + gradient.addColorStop(stop, fill[i]); currentIteration++; } } @@ -569,7 +635,7 @@ /** * Destroys this text object. * Note* Unlike a Sprite, a Text object will automatically destroy its baseTexture and texture as - * the majorety of the time the texture will not be shared with any other Sprites. + * the majority of the time the texture will not be shared with any other Sprites. * * @param {object|boolean} [options] - Options parameter. A boolean will act as if all options * have been set to that value @@ -680,7 +746,7 @@ set text(text) // eslint-disable-line require-jsdoc { - text = String(text || ' '); + text = String(text === '' || text === null || text === undefined ? ' ' : text); if (this._text === text) { diff --git a/src/core/text/TextStyle.js b/src/core/text/TextStyle.js index 3b25528..890950b 100644 --- a/src/core/text/TextStyle.js +++ b/src/core/text/TextStyle.js @@ -8,12 +8,14 @@ align: 'left', breakWords: false, dropShadow: false, + dropShadowAlpha: 1, dropShadowAngle: Math.PI / 6, dropShadowBlur: 0, dropShadowColor: '#000000', dropShadowDistance: 5, fill: 'black', fillGradientType: TEXT_GRADIENT.LINEAR_VERTICAL, + fillGradientStops: [], fontFamily: 'Arial', fontSize: 26, fontStyle: 'normal', @@ -27,6 +29,7 @@ stroke: 'black', strokeThickness: 0, textBaseline: 'alphabetic', + trim: false, wordWrap: false, wordWrapWidth: 100, }; @@ -47,6 +50,7 @@ * @param {boolean} [style.breakWords=false] - Indicates if lines can be wrapped within words, it * needs wordWrap to be set to true * @param {boolean} [style.dropShadow=false] - Set a drop shadow for the text + * @param {number} [style.dropShadowAlpha=1] - Set alpha for the drop shadow * @param {number} [style.dropShadowAngle=Math.PI/6] - Set a angle of the drop shadow * @param {number} [style.dropShadowBlur=0] - Set a shadow blur radius * @param {string} [style.dropShadowColor='#000000'] - A fill style to be used on the dropshadow e.g 'red', '#00FF00' @@ -55,8 +59,10 @@ * fillstyle that will be used on the text e.g 'red', '#00FF00'. Can be an array to create a gradient * eg ['#000000','#FFFFFF'] * {@link https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/fillStyle|MDN} - * @param {number} [style.fillGradientType=PIXI.TEXT_GRADIENT.LINEAR_VERTICAL] - If fills styles are - * supplied, this can change the type/direction of the gradient. See {@link PIXI.TEXT_GRADIENT} for possible values + * @param {number} [style.fillGradientType=PIXI.TEXT_GRADIENT.LINEAR_VERTICAL] - If fill is an array of colours + * to create a gradient, this can change the type/direction of the gradient. See {@link PIXI.TEXT_GRADIENT} + * @param {number[]} [style.fillGradientStops] - If fill is an array of colours to create a gradient, this array can set + * the stop points (numbers between 0 and 1) for the color, overriding the default behaviour of evenly spacing them. * @param {string|string[]} [style.fontFamily='Arial'] - The font family * @param {number|string} [style.fontSize=26] - The font size (as a number it converts to px, but as a string, * equivalents are '26px','20pt','160%' or '1.6em') @@ -76,6 +82,7 @@ * e.g 'blue', '#FCFF00' * @param {number} [style.strokeThickness=0] - A number that represents the thickness of the stroke. * Default is 0 (no stroke) + * @param {boolean} [style.trim=false] - Trim transparent borders * @param {string} [style.textBaseline='alphabetic'] - The baseline of the text that is rendered. * @param {boolean} [style.wordWrap=false] - Indicates if word wrap should be used * @param {number} [style.wordWrapWidth=100] - The width at which text will wrap, it needs wordWrap to be set to true @@ -152,6 +159,19 @@ } } + get dropShadowAlpha() + { + return this._dropShadowAlpha; + } + set dropShadowAlpha(dropShadowAlpha) + { + if (this._dropShadowAlpha !== dropShadowAlpha) + { + this._dropShadowAlpha = dropShadowAlpha; + this.styleID++; + } + } + get dropShadowAngle() { return this._dropShadowAngle; @@ -232,6 +252,19 @@ } } + get fillGradientStops() + { + return this._fillGradientStops; + } + set fillGradientStops(fillGradientStops) + { + if (!areArraysEqual(this._fillGradientStops,fillGradientStops)) + { + this._fillGradientStops = fillGradientStops; + this.styleID++; + } + } + get fontFamily() { return this._fontFamily; @@ -402,6 +435,19 @@ } } + get trim() + { + return this._trim; + } + set trim(trim) + { + if (this._trim !== trim) + { + this._trim = trim; + this.styleID++; + } + } + get wordWrap() { return this._wordWrap; @@ -475,3 +521,34 @@ return color; } } + +/** + * Utility function to convert hexadecimal colors to strings, and simply return the color if it's a string. + * This version can also convert array of colors + * + * @param {Array} array1 First array to compare + * @param {Array} array2 Second array to compare + * @return {boolean} Do the arrays contain the same values in the same order + */ +function areArraysEqual(array1, array2) +{ + if (!Array.isArray(array1) || !Array.isArray(array2)) + { + return false; + } + + if (array1.length !== array2.length) + { + return false; + } + + for (let i = 0; i < array1.length; ++i) + { + if (array1[i] !== array2[i]) + { + return false; + } + } + + return true; +} diff --git a/src/core/textures/BaseRenderTexture.js b/src/core/textures/BaseRenderTexture.js index 5d50146..aa727f0 100644 --- a/src/core/textures/BaseRenderTexture.js +++ b/src/core/textures/BaseRenderTexture.js @@ -1,8 +1,6 @@ import BaseTexture from './BaseTexture'; import settings from '../settings'; -const { RESOLUTION, SCALE_MODE } = settings; - /** * A BaseRenderTexture is a special texture that allows any Pixi display object to be rendered to it. * @@ -54,7 +52,7 @@ { super(null, scaleMode); - this.resolution = resolution || RESOLUTION; + this.resolution = resolution || settings.RESOLUTION; this.width = width; this.height = height; @@ -62,7 +60,7 @@ this.realWidth = this.width * this.resolution; this.realHeight = this.height * this.resolution; - this.scaleMode = scaleMode || SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; this.hasLoaded = true; /** diff --git a/src/core/textures/BaseTexture.js b/src/core/textures/BaseTexture.js index 676dca3..aeccf39 100644 --- a/src/core/textures/BaseTexture.js +++ b/src/core/textures/BaseTexture.js @@ -77,7 +77,7 @@ * @default PIXI.settings.SCALE_MODE * @see PIXI.SCALE_MODES */ - this.scaleMode = scaleMode || settings.SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; /** * Set to true once the base texture has successfully loaded. @@ -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/RenderTexture.js b/src/core/textures/RenderTexture.js index 4c5034f..b3c6297 100644 --- a/src/core/textures/RenderTexture.js +++ b/src/core/textures/RenderTexture.js @@ -54,8 +54,8 @@ /* eslint-disable prefer-rest-params, no-console */ const width = arguments[1]; const height = arguments[2]; - const scaleMode = arguments[3] || 0; - const resolution = arguments[4] || 1; + const scaleMode = arguments[3]; + const resolution = arguments[4]; // we have an old render texture.. console.warn(`Please use RenderTexture.create(${width}, ${height}) instead of the ctor directly.`); 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 ca31456..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 @@ -459,7 +497,9 @@ if (frame.x + frame.width > this.baseTexture.width || frame.y + frame.height > this.baseTexture.height) { - throw new Error(`Texture Error: frame does not fit inside the base Texture dimensions ${this}`); + throw new Error('Texture Error: frame does not fit inside the base Texture dimensions: ' + + `X: ${frame.x} + ${frame.width} > ${this.baseTexture.width} ` + + `Y: ${frame.y} + ${frame.height} > ${this.baseTexture.height}`); } // this.valid = frame && frame.width && frame.height && this.baseTexture.source && this.baseTexture.hasLoaded; @@ -520,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. @@ -528,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 2a7497b..99e585a 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)); } /** @@ -360,3 +362,43 @@ * @private */ export const BaseTextureCache = {}; + +/** + * 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/core/utils/trimCanvas.js b/src/core/utils/trimCanvas.js new file mode 100644 index 0000000..5af6de0 --- /dev/null +++ b/src/core/utils/trimCanvas.js @@ -0,0 +1,83 @@ +/** + * Trim transparent borders from a canvas + * + * @memberof PIXI + * @function trimCanvas + * @private + * @param {HTMLCanvasElement} canvas - the canvas to trim + * @returns {object} Trim data + */ +export default function trimCanvas(canvas) +{ + // https://gist.github.com/remy/784508 + + let width = canvas.width; + let height = canvas.height; + + const context = canvas.getContext('2d'); + const imageData = context.getImageData(0, 0, width, height); + const pixels = imageData.data; + const len = pixels.length; + + const bound = { + top: null, + left: null, + right: null, + bottom: null, + }; + let i; + let x; + let y; + + for (i = 0; i < len; i += 4) + { + if (pixels[i + 3] !== 0) + { + x = (i / 4) % width; + y = ~~((i / 4) / width); + + if (bound.top === null) + { + bound.top = y; + } + + if (bound.left === null) + { + bound.left = x; + } + else if (x < bound.left) + { + bound.left = x; + } + + if (bound.right === null) + { + bound.right = x + 1; + } + else if (bound.right < x) + { + bound.right = x + 1; + } + + if (bound.bottom === null) + { + bound.bottom = y; + } + else if (bound.bottom < y) + { + bound.bottom = y; + } + } + } + + width = bound.right - bound.left; + height = bound.bottom - bound.top + 1; + + const data = context.getImageData(bound.left, bound.top, width, height); + + return { + height, + width, + data, + }; +} diff --git a/src/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/extras/TilingSprite.js b/src/extras/TilingSprite.js index f90d248..fe82451 100644 --- a/src/extras/TilingSprite.js +++ b/src/extras/TilingSprite.js @@ -220,20 +220,33 @@ transform.tx * resolution, transform.ty * resolution); - // TODO - this should be rolled into the setTransform above.. - context.scale(this.tileScale.x / baseTextureResolution, this.tileScale.y / baseTextureResolution); - - context.translate(modX + (this.anchor.x * -this._width), - modY + (this.anchor.y * -this._height)); - renderer.setBlendMode(this.blendMode); // fill the pattern! context.fillStyle = this._canvasPattern; - context.fillRect(-modX, - -modY, - this._width / this.tileScale.x * baseTextureResolution, - this._height / this.tileScale.y * baseTextureResolution); + + // TODO - this should be rolled into the setTransform above.. + context.scale(this.tileScale.x / baseTextureResolution, this.tileScale.y / baseTextureResolution); + + const anchorX = this.anchor.x * -this._width; + const anchorY = this.anchor.y * -this._height; + + if (this.uvRespectAnchor) + { + context.translate(modX, modY); + + context.fillRect(-modX + anchorX, -modY + anchorY, + this._width / this.tileScale.x * baseTextureResolution, + this._height / this.tileScale.y * baseTextureResolution); + } + else + { + context.translate(modX + anchorX, modY + anchorY); + + context.fillRect(-modX, -modY, + this._width / this.tileScale.x * baseTextureResolution, + this._height / this.tileScale.y * baseTextureResolution); + } } /** diff --git a/src/extras/cacheAsBitmap.js b/src/extras/cacheAsBitmap.js index 18043a7..5dd5640 100644 --- a/src/extras/cacheAsBitmap.js +++ b/src/extras/cacheAsBitmap.js @@ -41,6 +41,9 @@ * provide a performance benefit for complex static displayObjects. * To remove simply set this property to 'false' * + * IMPORTANT GOTCHA - make sure that all your textures are preloaded BEFORE setting this property to true + * as it will take a snapshot of what is currently there. If the textures have not loaded then they will not appear. + * * @member {boolean} * @memberof PIXI.DisplayObject# */ diff --git a/src/filters/blur/BlurFilter.js b/src/filters/blur/BlurFilter.js index 340fc58..1575db9 100644 --- a/src/filters/blur/BlurFilter.js +++ b/src/filters/blur/BlurFilter.js @@ -24,10 +24,9 @@ this.blurXFilter = new BlurXFilter(strength, quality, resolution, kernelSize); this.blurYFilter = new BlurYFilter(strength, quality, resolution, kernelSize); - this.resolution = 1; this.padding = 0; - this.resolution = resolution || 1; + this.resolution = resolution || core.settings.RESOLUTION; this.quality = quality || 4; this.blur = strength || 8; } diff --git a/src/filters/blur/BlurXFilter.js b/src/filters/blur/BlurXFilter.js index def99c3..0f48889 100644 --- a/src/filters/blur/BlurXFilter.js +++ b/src/filters/blur/BlurXFilter.js @@ -31,7 +31,7 @@ fragSrc ); - this.resolution = resolution || 1; + this.resolution = resolution || core.settings.RESOLUTION; this._quality = 0; diff --git a/src/filters/blur/BlurYFilter.js b/src/filters/blur/BlurYFilter.js index 638e0f0..6d489c7 100644 --- a/src/filters/blur/BlurYFilter.js +++ b/src/filters/blur/BlurYFilter.js @@ -31,7 +31,7 @@ fragSrc ); - this.resolution = resolution || 1; + this.resolution = resolution || core.settings.RESOLUTION; this._quality = 0; diff --git a/src/filters/colormatrix/ColorMatrixFilter.js b/src/filters/colormatrix/ColorMatrixFilter.js index 8509505..679f8ec 100644 --- a/src/filters/colormatrix/ColorMatrixFilter.js +++ b/src/filters/colormatrix/ColorMatrixFilter.js @@ -75,28 +75,28 @@ out[1] = (a[0] * b[1]) + (a[1] * b[6]) + (a[2] * b[11]) + (a[3] * b[16]); out[2] = (a[0] * b[2]) + (a[1] * b[7]) + (a[2] * b[12]) + (a[3] * b[17]); out[3] = (a[0] * b[3]) + (a[1] * b[8]) + (a[2] * b[13]) + (a[3] * b[18]); - out[4] = (a[0] * b[4]) + (a[1] * b[9]) + (a[2] * b[14]) + (a[3] * b[19]); + out[4] = (a[0] * b[4]) + (a[1] * b[9]) + (a[2] * b[14]) + (a[3] * b[19]) + a[4]; // Green Channel out[5] = (a[5] * b[0]) + (a[6] * b[5]) + (a[7] * b[10]) + (a[8] * b[15]); out[6] = (a[5] * b[1]) + (a[6] * b[6]) + (a[7] * b[11]) + (a[8] * b[16]); out[7] = (a[5] * b[2]) + (a[6] * b[7]) + (a[7] * b[12]) + (a[8] * b[17]); out[8] = (a[5] * b[3]) + (a[6] * b[8]) + (a[7] * b[13]) + (a[8] * b[18]); - out[9] = (a[5] * b[4]) + (a[6] * b[9]) + (a[7] * b[14]) + (a[8] * b[19]); + out[9] = (a[5] * b[4]) + (a[6] * b[9]) + (a[7] * b[14]) + (a[8] * b[19]) + a[9]; // Blue Channel out[10] = (a[10] * b[0]) + (a[11] * b[5]) + (a[12] * b[10]) + (a[13] * b[15]); out[11] = (a[10] * b[1]) + (a[11] * b[6]) + (a[12] * b[11]) + (a[13] * b[16]); out[12] = (a[10] * b[2]) + (a[11] * b[7]) + (a[12] * b[12]) + (a[13] * b[17]); out[13] = (a[10] * b[3]) + (a[11] * b[8]) + (a[12] * b[13]) + (a[13] * b[18]); - out[14] = (a[10] * b[4]) + (a[11] * b[9]) + (a[12] * b[14]) + (a[13] * b[19]); + out[14] = (a[10] * b[4]) + (a[11] * b[9]) + (a[12] * b[14]) + (a[13] * b[19]) + a[14]; // Alpha Channel out[15] = (a[15] * b[0]) + (a[16] * b[5]) + (a[17] * b[10]) + (a[18] * b[15]); out[16] = (a[15] * b[1]) + (a[16] * b[6]) + (a[17] * b[11]) + (a[18] * b[16]); out[17] = (a[15] * b[2]) + (a[16] * b[7]) + (a[17] * b[12]) + (a[18] * b[17]); out[18] = (a[15] * b[3]) + (a[16] * b[8]) + (a[17] * b[13]) + (a[18] * b[18]); - out[19] = (a[15] * b[4]) + (a[16] * b[9]) + (a[17] * b[14]) + (a[18] * b[19]); + out[19] = (a[15] * b[4]) + (a[16] * b[9]) + (a[17] * b[14]) + (a[18] * b[19]) + a[19]; return out; } diff --git a/src/filters/colormatrix/colorMatrix.frag b/src/filters/colormatrix/colorMatrix.frag index 3aaf2b3..c73c0e9 100644 --- a/src/filters/colormatrix/colorMatrix.frag +++ b/src/filters/colormatrix/colorMatrix.frag @@ -4,32 +4,38 @@ void main(void) { - vec4 c = texture2D(uSampler, vTextureCoord); + // Un-premultiply alpha before applying the color matrix. See issue #3539. + if (c.a > 0.0) { + c.rgb /= c.a; + } + vec4 result; + result.r = (m[0] * c.r); + result.r += (m[1] * c.g); + result.r += (m[2] * c.b); + result.r += (m[3] * c.a); + result.r += m[4]; - gl_FragColor.r = (m[0] * c.r); - gl_FragColor.r += (m[1] * c.g); - gl_FragColor.r += (m[2] * c.b); - gl_FragColor.r += (m[3] * c.a); - gl_FragColor.r += m[4] * c.a; + result.g = (m[5] * c.r); + result.g += (m[6] * c.g); + result.g += (m[7] * c.b); + result.g += (m[8] * c.a); + result.g += m[9]; - gl_FragColor.g = (m[5] * c.r); - gl_FragColor.g += (m[6] * c.g); - gl_FragColor.g += (m[7] * c.b); - gl_FragColor.g += (m[8] * c.a); - gl_FragColor.g += m[9] * c.a; + result.b = (m[10] * c.r); + result.b += (m[11] * c.g); + result.b += (m[12] * c.b); + result.b += (m[13] * c.a); + result.b += m[14]; - gl_FragColor.b = (m[10] * c.r); - gl_FragColor.b += (m[11] * c.g); - gl_FragColor.b += (m[12] * c.b); - gl_FragColor.b += (m[13] * c.a); - gl_FragColor.b += m[14] * c.a; + result.a = (m[15] * c.r); + result.a += (m[16] * c.g); + result.a += (m[17] * c.b); + result.a += (m[18] * c.a); + result.a += m[19]; - gl_FragColor.a = (m[15] * c.r); - gl_FragColor.a += (m[16] * c.g); - gl_FragColor.a += (m[17] * c.b); - gl_FragColor.a += (m[18] * c.a); - gl_FragColor.a += m[19] * c.a; + // Premultiply alpha again. + result.rgb *= result.a; -// gl_FragColor = vec4(m[0]); + gl_FragColor = result; } diff --git a/src/filters/displacement/DisplacementFilter.js b/src/filters/displacement/DisplacementFilter.js index 112de0b..d5355c3 100644 --- a/src/filters/displacement/DisplacementFilter.js +++ b/src/filters/displacement/DisplacementFilter.js @@ -36,7 +36,7 @@ this.maskMatrix = maskMatrix; this.uniforms.mapSampler = sprite.texture; - this.uniforms.filterMatrix = maskMatrix.toArray(true); + this.uniforms.filterMatrix = maskMatrix; this.uniforms.scale = { x: 1, y: 1 }; if (scale === null || scale === undefined) diff --git a/src/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/InteractionData.js b/src/interaction/InteractionData.js index 02d53a3..818da35 100644 --- a/src/interaction/InteractionData.js +++ b/src/interaction/InteractionData.js @@ -21,9 +21,9 @@ this.global = new core.Point(); /** - * The target Sprite that was interacted with + * The target DisplayObject that was interacted with * - * @member {PIXI.Sprite} + * @member {PIXI.DisplayObject} */ this.target = null; @@ -33,6 +33,13 @@ * @member {Event} */ this.originalEvent = null; + + /** + * Unique identifier for this interaction + * + * @member {number} + */ + this.identifier = null; } /** diff --git a/src/interaction/InteractionEvent.js b/src/interaction/InteractionEvent.js index 75b2966..dc554f9 100644 --- a/src/interaction/InteractionEvent.js +++ b/src/interaction/InteractionEvent.js @@ -58,7 +58,7 @@ } /** - * Prevents event from reaching any objects other than the current object. + * Resets the event. * * @private */ diff --git a/package.json b/package.json index 1271fef..71ade15 100644 --- a/package.json +++ b/package.json @@ -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/scripts/release.js b/scripts/release.js index c4c8e36..156620c 100644 --- a/scripts/release.js +++ b/scripts/release.js @@ -12,7 +12,8 @@ 'dist/**/*', 'lib/**/*', 'src/**/*', - 'scripts/**/*', + 'scripts/*', + 'scripts/renders/*', 'test/**/*', '*.json', '*.md', diff --git a/scripts/renders/client.js b/scripts/renders/client.js index 16c9565..6faee34 100755 --- a/scripts/renders/client.js +++ b/scripts/renders/client.js @@ -1,6 +1,6 @@ 'use strict'; -require('../../bin/pixi'); +require('../../dist/pixi'); PIXI.utils.skipHello(); const fs = require('fs'); 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 bc730db..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. @@ -33,8 +33,11 @@ * need to call toDataUrl on the webgl context * @param {number} [options.resolution=1] - The resolution / device pixel ratio of the renderer, retina would be 2 * @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 @@ -43,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. */ @@ -73,7 +99,7 @@ */ stop() { - this.ticker.stop(); + this._ticker.stop(); } /** @@ -81,7 +107,7 @@ */ start() { - this.ticker.start(); + this._ticker.start(); } /** @@ -95,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 index 81e076b..78b17e6 100644 --- a/src/core/Shader.js +++ b/src/core/Shader.js @@ -1,9 +1,7 @@ import { GLShader } from 'pixi-gl-core'; import settings from './settings'; -const { PRECISION } = settings; - -function checkPrecision(src) +function checkPrecision(src, def) { if (src instanceof Array) { @@ -11,14 +9,14 @@ { const copy = src.slice(0); - copy.unshift(`precision ${PRECISION} float;`); + copy.unshift(`precision ${def} float;`); return copy; } } else if (src.substring(0, 9) !== 'precision') { - return `precision ${PRECISION} float;\n${src}`; + return `precision ${def} float;\n${src}`; } return src; @@ -42,6 +40,7 @@ */ constructor(gl, vertexSrc, fragmentSrc) { - super(gl, checkPrecision(vertexSrc), checkPrecision(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 3425b41..f9235e4 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -181,7 +181,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 b3792ef..17fde8d 100644 --- a/src/core/display/Container.js +++ b/src/core/display/Container.js @@ -73,13 +73,14 @@ } child.parent = this; - - // ensure a transform will be recalculated.. - this.transform._parentID = -1; - this._boundsID++; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.push(child); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(this.children.length - 1); child.emit('added', this); @@ -108,9 +109,14 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.splice(index, 0, child); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(index); child.emit('added', this); @@ -174,6 +180,7 @@ removeItems(this.children, currentIndex, 1); // remove from old position this.children.splice(index, 0, child); // add at new position + this.onChildrenChange(index); } @@ -220,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! @@ -244,9 +252,14 @@ { const child = this.getChildAt(index); + // ensure child transform will be recalculated.. child.parent = null; + child.transform._parentID = -1; removeItems(this.children, index, 1); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(index); child.emit('removed', this); @@ -275,8 +288,14 @@ for (let i = 0; i < removed.length; ++i) { removed[i].parent = null; + if (removed[i].transform) + { + removed[i].transform._parentID = -1; + } } + this._boundsID++; + this.onChildrenChange(beginIndex); for (let i = 0; i < removed.length; ++i) diff --git a/src/core/display/DisplayObject.js b/src/core/display/DisplayObject.js index 3affe36..4e6c77d 100644 --- a/src/core/display/DisplayObject.js +++ b/src/core/display/DisplayObject.js @@ -109,7 +109,7 @@ /** * The original, cached mask of the object * - * @member {PIXI.Rectangle} + * @member {PIXI.Graphics|PIXI.Sprite} * @private */ this._mask = null; diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index 95f2af4..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 ); @@ -1065,10 +1061,15 @@ canvasRenderer = new CanvasRenderer(); } - tempMatrix.tx = -bounds.x; - tempMatrix.ty = -bounds.y; + this.transform.updateLocalTransform(); + this.transform.localTransform.copy(tempMatrix); - canvasRenderer.render(this, canvasBuffer, false, tempMatrix); + tempMatrix.invert(); + + tempMatrix.tx -= bounds.x; + tempMatrix.ty -= bounds.y; + + canvasRenderer.render(this, canvasBuffer, true, tempMatrix); const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode); 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 e67680f..867dde2 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/buildCircle.js b/src/core/graphics/webgl/utils/buildCircle.js index 0400d13..abf5d02 100644 --- a/src/core/graphics/webgl/utils/buildCircle.js +++ b/src/core/graphics/webgl/utils/buildCircle.js @@ -33,6 +33,11 @@ height = circleData.height; } + if (width === 0 || height === 0) + { + return; + } + const totalSegs = Math.floor(30 * Math.sqrt(circleData.radius)) || Math.floor(15 * Math.sqrt(circleData.width + circleData.height)); diff --git a/src/core/graphics/webgl/utils/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 90155f9..f03fe15 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -30,6 +30,7 @@ export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; export { default as CanvasGraphicsRenderer } from './graphics/canvas/CanvasGraphicsRenderer'; +export { default as Spritesheet } from './textures/Spritesheet'; export { default as Texture } from './textures/Texture'; export { default as BaseTexture } from './textures/BaseTexture'; export { default as RenderTexture } from './textures/RenderTexture'; diff --git a/src/core/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 ccce680..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); } /** @@ -162,6 +162,9 @@ if (transform) { transform.copy(tempWt); + + // lets not forget to flag the parent transform as dirty... + this._tempDisplayObjectParent.transform._worldID = -1; } else { @@ -169,6 +172,7 @@ } displayObject.parent = this._tempDisplayObjectParent; + displayObject.updateTransform(); displayObject.parent = cacheParent; // displayObject.hitArea = //TODO add a temp hit area @@ -279,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/canvas/utils/CanvasRenderTarget.js b/src/core/renderers/canvas/utils/CanvasRenderTarget.js index 3de0aef..b409891 100644 --- a/src/core/renderers/canvas/utils/CanvasRenderTarget.js +++ b/src/core/renderers/canvas/utils/CanvasRenderTarget.js @@ -1,5 +1,4 @@ import settings from '../../../settings'; -const { RESOLUTION } = settings; /** * Creates a Canvas element of the given size. @@ -30,7 +29,7 @@ */ this.context = this.canvas.getContext('2d'); - this.resolution = resolution || RESOLUTION; + this.resolution = resolution || settings.RESOLUTION; this.resize(width, height); } 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 ce0e1c4..788ed6c 100644 --- a/src/core/renderers/webgl/WebGLRenderer.js +++ b/src/core/renderers/webgl/WebGLRenderer.js @@ -30,8 +30,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 @@ -49,10 +49,19 @@ * enable this if you need to call toDataUrl on the webgl context. * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when * rendering, stopping pixel interpolation. + * @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; + + if (this.legacy) + { + glCore.VertexArrayObject.FORCE_NATIVE = true; + } /** * The type of this renderer as a standardised const @@ -229,7 +238,7 @@ this.emit('context', gl); // setup the width/height properties and gl viewport - this.resize(this.width, this.height); + this.resize(this.screen.width, this.screen.height); } /** @@ -323,16 +332,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) { @@ -376,6 +385,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 @@ -440,9 +469,10 @@ * Changes the current shader to the one given in parameter * * @param {PIXI.Shader} shader - the new shader + * @param {boolean} [autoProject=true] - Whether automatically set the projection matrix * @return {PIXI.WebGLRenderer} Returns itself. */ - bindShader(shader) + bindShader(shader, autoProject) { // TODO cache if (this._activeShader !== shader) @@ -450,8 +480,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 bbab3b3..944f41d 100644 --- a/src/core/renderers/webgl/filters/Filter.js +++ b/src/core/renderers/webgl/filters/Filter.js @@ -1,6 +1,7 @@ import extractUniformsFromSrc from './extractUniformsFromSrc'; import { uid } from '../../../utils'; import { BLEND_MODES } from '../../../const'; +import settings from '../../../settings'; const SOURCE_KEY_MAP = {}; @@ -10,7 +11,7 @@ * @memberof PIXI * @extends PIXI.Shader */ -class Filter +export default class Filter { /** * @param {string} [vertexSrc] - The source of the vertex shader. @@ -35,8 +36,6 @@ this.blendMode = BLEND_MODES.NORMAL; - // pull out the vertex and shader uniforms if they are not specified.. - // currently this does not extract structs only default types this.uniformData = uniforms || extractUniformsFromSrc(this.vertexSrc, this.fragmentSrc, 'projectionMatrix|uSampler'); /** @@ -80,7 +79,7 @@ * * @member {number} */ - this.resolution = 1; + this.resolution = settings.RESOLUTION; /** * If enabled is true the filter is applied, if false it will not. @@ -97,8 +96,11 @@ * @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 { // --- // // this.uniforms.filterMatrix = filterManager.calculateSpriteMatrix(tempMatrix, window.panda ); @@ -170,5 +172,3 @@ ].join('\n'); } } - -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 dc7072a..6c7422d 100644 --- a/src/core/renderers/webgl/managers/FilterManager.js +++ b/src/core/renderers/webgl/managers/FilterManager.js @@ -154,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 @@ -176,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; @@ -184,7 +184,7 @@ flop = t; } - filters[i].apply(this, flip, lastState.renderTarget, true); + filters[i].apply(this, flip, lastState.renderTarget, false, currentState); this.freePotRenderTarget(flip); this.freePotRenderTarget(flop); @@ -291,9 +291,13 @@ let textureCount = 1; let currentState; - if (shader.uniforms.data.filterArea) + // 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; @@ -306,9 +310,9 @@ // use this to clamp displaced texture coords so they belong to filterArea // see displacementFilter fragment shader for an example - if (shader.uniforms.data.filterClamp) + if (shader.uniforms.filterClamp) { - currentState = this.filterData.stack[this.filterData.index]; + currentState = currentState || this.filterData.stack[this.filterData.index]; const filterClamp = shader.uniforms.filterClamp; @@ -485,7 +489,7 @@ */ destroy() { - this.shaderCache = []; + this.shaderCache = {}; this.emptyPool(); } diff --git a/src/core/renderers/webgl/utils/RenderTarget.js b/src/core/renderers/webgl/utils/RenderTarget.js index afe51ab..6d2467f 100644 --- a/src/core/renderers/webgl/utils/RenderTarget.js +++ b/src/core/renderers/webgl/utils/RenderTarget.js @@ -124,7 +124,7 @@ * @default PIXI.settings.SCALE_MODE * @see PIXI.SCALE_MODES */ - this.scaleMode = scaleMode || settings.SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; /** * Whether this object is the root element or not 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 36ea5bb..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; @@ -346,8 +346,8 @@ /** * Gets the local bounds of the sprite object. * - * @param {Rectangle} rect - The output rectangle. - * @return {Rectangle} The bounds. + * @param {PIXI.Rectangle} rect - The output rectangle. + * @return {PIXI.Rectangle} The bounds. */ getLocalBounds(rect) { @@ -440,7 +440,7 @@ * * @static * @param {number|string|PIXI.BaseTexture|HTMLCanvasElement|HTMLVideoElement} source Source to create texture from - * @return {PIXI.Texture} 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 e77b39a..90b08c0 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++) @@ -103,11 +102,18 @@ { const gl = this.renderer.gl; - // step 1: first check max textures the GPU can handle. - this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); + if (this.renderer.legacy) + { + this.MAX_TEXTURES = 1; + } + else + { + // step 1: first check max textures the GPU can handle. + this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); - // step 2: check the maximum number of if statements the shader can have too.. - this.MAX_TEXTURES = checkMaxIfStatmentsInShader(this.MAX_TEXTURES, gl); + // step 2: check the maximum number of if statements the shader can have too.. + this.MAX_TEXTURES = checkMaxIfStatmentsInShader(this.MAX_TEXTURES, gl); + } const shader = this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); @@ -130,8 +136,12 @@ .addIndex(this.indexBuffer) .addAttribute(this.vertexBuffers[i], shader.attributes.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) .addAttribute(this.vertexBuffers[i], shader.attributes.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) - .addAttribute(this.vertexBuffers[i], shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4) - .addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + .addAttribute(this.vertexBuffers[i], shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); + + if (shader.attributes.aTextureId) + { + this.vaos[i].addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + } /* eslint-enable max-len */ } @@ -378,8 +388,12 @@ .addIndex(this.indexBuffer) .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) - .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4) - .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); + + if (this.shader.attributes.aTextureId) + { + this.vaos[this.vertexCount].addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + } /* eslint-enable max-len */ } diff --git a/src/core/text/Text.js b/src/core/text/Text.js index 013625a..cec8c32 100644 --- a/src/core/text/Text.js +++ b/src/core/text/Text.js @@ -6,6 +6,7 @@ import { TEXT_GRADIENT } from '../const'; import settings from '../settings'; import TextStyle from './TextStyle'; +import trimCanvas from '../utils/trimCanvas'; const defaultDestroyOptions = { texture: true, @@ -57,7 +58,7 @@ /** * The canvas 2d context that everything is drawn with - * @member {HTMLCanvasElement} + * @member {CanvasRenderingContext2D} */ this.context = this.canvas.getContext('2d'); @@ -189,10 +190,12 @@ if (style.dropShadow) { + this.context.shadowBlur = style.dropShadowBlur; + this.context.globalAlpha = style.dropShadowAlpha; + if (style.dropShadowBlur > 0) { this.context.shadowColor = style.dropShadowColor; - this.context.shadowBlur = style.dropShadowBlur; } else { @@ -237,6 +240,10 @@ } } + // reset the shadow blur and alpha that was set by the drop shadow, for the regular text + this.context.shadowBlur = 0; + this.context.globalAlpha = 1; + // set canvas text styles this.context.fillStyle = this._generateFillStyle(style, lines); @@ -326,6 +333,15 @@ */ updateTexture() { + if (this._style.trim) + { + const trimmed = trimCanvas(this.canvas); + + this.canvas.width = trimmed.width; + this.canvas.height = trimmed.height; + this.context.putImageData(trimmed.data, 0, 0); + } + const texture = this._texture; const style = this._style; @@ -475,6 +491,19 @@ } /** + * Gets the local bounds of the text object. + * + * @param {Rectangle} rect - The output rectangle. + * @return {Rectangle} The bounds. + */ + getLocalBounds(rect) + { + this.updateText(true); + + return super.getLocalBounds.call(this, rect); + } + + /** * calculates the bounds of the Text as a rectangle. The bounds calculation takes the worldTransform into account. */ _calculateBounds() @@ -525,6 +554,29 @@ const width = this.canvas.width / this.resolution; const height = this.canvas.height / this.resolution; + // make a copy of the style settings, so we can manipulate them later + const fill = style.fill.slice(); + const fillGradientStops = style.fillGradientStops.slice(); + + // wanting to evenly distribute the fills. So an array of 4 colours should give fills of 0.25, 0.5 and 0.75 + if (!fillGradientStops.length) + { + const lengthPlus1 = fill.length + 1; + + for (let i = 1; i < lengthPlus1; ++i) + { + fillGradientStops.push(i / lengthPlus1); + } + } + + // stop the bleeding of the last gradient on the line above to the top gradient of the this line + // by hard defining the first gradient colour at point 0, and last gradient colour at point 1 + fill.unshift(style.fill[0]); + fillGradientStops.unshift(0); + + fill.push(style.fill[style.fill.length - 1]); + fillGradientStops.push(1); + if (style.fillGradientType === TEXT_GRADIENT.LINEAR_VERTICAL) { // start the gradient at the top center of the canvas, and end at the bottom middle of the canvas @@ -532,15 +584,22 @@ // we need to repeat the gradient so that each individual line of text has the same vertical gradient effect // ['#FF0000', '#00FF00', '#0000FF'] over 2 lines would create stops at 0.125, 0.25, 0.375, 0.625, 0.75, 0.875 - totalIterations = (style.fill.length + 1) * lines.length; + totalIterations = (fill.length + 1) * lines.length; currentIteration = 0; for (let i = 0; i < lines.length; i++) { currentIteration += 1; - for (let j = 0; j < style.fill.length; j++) + for (let j = 0; j < fill.length; j++) { - stop = (currentIteration / totalIterations); - gradient.addColorStop(stop, style.fill[j]); + if (fillGradientStops[j]) + { + stop = (fillGradientStops[j] / lines.length) + (i / lines.length); + } + else + { + stop = currentIteration / totalIterations; + } + gradient.addColorStop(stop, fill[j]); currentIteration++; } } @@ -552,13 +611,20 @@ // can just evenly space out the gradients in this case, as multiple lines makes no difference // to an even left to right gradient - totalIterations = style.fill.length + 1; + totalIterations = fill.length + 1; currentIteration = 1; - for (let i = 0; i < style.fill.length; i++) + for (let i = 0; i < fill.length; i++) { - stop = currentIteration / totalIterations; - gradient.addColorStop(stop, style.fill[i]); + if (fillGradientStops[i]) + { + stop = fillGradientStops[i]; + } + else + { + stop = currentIteration / totalIterations; + } + gradient.addColorStop(stop, fill[i]); currentIteration++; } } @@ -569,7 +635,7 @@ /** * Destroys this text object. * Note* Unlike a Sprite, a Text object will automatically destroy its baseTexture and texture as - * the majorety of the time the texture will not be shared with any other Sprites. + * the majority of the time the texture will not be shared with any other Sprites. * * @param {object|boolean} [options] - Options parameter. A boolean will act as if all options * have been set to that value @@ -680,7 +746,7 @@ set text(text) // eslint-disable-line require-jsdoc { - text = String(text || ' '); + text = String(text === '' || text === null || text === undefined ? ' ' : text); if (this._text === text) { diff --git a/src/core/text/TextStyle.js b/src/core/text/TextStyle.js index 3b25528..890950b 100644 --- a/src/core/text/TextStyle.js +++ b/src/core/text/TextStyle.js @@ -8,12 +8,14 @@ align: 'left', breakWords: false, dropShadow: false, + dropShadowAlpha: 1, dropShadowAngle: Math.PI / 6, dropShadowBlur: 0, dropShadowColor: '#000000', dropShadowDistance: 5, fill: 'black', fillGradientType: TEXT_GRADIENT.LINEAR_VERTICAL, + fillGradientStops: [], fontFamily: 'Arial', fontSize: 26, fontStyle: 'normal', @@ -27,6 +29,7 @@ stroke: 'black', strokeThickness: 0, textBaseline: 'alphabetic', + trim: false, wordWrap: false, wordWrapWidth: 100, }; @@ -47,6 +50,7 @@ * @param {boolean} [style.breakWords=false] - Indicates if lines can be wrapped within words, it * needs wordWrap to be set to true * @param {boolean} [style.dropShadow=false] - Set a drop shadow for the text + * @param {number} [style.dropShadowAlpha=1] - Set alpha for the drop shadow * @param {number} [style.dropShadowAngle=Math.PI/6] - Set a angle of the drop shadow * @param {number} [style.dropShadowBlur=0] - Set a shadow blur radius * @param {string} [style.dropShadowColor='#000000'] - A fill style to be used on the dropshadow e.g 'red', '#00FF00' @@ -55,8 +59,10 @@ * fillstyle that will be used on the text e.g 'red', '#00FF00'. Can be an array to create a gradient * eg ['#000000','#FFFFFF'] * {@link https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/fillStyle|MDN} - * @param {number} [style.fillGradientType=PIXI.TEXT_GRADIENT.LINEAR_VERTICAL] - If fills styles are - * supplied, this can change the type/direction of the gradient. See {@link PIXI.TEXT_GRADIENT} for possible values + * @param {number} [style.fillGradientType=PIXI.TEXT_GRADIENT.LINEAR_VERTICAL] - If fill is an array of colours + * to create a gradient, this can change the type/direction of the gradient. See {@link PIXI.TEXT_GRADIENT} + * @param {number[]} [style.fillGradientStops] - If fill is an array of colours to create a gradient, this array can set + * the stop points (numbers between 0 and 1) for the color, overriding the default behaviour of evenly spacing them. * @param {string|string[]} [style.fontFamily='Arial'] - The font family * @param {number|string} [style.fontSize=26] - The font size (as a number it converts to px, but as a string, * equivalents are '26px','20pt','160%' or '1.6em') @@ -76,6 +82,7 @@ * e.g 'blue', '#FCFF00' * @param {number} [style.strokeThickness=0] - A number that represents the thickness of the stroke. * Default is 0 (no stroke) + * @param {boolean} [style.trim=false] - Trim transparent borders * @param {string} [style.textBaseline='alphabetic'] - The baseline of the text that is rendered. * @param {boolean} [style.wordWrap=false] - Indicates if word wrap should be used * @param {number} [style.wordWrapWidth=100] - The width at which text will wrap, it needs wordWrap to be set to true @@ -152,6 +159,19 @@ } } + get dropShadowAlpha() + { + return this._dropShadowAlpha; + } + set dropShadowAlpha(dropShadowAlpha) + { + if (this._dropShadowAlpha !== dropShadowAlpha) + { + this._dropShadowAlpha = dropShadowAlpha; + this.styleID++; + } + } + get dropShadowAngle() { return this._dropShadowAngle; @@ -232,6 +252,19 @@ } } + get fillGradientStops() + { + return this._fillGradientStops; + } + set fillGradientStops(fillGradientStops) + { + if (!areArraysEqual(this._fillGradientStops,fillGradientStops)) + { + this._fillGradientStops = fillGradientStops; + this.styleID++; + } + } + get fontFamily() { return this._fontFamily; @@ -402,6 +435,19 @@ } } + get trim() + { + return this._trim; + } + set trim(trim) + { + if (this._trim !== trim) + { + this._trim = trim; + this.styleID++; + } + } + get wordWrap() { return this._wordWrap; @@ -475,3 +521,34 @@ return color; } } + +/** + * Utility function to convert hexadecimal colors to strings, and simply return the color if it's a string. + * This version can also convert array of colors + * + * @param {Array} array1 First array to compare + * @param {Array} array2 Second array to compare + * @return {boolean} Do the arrays contain the same values in the same order + */ +function areArraysEqual(array1, array2) +{ + if (!Array.isArray(array1) || !Array.isArray(array2)) + { + return false; + } + + if (array1.length !== array2.length) + { + return false; + } + + for (let i = 0; i < array1.length; ++i) + { + if (array1[i] !== array2[i]) + { + return false; + } + } + + return true; +} diff --git a/src/core/textures/BaseRenderTexture.js b/src/core/textures/BaseRenderTexture.js index 5d50146..aa727f0 100644 --- a/src/core/textures/BaseRenderTexture.js +++ b/src/core/textures/BaseRenderTexture.js @@ -1,8 +1,6 @@ import BaseTexture from './BaseTexture'; import settings from '../settings'; -const { RESOLUTION, SCALE_MODE } = settings; - /** * A BaseRenderTexture is a special texture that allows any Pixi display object to be rendered to it. * @@ -54,7 +52,7 @@ { super(null, scaleMode); - this.resolution = resolution || RESOLUTION; + this.resolution = resolution || settings.RESOLUTION; this.width = width; this.height = height; @@ -62,7 +60,7 @@ this.realWidth = this.width * this.resolution; this.realHeight = this.height * this.resolution; - this.scaleMode = scaleMode || SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; this.hasLoaded = true; /** diff --git a/src/core/textures/BaseTexture.js b/src/core/textures/BaseTexture.js index 676dca3..aeccf39 100644 --- a/src/core/textures/BaseTexture.js +++ b/src/core/textures/BaseTexture.js @@ -77,7 +77,7 @@ * @default PIXI.settings.SCALE_MODE * @see PIXI.SCALE_MODES */ - this.scaleMode = scaleMode || settings.SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; /** * Set to true once the base texture has successfully loaded. @@ -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/RenderTexture.js b/src/core/textures/RenderTexture.js index 4c5034f..b3c6297 100644 --- a/src/core/textures/RenderTexture.js +++ b/src/core/textures/RenderTexture.js @@ -54,8 +54,8 @@ /* eslint-disable prefer-rest-params, no-console */ const width = arguments[1]; const height = arguments[2]; - const scaleMode = arguments[3] || 0; - const resolution = arguments[4] || 1; + const scaleMode = arguments[3]; + const resolution = arguments[4]; // we have an old render texture.. console.warn(`Please use RenderTexture.create(${width}, ${height}) instead of the ctor directly.`); 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 ca31456..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 @@ -459,7 +497,9 @@ if (frame.x + frame.width > this.baseTexture.width || frame.y + frame.height > this.baseTexture.height) { - throw new Error(`Texture Error: frame does not fit inside the base Texture dimensions ${this}`); + throw new Error('Texture Error: frame does not fit inside the base Texture dimensions: ' + + `X: ${frame.x} + ${frame.width} > ${this.baseTexture.width} ` + + `Y: ${frame.y} + ${frame.height} > ${this.baseTexture.height}`); } // this.valid = frame && frame.width && frame.height && this.baseTexture.source && this.baseTexture.hasLoaded; @@ -520,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. @@ -528,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 2a7497b..99e585a 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)); } /** @@ -360,3 +362,43 @@ * @private */ export const BaseTextureCache = {}; + +/** + * 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/core/utils/trimCanvas.js b/src/core/utils/trimCanvas.js new file mode 100644 index 0000000..5af6de0 --- /dev/null +++ b/src/core/utils/trimCanvas.js @@ -0,0 +1,83 @@ +/** + * Trim transparent borders from a canvas + * + * @memberof PIXI + * @function trimCanvas + * @private + * @param {HTMLCanvasElement} canvas - the canvas to trim + * @returns {object} Trim data + */ +export default function trimCanvas(canvas) +{ + // https://gist.github.com/remy/784508 + + let width = canvas.width; + let height = canvas.height; + + const context = canvas.getContext('2d'); + const imageData = context.getImageData(0, 0, width, height); + const pixels = imageData.data; + const len = pixels.length; + + const bound = { + top: null, + left: null, + right: null, + bottom: null, + }; + let i; + let x; + let y; + + for (i = 0; i < len; i += 4) + { + if (pixels[i + 3] !== 0) + { + x = (i / 4) % width; + y = ~~((i / 4) / width); + + if (bound.top === null) + { + bound.top = y; + } + + if (bound.left === null) + { + bound.left = x; + } + else if (x < bound.left) + { + bound.left = x; + } + + if (bound.right === null) + { + bound.right = x + 1; + } + else if (bound.right < x) + { + bound.right = x + 1; + } + + if (bound.bottom === null) + { + bound.bottom = y; + } + else if (bound.bottom < y) + { + bound.bottom = y; + } + } + } + + width = bound.right - bound.left; + height = bound.bottom - bound.top + 1; + + const data = context.getImageData(bound.left, bound.top, width, height); + + return { + height, + width, + data, + }; +} diff --git a/src/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/extras/TilingSprite.js b/src/extras/TilingSprite.js index f90d248..fe82451 100644 --- a/src/extras/TilingSprite.js +++ b/src/extras/TilingSprite.js @@ -220,20 +220,33 @@ transform.tx * resolution, transform.ty * resolution); - // TODO - this should be rolled into the setTransform above.. - context.scale(this.tileScale.x / baseTextureResolution, this.tileScale.y / baseTextureResolution); - - context.translate(modX + (this.anchor.x * -this._width), - modY + (this.anchor.y * -this._height)); - renderer.setBlendMode(this.blendMode); // fill the pattern! context.fillStyle = this._canvasPattern; - context.fillRect(-modX, - -modY, - this._width / this.tileScale.x * baseTextureResolution, - this._height / this.tileScale.y * baseTextureResolution); + + // TODO - this should be rolled into the setTransform above.. + context.scale(this.tileScale.x / baseTextureResolution, this.tileScale.y / baseTextureResolution); + + const anchorX = this.anchor.x * -this._width; + const anchorY = this.anchor.y * -this._height; + + if (this.uvRespectAnchor) + { + context.translate(modX, modY); + + context.fillRect(-modX + anchorX, -modY + anchorY, + this._width / this.tileScale.x * baseTextureResolution, + this._height / this.tileScale.y * baseTextureResolution); + } + else + { + context.translate(modX + anchorX, modY + anchorY); + + context.fillRect(-modX, -modY, + this._width / this.tileScale.x * baseTextureResolution, + this._height / this.tileScale.y * baseTextureResolution); + } } /** diff --git a/src/extras/cacheAsBitmap.js b/src/extras/cacheAsBitmap.js index 18043a7..5dd5640 100644 --- a/src/extras/cacheAsBitmap.js +++ b/src/extras/cacheAsBitmap.js @@ -41,6 +41,9 @@ * provide a performance benefit for complex static displayObjects. * To remove simply set this property to 'false' * + * IMPORTANT GOTCHA - make sure that all your textures are preloaded BEFORE setting this property to true + * as it will take a snapshot of what is currently there. If the textures have not loaded then they will not appear. + * * @member {boolean} * @memberof PIXI.DisplayObject# */ diff --git a/src/filters/blur/BlurFilter.js b/src/filters/blur/BlurFilter.js index 340fc58..1575db9 100644 --- a/src/filters/blur/BlurFilter.js +++ b/src/filters/blur/BlurFilter.js @@ -24,10 +24,9 @@ this.blurXFilter = new BlurXFilter(strength, quality, resolution, kernelSize); this.blurYFilter = new BlurYFilter(strength, quality, resolution, kernelSize); - this.resolution = 1; this.padding = 0; - this.resolution = resolution || 1; + this.resolution = resolution || core.settings.RESOLUTION; this.quality = quality || 4; this.blur = strength || 8; } diff --git a/src/filters/blur/BlurXFilter.js b/src/filters/blur/BlurXFilter.js index def99c3..0f48889 100644 --- a/src/filters/blur/BlurXFilter.js +++ b/src/filters/blur/BlurXFilter.js @@ -31,7 +31,7 @@ fragSrc ); - this.resolution = resolution || 1; + this.resolution = resolution || core.settings.RESOLUTION; this._quality = 0; diff --git a/src/filters/blur/BlurYFilter.js b/src/filters/blur/BlurYFilter.js index 638e0f0..6d489c7 100644 --- a/src/filters/blur/BlurYFilter.js +++ b/src/filters/blur/BlurYFilter.js @@ -31,7 +31,7 @@ fragSrc ); - this.resolution = resolution || 1; + this.resolution = resolution || core.settings.RESOLUTION; this._quality = 0; diff --git a/src/filters/colormatrix/ColorMatrixFilter.js b/src/filters/colormatrix/ColorMatrixFilter.js index 8509505..679f8ec 100644 --- a/src/filters/colormatrix/ColorMatrixFilter.js +++ b/src/filters/colormatrix/ColorMatrixFilter.js @@ -75,28 +75,28 @@ out[1] = (a[0] * b[1]) + (a[1] * b[6]) + (a[2] * b[11]) + (a[3] * b[16]); out[2] = (a[0] * b[2]) + (a[1] * b[7]) + (a[2] * b[12]) + (a[3] * b[17]); out[3] = (a[0] * b[3]) + (a[1] * b[8]) + (a[2] * b[13]) + (a[3] * b[18]); - out[4] = (a[0] * b[4]) + (a[1] * b[9]) + (a[2] * b[14]) + (a[3] * b[19]); + out[4] = (a[0] * b[4]) + (a[1] * b[9]) + (a[2] * b[14]) + (a[3] * b[19]) + a[4]; // Green Channel out[5] = (a[5] * b[0]) + (a[6] * b[5]) + (a[7] * b[10]) + (a[8] * b[15]); out[6] = (a[5] * b[1]) + (a[6] * b[6]) + (a[7] * b[11]) + (a[8] * b[16]); out[7] = (a[5] * b[2]) + (a[6] * b[7]) + (a[7] * b[12]) + (a[8] * b[17]); out[8] = (a[5] * b[3]) + (a[6] * b[8]) + (a[7] * b[13]) + (a[8] * b[18]); - out[9] = (a[5] * b[4]) + (a[6] * b[9]) + (a[7] * b[14]) + (a[8] * b[19]); + out[9] = (a[5] * b[4]) + (a[6] * b[9]) + (a[7] * b[14]) + (a[8] * b[19]) + a[9]; // Blue Channel out[10] = (a[10] * b[0]) + (a[11] * b[5]) + (a[12] * b[10]) + (a[13] * b[15]); out[11] = (a[10] * b[1]) + (a[11] * b[6]) + (a[12] * b[11]) + (a[13] * b[16]); out[12] = (a[10] * b[2]) + (a[11] * b[7]) + (a[12] * b[12]) + (a[13] * b[17]); out[13] = (a[10] * b[3]) + (a[11] * b[8]) + (a[12] * b[13]) + (a[13] * b[18]); - out[14] = (a[10] * b[4]) + (a[11] * b[9]) + (a[12] * b[14]) + (a[13] * b[19]); + out[14] = (a[10] * b[4]) + (a[11] * b[9]) + (a[12] * b[14]) + (a[13] * b[19]) + a[14]; // Alpha Channel out[15] = (a[15] * b[0]) + (a[16] * b[5]) + (a[17] * b[10]) + (a[18] * b[15]); out[16] = (a[15] * b[1]) + (a[16] * b[6]) + (a[17] * b[11]) + (a[18] * b[16]); out[17] = (a[15] * b[2]) + (a[16] * b[7]) + (a[17] * b[12]) + (a[18] * b[17]); out[18] = (a[15] * b[3]) + (a[16] * b[8]) + (a[17] * b[13]) + (a[18] * b[18]); - out[19] = (a[15] * b[4]) + (a[16] * b[9]) + (a[17] * b[14]) + (a[18] * b[19]); + out[19] = (a[15] * b[4]) + (a[16] * b[9]) + (a[17] * b[14]) + (a[18] * b[19]) + a[19]; return out; } diff --git a/src/filters/colormatrix/colorMatrix.frag b/src/filters/colormatrix/colorMatrix.frag index 3aaf2b3..c73c0e9 100644 --- a/src/filters/colormatrix/colorMatrix.frag +++ b/src/filters/colormatrix/colorMatrix.frag @@ -4,32 +4,38 @@ void main(void) { - vec4 c = texture2D(uSampler, vTextureCoord); + // Un-premultiply alpha before applying the color matrix. See issue #3539. + if (c.a > 0.0) { + c.rgb /= c.a; + } + vec4 result; + result.r = (m[0] * c.r); + result.r += (m[1] * c.g); + result.r += (m[2] * c.b); + result.r += (m[3] * c.a); + result.r += m[4]; - gl_FragColor.r = (m[0] * c.r); - gl_FragColor.r += (m[1] * c.g); - gl_FragColor.r += (m[2] * c.b); - gl_FragColor.r += (m[3] * c.a); - gl_FragColor.r += m[4] * c.a; + result.g = (m[5] * c.r); + result.g += (m[6] * c.g); + result.g += (m[7] * c.b); + result.g += (m[8] * c.a); + result.g += m[9]; - gl_FragColor.g = (m[5] * c.r); - gl_FragColor.g += (m[6] * c.g); - gl_FragColor.g += (m[7] * c.b); - gl_FragColor.g += (m[8] * c.a); - gl_FragColor.g += m[9] * c.a; + result.b = (m[10] * c.r); + result.b += (m[11] * c.g); + result.b += (m[12] * c.b); + result.b += (m[13] * c.a); + result.b += m[14]; - gl_FragColor.b = (m[10] * c.r); - gl_FragColor.b += (m[11] * c.g); - gl_FragColor.b += (m[12] * c.b); - gl_FragColor.b += (m[13] * c.a); - gl_FragColor.b += m[14] * c.a; + result.a = (m[15] * c.r); + result.a += (m[16] * c.g); + result.a += (m[17] * c.b); + result.a += (m[18] * c.a); + result.a += m[19]; - gl_FragColor.a = (m[15] * c.r); - gl_FragColor.a += (m[16] * c.g); - gl_FragColor.a += (m[17] * c.b); - gl_FragColor.a += (m[18] * c.a); - gl_FragColor.a += m[19] * c.a; + // Premultiply alpha again. + result.rgb *= result.a; -// gl_FragColor = vec4(m[0]); + gl_FragColor = result; } diff --git a/src/filters/displacement/DisplacementFilter.js b/src/filters/displacement/DisplacementFilter.js index 112de0b..d5355c3 100644 --- a/src/filters/displacement/DisplacementFilter.js +++ b/src/filters/displacement/DisplacementFilter.js @@ -36,7 +36,7 @@ this.maskMatrix = maskMatrix; this.uniforms.mapSampler = sprite.texture; - this.uniforms.filterMatrix = maskMatrix.toArray(true); + this.uniforms.filterMatrix = maskMatrix; this.uniforms.scale = { x: 1, y: 1 }; if (scale === null || scale === undefined) diff --git a/src/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/InteractionData.js b/src/interaction/InteractionData.js index 02d53a3..818da35 100644 --- a/src/interaction/InteractionData.js +++ b/src/interaction/InteractionData.js @@ -21,9 +21,9 @@ this.global = new core.Point(); /** - * The target Sprite that was interacted with + * The target DisplayObject that was interacted with * - * @member {PIXI.Sprite} + * @member {PIXI.DisplayObject} */ this.target = null; @@ -33,6 +33,13 @@ * @member {Event} */ this.originalEvent = null; + + /** + * Unique identifier for this interaction + * + * @member {number} + */ + this.identifier = null; } /** diff --git a/src/interaction/InteractionEvent.js b/src/interaction/InteractionEvent.js index 75b2966..dc554f9 100644 --- a/src/interaction/InteractionEvent.js +++ b/src/interaction/InteractionEvent.js @@ -58,7 +58,7 @@ } /** - * Prevents event from reaching any objects other than the current object. + * Resets the event. * * @private */ diff --git a/src/interaction/InteractionManager.js b/src/interaction/InteractionManager.js index 7d51134..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 1271fef..71ade15 100644 --- a/package.json +++ b/package.json @@ -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/scripts/release.js b/scripts/release.js index c4c8e36..156620c 100644 --- a/scripts/release.js +++ b/scripts/release.js @@ -12,7 +12,8 @@ 'dist/**/*', 'lib/**/*', 'src/**/*', - 'scripts/**/*', + 'scripts/*', + 'scripts/renders/*', 'test/**/*', '*.json', '*.md', diff --git a/scripts/renders/client.js b/scripts/renders/client.js index 16c9565..6faee34 100755 --- a/scripts/renders/client.js +++ b/scripts/renders/client.js @@ -1,6 +1,6 @@ 'use strict'; -require('../../bin/pixi'); +require('../../dist/pixi'); PIXI.utils.skipHello(); const fs = require('fs'); 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 bc730db..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. @@ -33,8 +33,11 @@ * need to call toDataUrl on the webgl context * @param {number} [options.resolution=1] - The resolution / device pixel ratio of the renderer, retina would be 2 * @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 @@ -43,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. */ @@ -73,7 +99,7 @@ */ stop() { - this.ticker.stop(); + this._ticker.stop(); } /** @@ -81,7 +107,7 @@ */ start() { - this.ticker.start(); + this._ticker.start(); } /** @@ -95,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 index 81e076b..78b17e6 100644 --- a/src/core/Shader.js +++ b/src/core/Shader.js @@ -1,9 +1,7 @@ import { GLShader } from 'pixi-gl-core'; import settings from './settings'; -const { PRECISION } = settings; - -function checkPrecision(src) +function checkPrecision(src, def) { if (src instanceof Array) { @@ -11,14 +9,14 @@ { const copy = src.slice(0); - copy.unshift(`precision ${PRECISION} float;`); + copy.unshift(`precision ${def} float;`); return copy; } } else if (src.substring(0, 9) !== 'precision') { - return `precision ${PRECISION} float;\n${src}`; + return `precision ${def} float;\n${src}`; } return src; @@ -42,6 +40,7 @@ */ constructor(gl, vertexSrc, fragmentSrc) { - super(gl, checkPrecision(vertexSrc), checkPrecision(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 3425b41..f9235e4 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -181,7 +181,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 b3792ef..17fde8d 100644 --- a/src/core/display/Container.js +++ b/src/core/display/Container.js @@ -73,13 +73,14 @@ } child.parent = this; - - // ensure a transform will be recalculated.. - this.transform._parentID = -1; - this._boundsID++; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.push(child); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(this.children.length - 1); child.emit('added', this); @@ -108,9 +109,14 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.splice(index, 0, child); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(index); child.emit('added', this); @@ -174,6 +180,7 @@ removeItems(this.children, currentIndex, 1); // remove from old position this.children.splice(index, 0, child); // add at new position + this.onChildrenChange(index); } @@ -220,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! @@ -244,9 +252,14 @@ { const child = this.getChildAt(index); + // ensure child transform will be recalculated.. child.parent = null; + child.transform._parentID = -1; removeItems(this.children, index, 1); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(index); child.emit('removed', this); @@ -275,8 +288,14 @@ for (let i = 0; i < removed.length; ++i) { removed[i].parent = null; + if (removed[i].transform) + { + removed[i].transform._parentID = -1; + } } + this._boundsID++; + this.onChildrenChange(beginIndex); for (let i = 0; i < removed.length; ++i) diff --git a/src/core/display/DisplayObject.js b/src/core/display/DisplayObject.js index 3affe36..4e6c77d 100644 --- a/src/core/display/DisplayObject.js +++ b/src/core/display/DisplayObject.js @@ -109,7 +109,7 @@ /** * The original, cached mask of the object * - * @member {PIXI.Rectangle} + * @member {PIXI.Graphics|PIXI.Sprite} * @private */ this._mask = null; diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index 95f2af4..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 ); @@ -1065,10 +1061,15 @@ canvasRenderer = new CanvasRenderer(); } - tempMatrix.tx = -bounds.x; - tempMatrix.ty = -bounds.y; + this.transform.updateLocalTransform(); + this.transform.localTransform.copy(tempMatrix); - canvasRenderer.render(this, canvasBuffer, false, tempMatrix); + tempMatrix.invert(); + + tempMatrix.tx -= bounds.x; + tempMatrix.ty -= bounds.y; + + canvasRenderer.render(this, canvasBuffer, true, tempMatrix); const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode); 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 e67680f..867dde2 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/buildCircle.js b/src/core/graphics/webgl/utils/buildCircle.js index 0400d13..abf5d02 100644 --- a/src/core/graphics/webgl/utils/buildCircle.js +++ b/src/core/graphics/webgl/utils/buildCircle.js @@ -33,6 +33,11 @@ height = circleData.height; } + if (width === 0 || height === 0) + { + return; + } + const totalSegs = Math.floor(30 * Math.sqrt(circleData.radius)) || Math.floor(15 * Math.sqrt(circleData.width + circleData.height)); diff --git a/src/core/graphics/webgl/utils/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 90155f9..f03fe15 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -30,6 +30,7 @@ export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; export { default as CanvasGraphicsRenderer } from './graphics/canvas/CanvasGraphicsRenderer'; +export { default as Spritesheet } from './textures/Spritesheet'; export { default as Texture } from './textures/Texture'; export { default as BaseTexture } from './textures/BaseTexture'; export { default as RenderTexture } from './textures/RenderTexture'; diff --git a/src/core/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 ccce680..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); } /** @@ -162,6 +162,9 @@ if (transform) { transform.copy(tempWt); + + // lets not forget to flag the parent transform as dirty... + this._tempDisplayObjectParent.transform._worldID = -1; } else { @@ -169,6 +172,7 @@ } displayObject.parent = this._tempDisplayObjectParent; + displayObject.updateTransform(); displayObject.parent = cacheParent; // displayObject.hitArea = //TODO add a temp hit area @@ -279,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/canvas/utils/CanvasRenderTarget.js b/src/core/renderers/canvas/utils/CanvasRenderTarget.js index 3de0aef..b409891 100644 --- a/src/core/renderers/canvas/utils/CanvasRenderTarget.js +++ b/src/core/renderers/canvas/utils/CanvasRenderTarget.js @@ -1,5 +1,4 @@ import settings from '../../../settings'; -const { RESOLUTION } = settings; /** * Creates a Canvas element of the given size. @@ -30,7 +29,7 @@ */ this.context = this.canvas.getContext('2d'); - this.resolution = resolution || RESOLUTION; + this.resolution = resolution || settings.RESOLUTION; this.resize(width, height); } 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 ce0e1c4..788ed6c 100644 --- a/src/core/renderers/webgl/WebGLRenderer.js +++ b/src/core/renderers/webgl/WebGLRenderer.js @@ -30,8 +30,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 @@ -49,10 +49,19 @@ * enable this if you need to call toDataUrl on the webgl context. * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when * rendering, stopping pixel interpolation. + * @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; + + if (this.legacy) + { + glCore.VertexArrayObject.FORCE_NATIVE = true; + } /** * The type of this renderer as a standardised const @@ -229,7 +238,7 @@ this.emit('context', gl); // setup the width/height properties and gl viewport - this.resize(this.width, this.height); + this.resize(this.screen.width, this.screen.height); } /** @@ -323,16 +332,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) { @@ -376,6 +385,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 @@ -440,9 +469,10 @@ * Changes the current shader to the one given in parameter * * @param {PIXI.Shader} shader - the new shader + * @param {boolean} [autoProject=true] - Whether automatically set the projection matrix * @return {PIXI.WebGLRenderer} Returns itself. */ - bindShader(shader) + bindShader(shader, autoProject) { // TODO cache if (this._activeShader !== shader) @@ -450,8 +480,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 bbab3b3..944f41d 100644 --- a/src/core/renderers/webgl/filters/Filter.js +++ b/src/core/renderers/webgl/filters/Filter.js @@ -1,6 +1,7 @@ import extractUniformsFromSrc from './extractUniformsFromSrc'; import { uid } from '../../../utils'; import { BLEND_MODES } from '../../../const'; +import settings from '../../../settings'; const SOURCE_KEY_MAP = {}; @@ -10,7 +11,7 @@ * @memberof PIXI * @extends PIXI.Shader */ -class Filter +export default class Filter { /** * @param {string} [vertexSrc] - The source of the vertex shader. @@ -35,8 +36,6 @@ this.blendMode = BLEND_MODES.NORMAL; - // pull out the vertex and shader uniforms if they are not specified.. - // currently this does not extract structs only default types this.uniformData = uniforms || extractUniformsFromSrc(this.vertexSrc, this.fragmentSrc, 'projectionMatrix|uSampler'); /** @@ -80,7 +79,7 @@ * * @member {number} */ - this.resolution = 1; + this.resolution = settings.RESOLUTION; /** * If enabled is true the filter is applied, if false it will not. @@ -97,8 +96,11 @@ * @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 { // --- // // this.uniforms.filterMatrix = filterManager.calculateSpriteMatrix(tempMatrix, window.panda ); @@ -170,5 +172,3 @@ ].join('\n'); } } - -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 dc7072a..6c7422d 100644 --- a/src/core/renderers/webgl/managers/FilterManager.js +++ b/src/core/renderers/webgl/managers/FilterManager.js @@ -154,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 @@ -176,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; @@ -184,7 +184,7 @@ flop = t; } - filters[i].apply(this, flip, lastState.renderTarget, true); + filters[i].apply(this, flip, lastState.renderTarget, false, currentState); this.freePotRenderTarget(flip); this.freePotRenderTarget(flop); @@ -291,9 +291,13 @@ let textureCount = 1; let currentState; - if (shader.uniforms.data.filterArea) + // 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; @@ -306,9 +310,9 @@ // use this to clamp displaced texture coords so they belong to filterArea // see displacementFilter fragment shader for an example - if (shader.uniforms.data.filterClamp) + if (shader.uniforms.filterClamp) { - currentState = this.filterData.stack[this.filterData.index]; + currentState = currentState || this.filterData.stack[this.filterData.index]; const filterClamp = shader.uniforms.filterClamp; @@ -485,7 +489,7 @@ */ destroy() { - this.shaderCache = []; + this.shaderCache = {}; this.emptyPool(); } diff --git a/src/core/renderers/webgl/utils/RenderTarget.js b/src/core/renderers/webgl/utils/RenderTarget.js index afe51ab..6d2467f 100644 --- a/src/core/renderers/webgl/utils/RenderTarget.js +++ b/src/core/renderers/webgl/utils/RenderTarget.js @@ -124,7 +124,7 @@ * @default PIXI.settings.SCALE_MODE * @see PIXI.SCALE_MODES */ - this.scaleMode = scaleMode || settings.SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; /** * Whether this object is the root element or not 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 36ea5bb..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; @@ -346,8 +346,8 @@ /** * Gets the local bounds of the sprite object. * - * @param {Rectangle} rect - The output rectangle. - * @return {Rectangle} The bounds. + * @param {PIXI.Rectangle} rect - The output rectangle. + * @return {PIXI.Rectangle} The bounds. */ getLocalBounds(rect) { @@ -440,7 +440,7 @@ * * @static * @param {number|string|PIXI.BaseTexture|HTMLCanvasElement|HTMLVideoElement} source Source to create texture from - * @return {PIXI.Texture} 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 e77b39a..90b08c0 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++) @@ -103,11 +102,18 @@ { const gl = this.renderer.gl; - // step 1: first check max textures the GPU can handle. - this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); + if (this.renderer.legacy) + { + this.MAX_TEXTURES = 1; + } + else + { + // step 1: first check max textures the GPU can handle. + this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); - // step 2: check the maximum number of if statements the shader can have too.. - this.MAX_TEXTURES = checkMaxIfStatmentsInShader(this.MAX_TEXTURES, gl); + // step 2: check the maximum number of if statements the shader can have too.. + this.MAX_TEXTURES = checkMaxIfStatmentsInShader(this.MAX_TEXTURES, gl); + } const shader = this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); @@ -130,8 +136,12 @@ .addIndex(this.indexBuffer) .addAttribute(this.vertexBuffers[i], shader.attributes.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) .addAttribute(this.vertexBuffers[i], shader.attributes.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) - .addAttribute(this.vertexBuffers[i], shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4) - .addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + .addAttribute(this.vertexBuffers[i], shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); + + if (shader.attributes.aTextureId) + { + this.vaos[i].addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + } /* eslint-enable max-len */ } @@ -378,8 +388,12 @@ .addIndex(this.indexBuffer) .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) - .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4) - .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); + + if (this.shader.attributes.aTextureId) + { + this.vaos[this.vertexCount].addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + } /* eslint-enable max-len */ } diff --git a/src/core/text/Text.js b/src/core/text/Text.js index 013625a..cec8c32 100644 --- a/src/core/text/Text.js +++ b/src/core/text/Text.js @@ -6,6 +6,7 @@ import { TEXT_GRADIENT } from '../const'; import settings from '../settings'; import TextStyle from './TextStyle'; +import trimCanvas from '../utils/trimCanvas'; const defaultDestroyOptions = { texture: true, @@ -57,7 +58,7 @@ /** * The canvas 2d context that everything is drawn with - * @member {HTMLCanvasElement} + * @member {CanvasRenderingContext2D} */ this.context = this.canvas.getContext('2d'); @@ -189,10 +190,12 @@ if (style.dropShadow) { + this.context.shadowBlur = style.dropShadowBlur; + this.context.globalAlpha = style.dropShadowAlpha; + if (style.dropShadowBlur > 0) { this.context.shadowColor = style.dropShadowColor; - this.context.shadowBlur = style.dropShadowBlur; } else { @@ -237,6 +240,10 @@ } } + // reset the shadow blur and alpha that was set by the drop shadow, for the regular text + this.context.shadowBlur = 0; + this.context.globalAlpha = 1; + // set canvas text styles this.context.fillStyle = this._generateFillStyle(style, lines); @@ -326,6 +333,15 @@ */ updateTexture() { + if (this._style.trim) + { + const trimmed = trimCanvas(this.canvas); + + this.canvas.width = trimmed.width; + this.canvas.height = trimmed.height; + this.context.putImageData(trimmed.data, 0, 0); + } + const texture = this._texture; const style = this._style; @@ -475,6 +491,19 @@ } /** + * Gets the local bounds of the text object. + * + * @param {Rectangle} rect - The output rectangle. + * @return {Rectangle} The bounds. + */ + getLocalBounds(rect) + { + this.updateText(true); + + return super.getLocalBounds.call(this, rect); + } + + /** * calculates the bounds of the Text as a rectangle. The bounds calculation takes the worldTransform into account. */ _calculateBounds() @@ -525,6 +554,29 @@ const width = this.canvas.width / this.resolution; const height = this.canvas.height / this.resolution; + // make a copy of the style settings, so we can manipulate them later + const fill = style.fill.slice(); + const fillGradientStops = style.fillGradientStops.slice(); + + // wanting to evenly distribute the fills. So an array of 4 colours should give fills of 0.25, 0.5 and 0.75 + if (!fillGradientStops.length) + { + const lengthPlus1 = fill.length + 1; + + for (let i = 1; i < lengthPlus1; ++i) + { + fillGradientStops.push(i / lengthPlus1); + } + } + + // stop the bleeding of the last gradient on the line above to the top gradient of the this line + // by hard defining the first gradient colour at point 0, and last gradient colour at point 1 + fill.unshift(style.fill[0]); + fillGradientStops.unshift(0); + + fill.push(style.fill[style.fill.length - 1]); + fillGradientStops.push(1); + if (style.fillGradientType === TEXT_GRADIENT.LINEAR_VERTICAL) { // start the gradient at the top center of the canvas, and end at the bottom middle of the canvas @@ -532,15 +584,22 @@ // we need to repeat the gradient so that each individual line of text has the same vertical gradient effect // ['#FF0000', '#00FF00', '#0000FF'] over 2 lines would create stops at 0.125, 0.25, 0.375, 0.625, 0.75, 0.875 - totalIterations = (style.fill.length + 1) * lines.length; + totalIterations = (fill.length + 1) * lines.length; currentIteration = 0; for (let i = 0; i < lines.length; i++) { currentIteration += 1; - for (let j = 0; j < style.fill.length; j++) + for (let j = 0; j < fill.length; j++) { - stop = (currentIteration / totalIterations); - gradient.addColorStop(stop, style.fill[j]); + if (fillGradientStops[j]) + { + stop = (fillGradientStops[j] / lines.length) + (i / lines.length); + } + else + { + stop = currentIteration / totalIterations; + } + gradient.addColorStop(stop, fill[j]); currentIteration++; } } @@ -552,13 +611,20 @@ // can just evenly space out the gradients in this case, as multiple lines makes no difference // to an even left to right gradient - totalIterations = style.fill.length + 1; + totalIterations = fill.length + 1; currentIteration = 1; - for (let i = 0; i < style.fill.length; i++) + for (let i = 0; i < fill.length; i++) { - stop = currentIteration / totalIterations; - gradient.addColorStop(stop, style.fill[i]); + if (fillGradientStops[i]) + { + stop = fillGradientStops[i]; + } + else + { + stop = currentIteration / totalIterations; + } + gradient.addColorStop(stop, fill[i]); currentIteration++; } } @@ -569,7 +635,7 @@ /** * Destroys this text object. * Note* Unlike a Sprite, a Text object will automatically destroy its baseTexture and texture as - * the majorety of the time the texture will not be shared with any other Sprites. + * the majority of the time the texture will not be shared with any other Sprites. * * @param {object|boolean} [options] - Options parameter. A boolean will act as if all options * have been set to that value @@ -680,7 +746,7 @@ set text(text) // eslint-disable-line require-jsdoc { - text = String(text || ' '); + text = String(text === '' || text === null || text === undefined ? ' ' : text); if (this._text === text) { diff --git a/src/core/text/TextStyle.js b/src/core/text/TextStyle.js index 3b25528..890950b 100644 --- a/src/core/text/TextStyle.js +++ b/src/core/text/TextStyle.js @@ -8,12 +8,14 @@ align: 'left', breakWords: false, dropShadow: false, + dropShadowAlpha: 1, dropShadowAngle: Math.PI / 6, dropShadowBlur: 0, dropShadowColor: '#000000', dropShadowDistance: 5, fill: 'black', fillGradientType: TEXT_GRADIENT.LINEAR_VERTICAL, + fillGradientStops: [], fontFamily: 'Arial', fontSize: 26, fontStyle: 'normal', @@ -27,6 +29,7 @@ stroke: 'black', strokeThickness: 0, textBaseline: 'alphabetic', + trim: false, wordWrap: false, wordWrapWidth: 100, }; @@ -47,6 +50,7 @@ * @param {boolean} [style.breakWords=false] - Indicates if lines can be wrapped within words, it * needs wordWrap to be set to true * @param {boolean} [style.dropShadow=false] - Set a drop shadow for the text + * @param {number} [style.dropShadowAlpha=1] - Set alpha for the drop shadow * @param {number} [style.dropShadowAngle=Math.PI/6] - Set a angle of the drop shadow * @param {number} [style.dropShadowBlur=0] - Set a shadow blur radius * @param {string} [style.dropShadowColor='#000000'] - A fill style to be used on the dropshadow e.g 'red', '#00FF00' @@ -55,8 +59,10 @@ * fillstyle that will be used on the text e.g 'red', '#00FF00'. Can be an array to create a gradient * eg ['#000000','#FFFFFF'] * {@link https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/fillStyle|MDN} - * @param {number} [style.fillGradientType=PIXI.TEXT_GRADIENT.LINEAR_VERTICAL] - If fills styles are - * supplied, this can change the type/direction of the gradient. See {@link PIXI.TEXT_GRADIENT} for possible values + * @param {number} [style.fillGradientType=PIXI.TEXT_GRADIENT.LINEAR_VERTICAL] - If fill is an array of colours + * to create a gradient, this can change the type/direction of the gradient. See {@link PIXI.TEXT_GRADIENT} + * @param {number[]} [style.fillGradientStops] - If fill is an array of colours to create a gradient, this array can set + * the stop points (numbers between 0 and 1) for the color, overriding the default behaviour of evenly spacing them. * @param {string|string[]} [style.fontFamily='Arial'] - The font family * @param {number|string} [style.fontSize=26] - The font size (as a number it converts to px, but as a string, * equivalents are '26px','20pt','160%' or '1.6em') @@ -76,6 +82,7 @@ * e.g 'blue', '#FCFF00' * @param {number} [style.strokeThickness=0] - A number that represents the thickness of the stroke. * Default is 0 (no stroke) + * @param {boolean} [style.trim=false] - Trim transparent borders * @param {string} [style.textBaseline='alphabetic'] - The baseline of the text that is rendered. * @param {boolean} [style.wordWrap=false] - Indicates if word wrap should be used * @param {number} [style.wordWrapWidth=100] - The width at which text will wrap, it needs wordWrap to be set to true @@ -152,6 +159,19 @@ } } + get dropShadowAlpha() + { + return this._dropShadowAlpha; + } + set dropShadowAlpha(dropShadowAlpha) + { + if (this._dropShadowAlpha !== dropShadowAlpha) + { + this._dropShadowAlpha = dropShadowAlpha; + this.styleID++; + } + } + get dropShadowAngle() { return this._dropShadowAngle; @@ -232,6 +252,19 @@ } } + get fillGradientStops() + { + return this._fillGradientStops; + } + set fillGradientStops(fillGradientStops) + { + if (!areArraysEqual(this._fillGradientStops,fillGradientStops)) + { + this._fillGradientStops = fillGradientStops; + this.styleID++; + } + } + get fontFamily() { return this._fontFamily; @@ -402,6 +435,19 @@ } } + get trim() + { + return this._trim; + } + set trim(trim) + { + if (this._trim !== trim) + { + this._trim = trim; + this.styleID++; + } + } + get wordWrap() { return this._wordWrap; @@ -475,3 +521,34 @@ return color; } } + +/** + * Utility function to convert hexadecimal colors to strings, and simply return the color if it's a string. + * This version can also convert array of colors + * + * @param {Array} array1 First array to compare + * @param {Array} array2 Second array to compare + * @return {boolean} Do the arrays contain the same values in the same order + */ +function areArraysEqual(array1, array2) +{ + if (!Array.isArray(array1) || !Array.isArray(array2)) + { + return false; + } + + if (array1.length !== array2.length) + { + return false; + } + + for (let i = 0; i < array1.length; ++i) + { + if (array1[i] !== array2[i]) + { + return false; + } + } + + return true; +} diff --git a/src/core/textures/BaseRenderTexture.js b/src/core/textures/BaseRenderTexture.js index 5d50146..aa727f0 100644 --- a/src/core/textures/BaseRenderTexture.js +++ b/src/core/textures/BaseRenderTexture.js @@ -1,8 +1,6 @@ import BaseTexture from './BaseTexture'; import settings from '../settings'; -const { RESOLUTION, SCALE_MODE } = settings; - /** * A BaseRenderTexture is a special texture that allows any Pixi display object to be rendered to it. * @@ -54,7 +52,7 @@ { super(null, scaleMode); - this.resolution = resolution || RESOLUTION; + this.resolution = resolution || settings.RESOLUTION; this.width = width; this.height = height; @@ -62,7 +60,7 @@ this.realWidth = this.width * this.resolution; this.realHeight = this.height * this.resolution; - this.scaleMode = scaleMode || SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; this.hasLoaded = true; /** diff --git a/src/core/textures/BaseTexture.js b/src/core/textures/BaseTexture.js index 676dca3..aeccf39 100644 --- a/src/core/textures/BaseTexture.js +++ b/src/core/textures/BaseTexture.js @@ -77,7 +77,7 @@ * @default PIXI.settings.SCALE_MODE * @see PIXI.SCALE_MODES */ - this.scaleMode = scaleMode || settings.SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; /** * Set to true once the base texture has successfully loaded. @@ -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/RenderTexture.js b/src/core/textures/RenderTexture.js index 4c5034f..b3c6297 100644 --- a/src/core/textures/RenderTexture.js +++ b/src/core/textures/RenderTexture.js @@ -54,8 +54,8 @@ /* eslint-disable prefer-rest-params, no-console */ const width = arguments[1]; const height = arguments[2]; - const scaleMode = arguments[3] || 0; - const resolution = arguments[4] || 1; + const scaleMode = arguments[3]; + const resolution = arguments[4]; // we have an old render texture.. console.warn(`Please use RenderTexture.create(${width}, ${height}) instead of the ctor directly.`); 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 ca31456..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 @@ -459,7 +497,9 @@ if (frame.x + frame.width > this.baseTexture.width || frame.y + frame.height > this.baseTexture.height) { - throw new Error(`Texture Error: frame does not fit inside the base Texture dimensions ${this}`); + throw new Error('Texture Error: frame does not fit inside the base Texture dimensions: ' + + `X: ${frame.x} + ${frame.width} > ${this.baseTexture.width} ` + + `Y: ${frame.y} + ${frame.height} > ${this.baseTexture.height}`); } // this.valid = frame && frame.width && frame.height && this.baseTexture.source && this.baseTexture.hasLoaded; @@ -520,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. @@ -528,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 2a7497b..99e585a 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)); } /** @@ -360,3 +362,43 @@ * @private */ export const BaseTextureCache = {}; + +/** + * 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/core/utils/trimCanvas.js b/src/core/utils/trimCanvas.js new file mode 100644 index 0000000..5af6de0 --- /dev/null +++ b/src/core/utils/trimCanvas.js @@ -0,0 +1,83 @@ +/** + * Trim transparent borders from a canvas + * + * @memberof PIXI + * @function trimCanvas + * @private + * @param {HTMLCanvasElement} canvas - the canvas to trim + * @returns {object} Trim data + */ +export default function trimCanvas(canvas) +{ + // https://gist.github.com/remy/784508 + + let width = canvas.width; + let height = canvas.height; + + const context = canvas.getContext('2d'); + const imageData = context.getImageData(0, 0, width, height); + const pixels = imageData.data; + const len = pixels.length; + + const bound = { + top: null, + left: null, + right: null, + bottom: null, + }; + let i; + let x; + let y; + + for (i = 0; i < len; i += 4) + { + if (pixels[i + 3] !== 0) + { + x = (i / 4) % width; + y = ~~((i / 4) / width); + + if (bound.top === null) + { + bound.top = y; + } + + if (bound.left === null) + { + bound.left = x; + } + else if (x < bound.left) + { + bound.left = x; + } + + if (bound.right === null) + { + bound.right = x + 1; + } + else if (bound.right < x) + { + bound.right = x + 1; + } + + if (bound.bottom === null) + { + bound.bottom = y; + } + else if (bound.bottom < y) + { + bound.bottom = y; + } + } + } + + width = bound.right - bound.left; + height = bound.bottom - bound.top + 1; + + const data = context.getImageData(bound.left, bound.top, width, height); + + return { + height, + width, + data, + }; +} diff --git a/src/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/extras/TilingSprite.js b/src/extras/TilingSprite.js index f90d248..fe82451 100644 --- a/src/extras/TilingSprite.js +++ b/src/extras/TilingSprite.js @@ -220,20 +220,33 @@ transform.tx * resolution, transform.ty * resolution); - // TODO - this should be rolled into the setTransform above.. - context.scale(this.tileScale.x / baseTextureResolution, this.tileScale.y / baseTextureResolution); - - context.translate(modX + (this.anchor.x * -this._width), - modY + (this.anchor.y * -this._height)); - renderer.setBlendMode(this.blendMode); // fill the pattern! context.fillStyle = this._canvasPattern; - context.fillRect(-modX, - -modY, - this._width / this.tileScale.x * baseTextureResolution, - this._height / this.tileScale.y * baseTextureResolution); + + // TODO - this should be rolled into the setTransform above.. + context.scale(this.tileScale.x / baseTextureResolution, this.tileScale.y / baseTextureResolution); + + const anchorX = this.anchor.x * -this._width; + const anchorY = this.anchor.y * -this._height; + + if (this.uvRespectAnchor) + { + context.translate(modX, modY); + + context.fillRect(-modX + anchorX, -modY + anchorY, + this._width / this.tileScale.x * baseTextureResolution, + this._height / this.tileScale.y * baseTextureResolution); + } + else + { + context.translate(modX + anchorX, modY + anchorY); + + context.fillRect(-modX, -modY, + this._width / this.tileScale.x * baseTextureResolution, + this._height / this.tileScale.y * baseTextureResolution); + } } /** diff --git a/src/extras/cacheAsBitmap.js b/src/extras/cacheAsBitmap.js index 18043a7..5dd5640 100644 --- a/src/extras/cacheAsBitmap.js +++ b/src/extras/cacheAsBitmap.js @@ -41,6 +41,9 @@ * provide a performance benefit for complex static displayObjects. * To remove simply set this property to 'false' * + * IMPORTANT GOTCHA - make sure that all your textures are preloaded BEFORE setting this property to true + * as it will take a snapshot of what is currently there. If the textures have not loaded then they will not appear. + * * @member {boolean} * @memberof PIXI.DisplayObject# */ diff --git a/src/filters/blur/BlurFilter.js b/src/filters/blur/BlurFilter.js index 340fc58..1575db9 100644 --- a/src/filters/blur/BlurFilter.js +++ b/src/filters/blur/BlurFilter.js @@ -24,10 +24,9 @@ this.blurXFilter = new BlurXFilter(strength, quality, resolution, kernelSize); this.blurYFilter = new BlurYFilter(strength, quality, resolution, kernelSize); - this.resolution = 1; this.padding = 0; - this.resolution = resolution || 1; + this.resolution = resolution || core.settings.RESOLUTION; this.quality = quality || 4; this.blur = strength || 8; } diff --git a/src/filters/blur/BlurXFilter.js b/src/filters/blur/BlurXFilter.js index def99c3..0f48889 100644 --- a/src/filters/blur/BlurXFilter.js +++ b/src/filters/blur/BlurXFilter.js @@ -31,7 +31,7 @@ fragSrc ); - this.resolution = resolution || 1; + this.resolution = resolution || core.settings.RESOLUTION; this._quality = 0; diff --git a/src/filters/blur/BlurYFilter.js b/src/filters/blur/BlurYFilter.js index 638e0f0..6d489c7 100644 --- a/src/filters/blur/BlurYFilter.js +++ b/src/filters/blur/BlurYFilter.js @@ -31,7 +31,7 @@ fragSrc ); - this.resolution = resolution || 1; + this.resolution = resolution || core.settings.RESOLUTION; this._quality = 0; diff --git a/src/filters/colormatrix/ColorMatrixFilter.js b/src/filters/colormatrix/ColorMatrixFilter.js index 8509505..679f8ec 100644 --- a/src/filters/colormatrix/ColorMatrixFilter.js +++ b/src/filters/colormatrix/ColorMatrixFilter.js @@ -75,28 +75,28 @@ out[1] = (a[0] * b[1]) + (a[1] * b[6]) + (a[2] * b[11]) + (a[3] * b[16]); out[2] = (a[0] * b[2]) + (a[1] * b[7]) + (a[2] * b[12]) + (a[3] * b[17]); out[3] = (a[0] * b[3]) + (a[1] * b[8]) + (a[2] * b[13]) + (a[3] * b[18]); - out[4] = (a[0] * b[4]) + (a[1] * b[9]) + (a[2] * b[14]) + (a[3] * b[19]); + out[4] = (a[0] * b[4]) + (a[1] * b[9]) + (a[2] * b[14]) + (a[3] * b[19]) + a[4]; // Green Channel out[5] = (a[5] * b[0]) + (a[6] * b[5]) + (a[7] * b[10]) + (a[8] * b[15]); out[6] = (a[5] * b[1]) + (a[6] * b[6]) + (a[7] * b[11]) + (a[8] * b[16]); out[7] = (a[5] * b[2]) + (a[6] * b[7]) + (a[7] * b[12]) + (a[8] * b[17]); out[8] = (a[5] * b[3]) + (a[6] * b[8]) + (a[7] * b[13]) + (a[8] * b[18]); - out[9] = (a[5] * b[4]) + (a[6] * b[9]) + (a[7] * b[14]) + (a[8] * b[19]); + out[9] = (a[5] * b[4]) + (a[6] * b[9]) + (a[7] * b[14]) + (a[8] * b[19]) + a[9]; // Blue Channel out[10] = (a[10] * b[0]) + (a[11] * b[5]) + (a[12] * b[10]) + (a[13] * b[15]); out[11] = (a[10] * b[1]) + (a[11] * b[6]) + (a[12] * b[11]) + (a[13] * b[16]); out[12] = (a[10] * b[2]) + (a[11] * b[7]) + (a[12] * b[12]) + (a[13] * b[17]); out[13] = (a[10] * b[3]) + (a[11] * b[8]) + (a[12] * b[13]) + (a[13] * b[18]); - out[14] = (a[10] * b[4]) + (a[11] * b[9]) + (a[12] * b[14]) + (a[13] * b[19]); + out[14] = (a[10] * b[4]) + (a[11] * b[9]) + (a[12] * b[14]) + (a[13] * b[19]) + a[14]; // Alpha Channel out[15] = (a[15] * b[0]) + (a[16] * b[5]) + (a[17] * b[10]) + (a[18] * b[15]); out[16] = (a[15] * b[1]) + (a[16] * b[6]) + (a[17] * b[11]) + (a[18] * b[16]); out[17] = (a[15] * b[2]) + (a[16] * b[7]) + (a[17] * b[12]) + (a[18] * b[17]); out[18] = (a[15] * b[3]) + (a[16] * b[8]) + (a[17] * b[13]) + (a[18] * b[18]); - out[19] = (a[15] * b[4]) + (a[16] * b[9]) + (a[17] * b[14]) + (a[18] * b[19]); + out[19] = (a[15] * b[4]) + (a[16] * b[9]) + (a[17] * b[14]) + (a[18] * b[19]) + a[19]; return out; } diff --git a/src/filters/colormatrix/colorMatrix.frag b/src/filters/colormatrix/colorMatrix.frag index 3aaf2b3..c73c0e9 100644 --- a/src/filters/colormatrix/colorMatrix.frag +++ b/src/filters/colormatrix/colorMatrix.frag @@ -4,32 +4,38 @@ void main(void) { - vec4 c = texture2D(uSampler, vTextureCoord); + // Un-premultiply alpha before applying the color matrix. See issue #3539. + if (c.a > 0.0) { + c.rgb /= c.a; + } + vec4 result; + result.r = (m[0] * c.r); + result.r += (m[1] * c.g); + result.r += (m[2] * c.b); + result.r += (m[3] * c.a); + result.r += m[4]; - gl_FragColor.r = (m[0] * c.r); - gl_FragColor.r += (m[1] * c.g); - gl_FragColor.r += (m[2] * c.b); - gl_FragColor.r += (m[3] * c.a); - gl_FragColor.r += m[4] * c.a; + result.g = (m[5] * c.r); + result.g += (m[6] * c.g); + result.g += (m[7] * c.b); + result.g += (m[8] * c.a); + result.g += m[9]; - gl_FragColor.g = (m[5] * c.r); - gl_FragColor.g += (m[6] * c.g); - gl_FragColor.g += (m[7] * c.b); - gl_FragColor.g += (m[8] * c.a); - gl_FragColor.g += m[9] * c.a; + result.b = (m[10] * c.r); + result.b += (m[11] * c.g); + result.b += (m[12] * c.b); + result.b += (m[13] * c.a); + result.b += m[14]; - gl_FragColor.b = (m[10] * c.r); - gl_FragColor.b += (m[11] * c.g); - gl_FragColor.b += (m[12] * c.b); - gl_FragColor.b += (m[13] * c.a); - gl_FragColor.b += m[14] * c.a; + result.a = (m[15] * c.r); + result.a += (m[16] * c.g); + result.a += (m[17] * c.b); + result.a += (m[18] * c.a); + result.a += m[19]; - gl_FragColor.a = (m[15] * c.r); - gl_FragColor.a += (m[16] * c.g); - gl_FragColor.a += (m[17] * c.b); - gl_FragColor.a += (m[18] * c.a); - gl_FragColor.a += m[19] * c.a; + // Premultiply alpha again. + result.rgb *= result.a; -// gl_FragColor = vec4(m[0]); + gl_FragColor = result; } diff --git a/src/filters/displacement/DisplacementFilter.js b/src/filters/displacement/DisplacementFilter.js index 112de0b..d5355c3 100644 --- a/src/filters/displacement/DisplacementFilter.js +++ b/src/filters/displacement/DisplacementFilter.js @@ -36,7 +36,7 @@ this.maskMatrix = maskMatrix; this.uniforms.mapSampler = sprite.texture; - this.uniforms.filterMatrix = maskMatrix.toArray(true); + this.uniforms.filterMatrix = maskMatrix; this.uniforms.scale = { x: 1, y: 1 }; if (scale === null || scale === undefined) diff --git a/src/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/InteractionData.js b/src/interaction/InteractionData.js index 02d53a3..818da35 100644 --- a/src/interaction/InteractionData.js +++ b/src/interaction/InteractionData.js @@ -21,9 +21,9 @@ this.global = new core.Point(); /** - * The target Sprite that was interacted with + * The target DisplayObject that was interacted with * - * @member {PIXI.Sprite} + * @member {PIXI.DisplayObject} */ this.target = null; @@ -33,6 +33,13 @@ * @member {Event} */ this.originalEvent = null; + + /** + * Unique identifier for this interaction + * + * @member {number} + */ + this.identifier = null; } /** diff --git a/src/interaction/InteractionEvent.js b/src/interaction/InteractionEvent.js index 75b2966..dc554f9 100644 --- a/src/interaction/InteractionEvent.js +++ b/src/interaction/InteractionEvent.js @@ -58,7 +58,7 @@ } /** - * Prevents event from reaching any objects other than the current object. + * Resets the event. * * @private */ diff --git a/src/interaction/InteractionManager.js b/src/interaction/InteractionManager.js index 7d51134..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 1271fef..71ade15 100644 --- a/package.json +++ b/package.json @@ -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/scripts/release.js b/scripts/release.js index c4c8e36..156620c 100644 --- a/scripts/release.js +++ b/scripts/release.js @@ -12,7 +12,8 @@ 'dist/**/*', 'lib/**/*', 'src/**/*', - 'scripts/**/*', + 'scripts/*', + 'scripts/renders/*', 'test/**/*', '*.json', '*.md', diff --git a/scripts/renders/client.js b/scripts/renders/client.js index 16c9565..6faee34 100755 --- a/scripts/renders/client.js +++ b/scripts/renders/client.js @@ -1,6 +1,6 @@ 'use strict'; -require('../../bin/pixi'); +require('../../dist/pixi'); PIXI.utils.skipHello(); const fs = require('fs'); 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 bc730db..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. @@ -33,8 +33,11 @@ * need to call toDataUrl on the webgl context * @param {number} [options.resolution=1] - The resolution / device pixel ratio of the renderer, retina would be 2 * @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 @@ -43,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. */ @@ -73,7 +99,7 @@ */ stop() { - this.ticker.stop(); + this._ticker.stop(); } /** @@ -81,7 +107,7 @@ */ start() { - this.ticker.start(); + this._ticker.start(); } /** @@ -95,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 index 81e076b..78b17e6 100644 --- a/src/core/Shader.js +++ b/src/core/Shader.js @@ -1,9 +1,7 @@ import { GLShader } from 'pixi-gl-core'; import settings from './settings'; -const { PRECISION } = settings; - -function checkPrecision(src) +function checkPrecision(src, def) { if (src instanceof Array) { @@ -11,14 +9,14 @@ { const copy = src.slice(0); - copy.unshift(`precision ${PRECISION} float;`); + copy.unshift(`precision ${def} float;`); return copy; } } else if (src.substring(0, 9) !== 'precision') { - return `precision ${PRECISION} float;\n${src}`; + return `precision ${def} float;\n${src}`; } return src; @@ -42,6 +40,7 @@ */ constructor(gl, vertexSrc, fragmentSrc) { - super(gl, checkPrecision(vertexSrc), checkPrecision(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 3425b41..f9235e4 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -181,7 +181,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 b3792ef..17fde8d 100644 --- a/src/core/display/Container.js +++ b/src/core/display/Container.js @@ -73,13 +73,14 @@ } child.parent = this; - - // ensure a transform will be recalculated.. - this.transform._parentID = -1; - this._boundsID++; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.push(child); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(this.children.length - 1); child.emit('added', this); @@ -108,9 +109,14 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.splice(index, 0, child); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(index); child.emit('added', this); @@ -174,6 +180,7 @@ removeItems(this.children, currentIndex, 1); // remove from old position this.children.splice(index, 0, child); // add at new position + this.onChildrenChange(index); } @@ -220,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! @@ -244,9 +252,14 @@ { const child = this.getChildAt(index); + // ensure child transform will be recalculated.. child.parent = null; + child.transform._parentID = -1; removeItems(this.children, index, 1); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(index); child.emit('removed', this); @@ -275,8 +288,14 @@ for (let i = 0; i < removed.length; ++i) { removed[i].parent = null; + if (removed[i].transform) + { + removed[i].transform._parentID = -1; + } } + this._boundsID++; + this.onChildrenChange(beginIndex); for (let i = 0; i < removed.length; ++i) diff --git a/src/core/display/DisplayObject.js b/src/core/display/DisplayObject.js index 3affe36..4e6c77d 100644 --- a/src/core/display/DisplayObject.js +++ b/src/core/display/DisplayObject.js @@ -109,7 +109,7 @@ /** * The original, cached mask of the object * - * @member {PIXI.Rectangle} + * @member {PIXI.Graphics|PIXI.Sprite} * @private */ this._mask = null; diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index 95f2af4..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 ); @@ -1065,10 +1061,15 @@ canvasRenderer = new CanvasRenderer(); } - tempMatrix.tx = -bounds.x; - tempMatrix.ty = -bounds.y; + this.transform.updateLocalTransform(); + this.transform.localTransform.copy(tempMatrix); - canvasRenderer.render(this, canvasBuffer, false, tempMatrix); + tempMatrix.invert(); + + tempMatrix.tx -= bounds.x; + tempMatrix.ty -= bounds.y; + + canvasRenderer.render(this, canvasBuffer, true, tempMatrix); const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode); 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 e67680f..867dde2 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/buildCircle.js b/src/core/graphics/webgl/utils/buildCircle.js index 0400d13..abf5d02 100644 --- a/src/core/graphics/webgl/utils/buildCircle.js +++ b/src/core/graphics/webgl/utils/buildCircle.js @@ -33,6 +33,11 @@ height = circleData.height; } + if (width === 0 || height === 0) + { + return; + } + const totalSegs = Math.floor(30 * Math.sqrt(circleData.radius)) || Math.floor(15 * Math.sqrt(circleData.width + circleData.height)); diff --git a/src/core/graphics/webgl/utils/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 90155f9..f03fe15 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -30,6 +30,7 @@ export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; export { default as CanvasGraphicsRenderer } from './graphics/canvas/CanvasGraphicsRenderer'; +export { default as Spritesheet } from './textures/Spritesheet'; export { default as Texture } from './textures/Texture'; export { default as BaseTexture } from './textures/BaseTexture'; export { default as RenderTexture } from './textures/RenderTexture'; diff --git a/src/core/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 ccce680..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); } /** @@ -162,6 +162,9 @@ if (transform) { transform.copy(tempWt); + + // lets not forget to flag the parent transform as dirty... + this._tempDisplayObjectParent.transform._worldID = -1; } else { @@ -169,6 +172,7 @@ } displayObject.parent = this._tempDisplayObjectParent; + displayObject.updateTransform(); displayObject.parent = cacheParent; // displayObject.hitArea = //TODO add a temp hit area @@ -279,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/canvas/utils/CanvasRenderTarget.js b/src/core/renderers/canvas/utils/CanvasRenderTarget.js index 3de0aef..b409891 100644 --- a/src/core/renderers/canvas/utils/CanvasRenderTarget.js +++ b/src/core/renderers/canvas/utils/CanvasRenderTarget.js @@ -1,5 +1,4 @@ import settings from '../../../settings'; -const { RESOLUTION } = settings; /** * Creates a Canvas element of the given size. @@ -30,7 +29,7 @@ */ this.context = this.canvas.getContext('2d'); - this.resolution = resolution || RESOLUTION; + this.resolution = resolution || settings.RESOLUTION; this.resize(width, height); } 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 ce0e1c4..788ed6c 100644 --- a/src/core/renderers/webgl/WebGLRenderer.js +++ b/src/core/renderers/webgl/WebGLRenderer.js @@ -30,8 +30,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 @@ -49,10 +49,19 @@ * enable this if you need to call toDataUrl on the webgl context. * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when * rendering, stopping pixel interpolation. + * @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; + + if (this.legacy) + { + glCore.VertexArrayObject.FORCE_NATIVE = true; + } /** * The type of this renderer as a standardised const @@ -229,7 +238,7 @@ this.emit('context', gl); // setup the width/height properties and gl viewport - this.resize(this.width, this.height); + this.resize(this.screen.width, this.screen.height); } /** @@ -323,16 +332,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) { @@ -376,6 +385,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 @@ -440,9 +469,10 @@ * Changes the current shader to the one given in parameter * * @param {PIXI.Shader} shader - the new shader + * @param {boolean} [autoProject=true] - Whether automatically set the projection matrix * @return {PIXI.WebGLRenderer} Returns itself. */ - bindShader(shader) + bindShader(shader, autoProject) { // TODO cache if (this._activeShader !== shader) @@ -450,8 +480,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 bbab3b3..944f41d 100644 --- a/src/core/renderers/webgl/filters/Filter.js +++ b/src/core/renderers/webgl/filters/Filter.js @@ -1,6 +1,7 @@ import extractUniformsFromSrc from './extractUniformsFromSrc'; import { uid } from '../../../utils'; import { BLEND_MODES } from '../../../const'; +import settings from '../../../settings'; const SOURCE_KEY_MAP = {}; @@ -10,7 +11,7 @@ * @memberof PIXI * @extends PIXI.Shader */ -class Filter +export default class Filter { /** * @param {string} [vertexSrc] - The source of the vertex shader. @@ -35,8 +36,6 @@ this.blendMode = BLEND_MODES.NORMAL; - // pull out the vertex and shader uniforms if they are not specified.. - // currently this does not extract structs only default types this.uniformData = uniforms || extractUniformsFromSrc(this.vertexSrc, this.fragmentSrc, 'projectionMatrix|uSampler'); /** @@ -80,7 +79,7 @@ * * @member {number} */ - this.resolution = 1; + this.resolution = settings.RESOLUTION; /** * If enabled is true the filter is applied, if false it will not. @@ -97,8 +96,11 @@ * @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 { // --- // // this.uniforms.filterMatrix = filterManager.calculateSpriteMatrix(tempMatrix, window.panda ); @@ -170,5 +172,3 @@ ].join('\n'); } } - -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 dc7072a..6c7422d 100644 --- a/src/core/renderers/webgl/managers/FilterManager.js +++ b/src/core/renderers/webgl/managers/FilterManager.js @@ -154,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 @@ -176,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; @@ -184,7 +184,7 @@ flop = t; } - filters[i].apply(this, flip, lastState.renderTarget, true); + filters[i].apply(this, flip, lastState.renderTarget, false, currentState); this.freePotRenderTarget(flip); this.freePotRenderTarget(flop); @@ -291,9 +291,13 @@ let textureCount = 1; let currentState; - if (shader.uniforms.data.filterArea) + // 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; @@ -306,9 +310,9 @@ // use this to clamp displaced texture coords so they belong to filterArea // see displacementFilter fragment shader for an example - if (shader.uniforms.data.filterClamp) + if (shader.uniforms.filterClamp) { - currentState = this.filterData.stack[this.filterData.index]; + currentState = currentState || this.filterData.stack[this.filterData.index]; const filterClamp = shader.uniforms.filterClamp; @@ -485,7 +489,7 @@ */ destroy() { - this.shaderCache = []; + this.shaderCache = {}; this.emptyPool(); } diff --git a/src/core/renderers/webgl/utils/RenderTarget.js b/src/core/renderers/webgl/utils/RenderTarget.js index afe51ab..6d2467f 100644 --- a/src/core/renderers/webgl/utils/RenderTarget.js +++ b/src/core/renderers/webgl/utils/RenderTarget.js @@ -124,7 +124,7 @@ * @default PIXI.settings.SCALE_MODE * @see PIXI.SCALE_MODES */ - this.scaleMode = scaleMode || settings.SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; /** * Whether this object is the root element or not 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 36ea5bb..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; @@ -346,8 +346,8 @@ /** * Gets the local bounds of the sprite object. * - * @param {Rectangle} rect - The output rectangle. - * @return {Rectangle} The bounds. + * @param {PIXI.Rectangle} rect - The output rectangle. + * @return {PIXI.Rectangle} The bounds. */ getLocalBounds(rect) { @@ -440,7 +440,7 @@ * * @static * @param {number|string|PIXI.BaseTexture|HTMLCanvasElement|HTMLVideoElement} source Source to create texture from - * @return {PIXI.Texture} 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 e77b39a..90b08c0 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++) @@ -103,11 +102,18 @@ { const gl = this.renderer.gl; - // step 1: first check max textures the GPU can handle. - this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); + if (this.renderer.legacy) + { + this.MAX_TEXTURES = 1; + } + else + { + // step 1: first check max textures the GPU can handle. + this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); - // step 2: check the maximum number of if statements the shader can have too.. - this.MAX_TEXTURES = checkMaxIfStatmentsInShader(this.MAX_TEXTURES, gl); + // step 2: check the maximum number of if statements the shader can have too.. + this.MAX_TEXTURES = checkMaxIfStatmentsInShader(this.MAX_TEXTURES, gl); + } const shader = this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); @@ -130,8 +136,12 @@ .addIndex(this.indexBuffer) .addAttribute(this.vertexBuffers[i], shader.attributes.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) .addAttribute(this.vertexBuffers[i], shader.attributes.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) - .addAttribute(this.vertexBuffers[i], shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4) - .addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + .addAttribute(this.vertexBuffers[i], shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); + + if (shader.attributes.aTextureId) + { + this.vaos[i].addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + } /* eslint-enable max-len */ } @@ -378,8 +388,12 @@ .addIndex(this.indexBuffer) .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) - .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4) - .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); + + if (this.shader.attributes.aTextureId) + { + this.vaos[this.vertexCount].addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + } /* eslint-enable max-len */ } diff --git a/src/core/text/Text.js b/src/core/text/Text.js index 013625a..cec8c32 100644 --- a/src/core/text/Text.js +++ b/src/core/text/Text.js @@ -6,6 +6,7 @@ import { TEXT_GRADIENT } from '../const'; import settings from '../settings'; import TextStyle from './TextStyle'; +import trimCanvas from '../utils/trimCanvas'; const defaultDestroyOptions = { texture: true, @@ -57,7 +58,7 @@ /** * The canvas 2d context that everything is drawn with - * @member {HTMLCanvasElement} + * @member {CanvasRenderingContext2D} */ this.context = this.canvas.getContext('2d'); @@ -189,10 +190,12 @@ if (style.dropShadow) { + this.context.shadowBlur = style.dropShadowBlur; + this.context.globalAlpha = style.dropShadowAlpha; + if (style.dropShadowBlur > 0) { this.context.shadowColor = style.dropShadowColor; - this.context.shadowBlur = style.dropShadowBlur; } else { @@ -237,6 +240,10 @@ } } + // reset the shadow blur and alpha that was set by the drop shadow, for the regular text + this.context.shadowBlur = 0; + this.context.globalAlpha = 1; + // set canvas text styles this.context.fillStyle = this._generateFillStyle(style, lines); @@ -326,6 +333,15 @@ */ updateTexture() { + if (this._style.trim) + { + const trimmed = trimCanvas(this.canvas); + + this.canvas.width = trimmed.width; + this.canvas.height = trimmed.height; + this.context.putImageData(trimmed.data, 0, 0); + } + const texture = this._texture; const style = this._style; @@ -475,6 +491,19 @@ } /** + * Gets the local bounds of the text object. + * + * @param {Rectangle} rect - The output rectangle. + * @return {Rectangle} The bounds. + */ + getLocalBounds(rect) + { + this.updateText(true); + + return super.getLocalBounds.call(this, rect); + } + + /** * calculates the bounds of the Text as a rectangle. The bounds calculation takes the worldTransform into account. */ _calculateBounds() @@ -525,6 +554,29 @@ const width = this.canvas.width / this.resolution; const height = this.canvas.height / this.resolution; + // make a copy of the style settings, so we can manipulate them later + const fill = style.fill.slice(); + const fillGradientStops = style.fillGradientStops.slice(); + + // wanting to evenly distribute the fills. So an array of 4 colours should give fills of 0.25, 0.5 and 0.75 + if (!fillGradientStops.length) + { + const lengthPlus1 = fill.length + 1; + + for (let i = 1; i < lengthPlus1; ++i) + { + fillGradientStops.push(i / lengthPlus1); + } + } + + // stop the bleeding of the last gradient on the line above to the top gradient of the this line + // by hard defining the first gradient colour at point 0, and last gradient colour at point 1 + fill.unshift(style.fill[0]); + fillGradientStops.unshift(0); + + fill.push(style.fill[style.fill.length - 1]); + fillGradientStops.push(1); + if (style.fillGradientType === TEXT_GRADIENT.LINEAR_VERTICAL) { // start the gradient at the top center of the canvas, and end at the bottom middle of the canvas @@ -532,15 +584,22 @@ // we need to repeat the gradient so that each individual line of text has the same vertical gradient effect // ['#FF0000', '#00FF00', '#0000FF'] over 2 lines would create stops at 0.125, 0.25, 0.375, 0.625, 0.75, 0.875 - totalIterations = (style.fill.length + 1) * lines.length; + totalIterations = (fill.length + 1) * lines.length; currentIteration = 0; for (let i = 0; i < lines.length; i++) { currentIteration += 1; - for (let j = 0; j < style.fill.length; j++) + for (let j = 0; j < fill.length; j++) { - stop = (currentIteration / totalIterations); - gradient.addColorStop(stop, style.fill[j]); + if (fillGradientStops[j]) + { + stop = (fillGradientStops[j] / lines.length) + (i / lines.length); + } + else + { + stop = currentIteration / totalIterations; + } + gradient.addColorStop(stop, fill[j]); currentIteration++; } } @@ -552,13 +611,20 @@ // can just evenly space out the gradients in this case, as multiple lines makes no difference // to an even left to right gradient - totalIterations = style.fill.length + 1; + totalIterations = fill.length + 1; currentIteration = 1; - for (let i = 0; i < style.fill.length; i++) + for (let i = 0; i < fill.length; i++) { - stop = currentIteration / totalIterations; - gradient.addColorStop(stop, style.fill[i]); + if (fillGradientStops[i]) + { + stop = fillGradientStops[i]; + } + else + { + stop = currentIteration / totalIterations; + } + gradient.addColorStop(stop, fill[i]); currentIteration++; } } @@ -569,7 +635,7 @@ /** * Destroys this text object. * Note* Unlike a Sprite, a Text object will automatically destroy its baseTexture and texture as - * the majorety of the time the texture will not be shared with any other Sprites. + * the majority of the time the texture will not be shared with any other Sprites. * * @param {object|boolean} [options] - Options parameter. A boolean will act as if all options * have been set to that value @@ -680,7 +746,7 @@ set text(text) // eslint-disable-line require-jsdoc { - text = String(text || ' '); + text = String(text === '' || text === null || text === undefined ? ' ' : text); if (this._text === text) { diff --git a/src/core/text/TextStyle.js b/src/core/text/TextStyle.js index 3b25528..890950b 100644 --- a/src/core/text/TextStyle.js +++ b/src/core/text/TextStyle.js @@ -8,12 +8,14 @@ align: 'left', breakWords: false, dropShadow: false, + dropShadowAlpha: 1, dropShadowAngle: Math.PI / 6, dropShadowBlur: 0, dropShadowColor: '#000000', dropShadowDistance: 5, fill: 'black', fillGradientType: TEXT_GRADIENT.LINEAR_VERTICAL, + fillGradientStops: [], fontFamily: 'Arial', fontSize: 26, fontStyle: 'normal', @@ -27,6 +29,7 @@ stroke: 'black', strokeThickness: 0, textBaseline: 'alphabetic', + trim: false, wordWrap: false, wordWrapWidth: 100, }; @@ -47,6 +50,7 @@ * @param {boolean} [style.breakWords=false] - Indicates if lines can be wrapped within words, it * needs wordWrap to be set to true * @param {boolean} [style.dropShadow=false] - Set a drop shadow for the text + * @param {number} [style.dropShadowAlpha=1] - Set alpha for the drop shadow * @param {number} [style.dropShadowAngle=Math.PI/6] - Set a angle of the drop shadow * @param {number} [style.dropShadowBlur=0] - Set a shadow blur radius * @param {string} [style.dropShadowColor='#000000'] - A fill style to be used on the dropshadow e.g 'red', '#00FF00' @@ -55,8 +59,10 @@ * fillstyle that will be used on the text e.g 'red', '#00FF00'. Can be an array to create a gradient * eg ['#000000','#FFFFFF'] * {@link https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/fillStyle|MDN} - * @param {number} [style.fillGradientType=PIXI.TEXT_GRADIENT.LINEAR_VERTICAL] - If fills styles are - * supplied, this can change the type/direction of the gradient. See {@link PIXI.TEXT_GRADIENT} for possible values + * @param {number} [style.fillGradientType=PIXI.TEXT_GRADIENT.LINEAR_VERTICAL] - If fill is an array of colours + * to create a gradient, this can change the type/direction of the gradient. See {@link PIXI.TEXT_GRADIENT} + * @param {number[]} [style.fillGradientStops] - If fill is an array of colours to create a gradient, this array can set + * the stop points (numbers between 0 and 1) for the color, overriding the default behaviour of evenly spacing them. * @param {string|string[]} [style.fontFamily='Arial'] - The font family * @param {number|string} [style.fontSize=26] - The font size (as a number it converts to px, but as a string, * equivalents are '26px','20pt','160%' or '1.6em') @@ -76,6 +82,7 @@ * e.g 'blue', '#FCFF00' * @param {number} [style.strokeThickness=0] - A number that represents the thickness of the stroke. * Default is 0 (no stroke) + * @param {boolean} [style.trim=false] - Trim transparent borders * @param {string} [style.textBaseline='alphabetic'] - The baseline of the text that is rendered. * @param {boolean} [style.wordWrap=false] - Indicates if word wrap should be used * @param {number} [style.wordWrapWidth=100] - The width at which text will wrap, it needs wordWrap to be set to true @@ -152,6 +159,19 @@ } } + get dropShadowAlpha() + { + return this._dropShadowAlpha; + } + set dropShadowAlpha(dropShadowAlpha) + { + if (this._dropShadowAlpha !== dropShadowAlpha) + { + this._dropShadowAlpha = dropShadowAlpha; + this.styleID++; + } + } + get dropShadowAngle() { return this._dropShadowAngle; @@ -232,6 +252,19 @@ } } + get fillGradientStops() + { + return this._fillGradientStops; + } + set fillGradientStops(fillGradientStops) + { + if (!areArraysEqual(this._fillGradientStops,fillGradientStops)) + { + this._fillGradientStops = fillGradientStops; + this.styleID++; + } + } + get fontFamily() { return this._fontFamily; @@ -402,6 +435,19 @@ } } + get trim() + { + return this._trim; + } + set trim(trim) + { + if (this._trim !== trim) + { + this._trim = trim; + this.styleID++; + } + } + get wordWrap() { return this._wordWrap; @@ -475,3 +521,34 @@ return color; } } + +/** + * Utility function to convert hexadecimal colors to strings, and simply return the color if it's a string. + * This version can also convert array of colors + * + * @param {Array} array1 First array to compare + * @param {Array} array2 Second array to compare + * @return {boolean} Do the arrays contain the same values in the same order + */ +function areArraysEqual(array1, array2) +{ + if (!Array.isArray(array1) || !Array.isArray(array2)) + { + return false; + } + + if (array1.length !== array2.length) + { + return false; + } + + for (let i = 0; i < array1.length; ++i) + { + if (array1[i] !== array2[i]) + { + return false; + } + } + + return true; +} diff --git a/src/core/textures/BaseRenderTexture.js b/src/core/textures/BaseRenderTexture.js index 5d50146..aa727f0 100644 --- a/src/core/textures/BaseRenderTexture.js +++ b/src/core/textures/BaseRenderTexture.js @@ -1,8 +1,6 @@ import BaseTexture from './BaseTexture'; import settings from '../settings'; -const { RESOLUTION, SCALE_MODE } = settings; - /** * A BaseRenderTexture is a special texture that allows any Pixi display object to be rendered to it. * @@ -54,7 +52,7 @@ { super(null, scaleMode); - this.resolution = resolution || RESOLUTION; + this.resolution = resolution || settings.RESOLUTION; this.width = width; this.height = height; @@ -62,7 +60,7 @@ this.realWidth = this.width * this.resolution; this.realHeight = this.height * this.resolution; - this.scaleMode = scaleMode || SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; this.hasLoaded = true; /** diff --git a/src/core/textures/BaseTexture.js b/src/core/textures/BaseTexture.js index 676dca3..aeccf39 100644 --- a/src/core/textures/BaseTexture.js +++ b/src/core/textures/BaseTexture.js @@ -77,7 +77,7 @@ * @default PIXI.settings.SCALE_MODE * @see PIXI.SCALE_MODES */ - this.scaleMode = scaleMode || settings.SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; /** * Set to true once the base texture has successfully loaded. @@ -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/RenderTexture.js b/src/core/textures/RenderTexture.js index 4c5034f..b3c6297 100644 --- a/src/core/textures/RenderTexture.js +++ b/src/core/textures/RenderTexture.js @@ -54,8 +54,8 @@ /* eslint-disable prefer-rest-params, no-console */ const width = arguments[1]; const height = arguments[2]; - const scaleMode = arguments[3] || 0; - const resolution = arguments[4] || 1; + const scaleMode = arguments[3]; + const resolution = arguments[4]; // we have an old render texture.. console.warn(`Please use RenderTexture.create(${width}, ${height}) instead of the ctor directly.`); 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 ca31456..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 @@ -459,7 +497,9 @@ if (frame.x + frame.width > this.baseTexture.width || frame.y + frame.height > this.baseTexture.height) { - throw new Error(`Texture Error: frame does not fit inside the base Texture dimensions ${this}`); + throw new Error('Texture Error: frame does not fit inside the base Texture dimensions: ' + + `X: ${frame.x} + ${frame.width} > ${this.baseTexture.width} ` + + `Y: ${frame.y} + ${frame.height} > ${this.baseTexture.height}`); } // this.valid = frame && frame.width && frame.height && this.baseTexture.source && this.baseTexture.hasLoaded; @@ -520,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. @@ -528,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 2a7497b..99e585a 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)); } /** @@ -360,3 +362,43 @@ * @private */ export const BaseTextureCache = {}; + +/** + * 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/core/utils/trimCanvas.js b/src/core/utils/trimCanvas.js new file mode 100644 index 0000000..5af6de0 --- /dev/null +++ b/src/core/utils/trimCanvas.js @@ -0,0 +1,83 @@ +/** + * Trim transparent borders from a canvas + * + * @memberof PIXI + * @function trimCanvas + * @private + * @param {HTMLCanvasElement} canvas - the canvas to trim + * @returns {object} Trim data + */ +export default function trimCanvas(canvas) +{ + // https://gist.github.com/remy/784508 + + let width = canvas.width; + let height = canvas.height; + + const context = canvas.getContext('2d'); + const imageData = context.getImageData(0, 0, width, height); + const pixels = imageData.data; + const len = pixels.length; + + const bound = { + top: null, + left: null, + right: null, + bottom: null, + }; + let i; + let x; + let y; + + for (i = 0; i < len; i += 4) + { + if (pixels[i + 3] !== 0) + { + x = (i / 4) % width; + y = ~~((i / 4) / width); + + if (bound.top === null) + { + bound.top = y; + } + + if (bound.left === null) + { + bound.left = x; + } + else if (x < bound.left) + { + bound.left = x; + } + + if (bound.right === null) + { + bound.right = x + 1; + } + else if (bound.right < x) + { + bound.right = x + 1; + } + + if (bound.bottom === null) + { + bound.bottom = y; + } + else if (bound.bottom < y) + { + bound.bottom = y; + } + } + } + + width = bound.right - bound.left; + height = bound.bottom - bound.top + 1; + + const data = context.getImageData(bound.left, bound.top, width, height); + + return { + height, + width, + data, + }; +} diff --git a/src/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/extras/TilingSprite.js b/src/extras/TilingSprite.js index f90d248..fe82451 100644 --- a/src/extras/TilingSprite.js +++ b/src/extras/TilingSprite.js @@ -220,20 +220,33 @@ transform.tx * resolution, transform.ty * resolution); - // TODO - this should be rolled into the setTransform above.. - context.scale(this.tileScale.x / baseTextureResolution, this.tileScale.y / baseTextureResolution); - - context.translate(modX + (this.anchor.x * -this._width), - modY + (this.anchor.y * -this._height)); - renderer.setBlendMode(this.blendMode); // fill the pattern! context.fillStyle = this._canvasPattern; - context.fillRect(-modX, - -modY, - this._width / this.tileScale.x * baseTextureResolution, - this._height / this.tileScale.y * baseTextureResolution); + + // TODO - this should be rolled into the setTransform above.. + context.scale(this.tileScale.x / baseTextureResolution, this.tileScale.y / baseTextureResolution); + + const anchorX = this.anchor.x * -this._width; + const anchorY = this.anchor.y * -this._height; + + if (this.uvRespectAnchor) + { + context.translate(modX, modY); + + context.fillRect(-modX + anchorX, -modY + anchorY, + this._width / this.tileScale.x * baseTextureResolution, + this._height / this.tileScale.y * baseTextureResolution); + } + else + { + context.translate(modX + anchorX, modY + anchorY); + + context.fillRect(-modX, -modY, + this._width / this.tileScale.x * baseTextureResolution, + this._height / this.tileScale.y * baseTextureResolution); + } } /** diff --git a/src/extras/cacheAsBitmap.js b/src/extras/cacheAsBitmap.js index 18043a7..5dd5640 100644 --- a/src/extras/cacheAsBitmap.js +++ b/src/extras/cacheAsBitmap.js @@ -41,6 +41,9 @@ * provide a performance benefit for complex static displayObjects. * To remove simply set this property to 'false' * + * IMPORTANT GOTCHA - make sure that all your textures are preloaded BEFORE setting this property to true + * as it will take a snapshot of what is currently there. If the textures have not loaded then they will not appear. + * * @member {boolean} * @memberof PIXI.DisplayObject# */ diff --git a/src/filters/blur/BlurFilter.js b/src/filters/blur/BlurFilter.js index 340fc58..1575db9 100644 --- a/src/filters/blur/BlurFilter.js +++ b/src/filters/blur/BlurFilter.js @@ -24,10 +24,9 @@ this.blurXFilter = new BlurXFilter(strength, quality, resolution, kernelSize); this.blurYFilter = new BlurYFilter(strength, quality, resolution, kernelSize); - this.resolution = 1; this.padding = 0; - this.resolution = resolution || 1; + this.resolution = resolution || core.settings.RESOLUTION; this.quality = quality || 4; this.blur = strength || 8; } diff --git a/src/filters/blur/BlurXFilter.js b/src/filters/blur/BlurXFilter.js index def99c3..0f48889 100644 --- a/src/filters/blur/BlurXFilter.js +++ b/src/filters/blur/BlurXFilter.js @@ -31,7 +31,7 @@ fragSrc ); - this.resolution = resolution || 1; + this.resolution = resolution || core.settings.RESOLUTION; this._quality = 0; diff --git a/src/filters/blur/BlurYFilter.js b/src/filters/blur/BlurYFilter.js index 638e0f0..6d489c7 100644 --- a/src/filters/blur/BlurYFilter.js +++ b/src/filters/blur/BlurYFilter.js @@ -31,7 +31,7 @@ fragSrc ); - this.resolution = resolution || 1; + this.resolution = resolution || core.settings.RESOLUTION; this._quality = 0; diff --git a/src/filters/colormatrix/ColorMatrixFilter.js b/src/filters/colormatrix/ColorMatrixFilter.js index 8509505..679f8ec 100644 --- a/src/filters/colormatrix/ColorMatrixFilter.js +++ b/src/filters/colormatrix/ColorMatrixFilter.js @@ -75,28 +75,28 @@ out[1] = (a[0] * b[1]) + (a[1] * b[6]) + (a[2] * b[11]) + (a[3] * b[16]); out[2] = (a[0] * b[2]) + (a[1] * b[7]) + (a[2] * b[12]) + (a[3] * b[17]); out[3] = (a[0] * b[3]) + (a[1] * b[8]) + (a[2] * b[13]) + (a[3] * b[18]); - out[4] = (a[0] * b[4]) + (a[1] * b[9]) + (a[2] * b[14]) + (a[3] * b[19]); + out[4] = (a[0] * b[4]) + (a[1] * b[9]) + (a[2] * b[14]) + (a[3] * b[19]) + a[4]; // Green Channel out[5] = (a[5] * b[0]) + (a[6] * b[5]) + (a[7] * b[10]) + (a[8] * b[15]); out[6] = (a[5] * b[1]) + (a[6] * b[6]) + (a[7] * b[11]) + (a[8] * b[16]); out[7] = (a[5] * b[2]) + (a[6] * b[7]) + (a[7] * b[12]) + (a[8] * b[17]); out[8] = (a[5] * b[3]) + (a[6] * b[8]) + (a[7] * b[13]) + (a[8] * b[18]); - out[9] = (a[5] * b[4]) + (a[6] * b[9]) + (a[7] * b[14]) + (a[8] * b[19]); + out[9] = (a[5] * b[4]) + (a[6] * b[9]) + (a[7] * b[14]) + (a[8] * b[19]) + a[9]; // Blue Channel out[10] = (a[10] * b[0]) + (a[11] * b[5]) + (a[12] * b[10]) + (a[13] * b[15]); out[11] = (a[10] * b[1]) + (a[11] * b[6]) + (a[12] * b[11]) + (a[13] * b[16]); out[12] = (a[10] * b[2]) + (a[11] * b[7]) + (a[12] * b[12]) + (a[13] * b[17]); out[13] = (a[10] * b[3]) + (a[11] * b[8]) + (a[12] * b[13]) + (a[13] * b[18]); - out[14] = (a[10] * b[4]) + (a[11] * b[9]) + (a[12] * b[14]) + (a[13] * b[19]); + out[14] = (a[10] * b[4]) + (a[11] * b[9]) + (a[12] * b[14]) + (a[13] * b[19]) + a[14]; // Alpha Channel out[15] = (a[15] * b[0]) + (a[16] * b[5]) + (a[17] * b[10]) + (a[18] * b[15]); out[16] = (a[15] * b[1]) + (a[16] * b[6]) + (a[17] * b[11]) + (a[18] * b[16]); out[17] = (a[15] * b[2]) + (a[16] * b[7]) + (a[17] * b[12]) + (a[18] * b[17]); out[18] = (a[15] * b[3]) + (a[16] * b[8]) + (a[17] * b[13]) + (a[18] * b[18]); - out[19] = (a[15] * b[4]) + (a[16] * b[9]) + (a[17] * b[14]) + (a[18] * b[19]); + out[19] = (a[15] * b[4]) + (a[16] * b[9]) + (a[17] * b[14]) + (a[18] * b[19]) + a[19]; return out; } diff --git a/src/filters/colormatrix/colorMatrix.frag b/src/filters/colormatrix/colorMatrix.frag index 3aaf2b3..c73c0e9 100644 --- a/src/filters/colormatrix/colorMatrix.frag +++ b/src/filters/colormatrix/colorMatrix.frag @@ -4,32 +4,38 @@ void main(void) { - vec4 c = texture2D(uSampler, vTextureCoord); + // Un-premultiply alpha before applying the color matrix. See issue #3539. + if (c.a > 0.0) { + c.rgb /= c.a; + } + vec4 result; + result.r = (m[0] * c.r); + result.r += (m[1] * c.g); + result.r += (m[2] * c.b); + result.r += (m[3] * c.a); + result.r += m[4]; - gl_FragColor.r = (m[0] * c.r); - gl_FragColor.r += (m[1] * c.g); - gl_FragColor.r += (m[2] * c.b); - gl_FragColor.r += (m[3] * c.a); - gl_FragColor.r += m[4] * c.a; + result.g = (m[5] * c.r); + result.g += (m[6] * c.g); + result.g += (m[7] * c.b); + result.g += (m[8] * c.a); + result.g += m[9]; - gl_FragColor.g = (m[5] * c.r); - gl_FragColor.g += (m[6] * c.g); - gl_FragColor.g += (m[7] * c.b); - gl_FragColor.g += (m[8] * c.a); - gl_FragColor.g += m[9] * c.a; + result.b = (m[10] * c.r); + result.b += (m[11] * c.g); + result.b += (m[12] * c.b); + result.b += (m[13] * c.a); + result.b += m[14]; - gl_FragColor.b = (m[10] * c.r); - gl_FragColor.b += (m[11] * c.g); - gl_FragColor.b += (m[12] * c.b); - gl_FragColor.b += (m[13] * c.a); - gl_FragColor.b += m[14] * c.a; + result.a = (m[15] * c.r); + result.a += (m[16] * c.g); + result.a += (m[17] * c.b); + result.a += (m[18] * c.a); + result.a += m[19]; - gl_FragColor.a = (m[15] * c.r); - gl_FragColor.a += (m[16] * c.g); - gl_FragColor.a += (m[17] * c.b); - gl_FragColor.a += (m[18] * c.a); - gl_FragColor.a += m[19] * c.a; + // Premultiply alpha again. + result.rgb *= result.a; -// gl_FragColor = vec4(m[0]); + gl_FragColor = result; } diff --git a/src/filters/displacement/DisplacementFilter.js b/src/filters/displacement/DisplacementFilter.js index 112de0b..d5355c3 100644 --- a/src/filters/displacement/DisplacementFilter.js +++ b/src/filters/displacement/DisplacementFilter.js @@ -36,7 +36,7 @@ this.maskMatrix = maskMatrix; this.uniforms.mapSampler = sprite.texture; - this.uniforms.filterMatrix = maskMatrix.toArray(true); + this.uniforms.filterMatrix = maskMatrix; this.uniforms.scale = { x: 1, y: 1 }; if (scale === null || scale === undefined) diff --git a/src/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/InteractionData.js b/src/interaction/InteractionData.js index 02d53a3..818da35 100644 --- a/src/interaction/InteractionData.js +++ b/src/interaction/InteractionData.js @@ -21,9 +21,9 @@ this.global = new core.Point(); /** - * The target Sprite that was interacted with + * The target DisplayObject that was interacted with * - * @member {PIXI.Sprite} + * @member {PIXI.DisplayObject} */ this.target = null; @@ -33,6 +33,13 @@ * @member {Event} */ this.originalEvent = null; + + /** + * Unique identifier for this interaction + * + * @member {number} + */ + this.identifier = null; } /** diff --git a/src/interaction/InteractionEvent.js b/src/interaction/InteractionEvent.js index 75b2966..dc554f9 100644 --- a/src/interaction/InteractionEvent.js +++ b/src/interaction/InteractionEvent.js @@ -58,7 +58,7 @@ } /** - * Prevents event from reaching any objects other than the current object. + * Resets the event. * * @private */ diff --git a/src/interaction/InteractionManager.js b/src/interaction/InteractionManager.js index 7d51134..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 1271fef..71ade15 100644 --- a/package.json +++ b/package.json @@ -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/scripts/release.js b/scripts/release.js index c4c8e36..156620c 100644 --- a/scripts/release.js +++ b/scripts/release.js @@ -12,7 +12,8 @@ 'dist/**/*', 'lib/**/*', 'src/**/*', - 'scripts/**/*', + 'scripts/*', + 'scripts/renders/*', 'test/**/*', '*.json', '*.md', diff --git a/scripts/renders/client.js b/scripts/renders/client.js index 16c9565..6faee34 100755 --- a/scripts/renders/client.js +++ b/scripts/renders/client.js @@ -1,6 +1,6 @@ 'use strict'; -require('../../bin/pixi'); +require('../../dist/pixi'); PIXI.utils.skipHello(); const fs = require('fs'); 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 bc730db..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. @@ -33,8 +33,11 @@ * need to call toDataUrl on the webgl context * @param {number} [options.resolution=1] - The resolution / device pixel ratio of the renderer, retina would be 2 * @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 @@ -43,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. */ @@ -73,7 +99,7 @@ */ stop() { - this.ticker.stop(); + this._ticker.stop(); } /** @@ -81,7 +107,7 @@ */ start() { - this.ticker.start(); + this._ticker.start(); } /** @@ -95,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 index 81e076b..78b17e6 100644 --- a/src/core/Shader.js +++ b/src/core/Shader.js @@ -1,9 +1,7 @@ import { GLShader } from 'pixi-gl-core'; import settings from './settings'; -const { PRECISION } = settings; - -function checkPrecision(src) +function checkPrecision(src, def) { if (src instanceof Array) { @@ -11,14 +9,14 @@ { const copy = src.slice(0); - copy.unshift(`precision ${PRECISION} float;`); + copy.unshift(`precision ${def} float;`); return copy; } } else if (src.substring(0, 9) !== 'precision') { - return `precision ${PRECISION} float;\n${src}`; + return `precision ${def} float;\n${src}`; } return src; @@ -42,6 +40,7 @@ */ constructor(gl, vertexSrc, fragmentSrc) { - super(gl, checkPrecision(vertexSrc), checkPrecision(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 3425b41..f9235e4 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -181,7 +181,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 b3792ef..17fde8d 100644 --- a/src/core/display/Container.js +++ b/src/core/display/Container.js @@ -73,13 +73,14 @@ } child.parent = this; - - // ensure a transform will be recalculated.. - this.transform._parentID = -1; - this._boundsID++; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.push(child); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(this.children.length - 1); child.emit('added', this); @@ -108,9 +109,14 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.splice(index, 0, child); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(index); child.emit('added', this); @@ -174,6 +180,7 @@ removeItems(this.children, currentIndex, 1); // remove from old position this.children.splice(index, 0, child); // add at new position + this.onChildrenChange(index); } @@ -220,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! @@ -244,9 +252,14 @@ { const child = this.getChildAt(index); + // ensure child transform will be recalculated.. child.parent = null; + child.transform._parentID = -1; removeItems(this.children, index, 1); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(index); child.emit('removed', this); @@ -275,8 +288,14 @@ for (let i = 0; i < removed.length; ++i) { removed[i].parent = null; + if (removed[i].transform) + { + removed[i].transform._parentID = -1; + } } + this._boundsID++; + this.onChildrenChange(beginIndex); for (let i = 0; i < removed.length; ++i) diff --git a/src/core/display/DisplayObject.js b/src/core/display/DisplayObject.js index 3affe36..4e6c77d 100644 --- a/src/core/display/DisplayObject.js +++ b/src/core/display/DisplayObject.js @@ -109,7 +109,7 @@ /** * The original, cached mask of the object * - * @member {PIXI.Rectangle} + * @member {PIXI.Graphics|PIXI.Sprite} * @private */ this._mask = null; diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index 95f2af4..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 ); @@ -1065,10 +1061,15 @@ canvasRenderer = new CanvasRenderer(); } - tempMatrix.tx = -bounds.x; - tempMatrix.ty = -bounds.y; + this.transform.updateLocalTransform(); + this.transform.localTransform.copy(tempMatrix); - canvasRenderer.render(this, canvasBuffer, false, tempMatrix); + tempMatrix.invert(); + + tempMatrix.tx -= bounds.x; + tempMatrix.ty -= bounds.y; + + canvasRenderer.render(this, canvasBuffer, true, tempMatrix); const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode); 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 e67680f..867dde2 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/buildCircle.js b/src/core/graphics/webgl/utils/buildCircle.js index 0400d13..abf5d02 100644 --- a/src/core/graphics/webgl/utils/buildCircle.js +++ b/src/core/graphics/webgl/utils/buildCircle.js @@ -33,6 +33,11 @@ height = circleData.height; } + if (width === 0 || height === 0) + { + return; + } + const totalSegs = Math.floor(30 * Math.sqrt(circleData.radius)) || Math.floor(15 * Math.sqrt(circleData.width + circleData.height)); diff --git a/src/core/graphics/webgl/utils/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 90155f9..f03fe15 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -30,6 +30,7 @@ export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; export { default as CanvasGraphicsRenderer } from './graphics/canvas/CanvasGraphicsRenderer'; +export { default as Spritesheet } from './textures/Spritesheet'; export { default as Texture } from './textures/Texture'; export { default as BaseTexture } from './textures/BaseTexture'; export { default as RenderTexture } from './textures/RenderTexture'; diff --git a/src/core/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 ccce680..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); } /** @@ -162,6 +162,9 @@ if (transform) { transform.copy(tempWt); + + // lets not forget to flag the parent transform as dirty... + this._tempDisplayObjectParent.transform._worldID = -1; } else { @@ -169,6 +172,7 @@ } displayObject.parent = this._tempDisplayObjectParent; + displayObject.updateTransform(); displayObject.parent = cacheParent; // displayObject.hitArea = //TODO add a temp hit area @@ -279,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/canvas/utils/CanvasRenderTarget.js b/src/core/renderers/canvas/utils/CanvasRenderTarget.js index 3de0aef..b409891 100644 --- a/src/core/renderers/canvas/utils/CanvasRenderTarget.js +++ b/src/core/renderers/canvas/utils/CanvasRenderTarget.js @@ -1,5 +1,4 @@ import settings from '../../../settings'; -const { RESOLUTION } = settings; /** * Creates a Canvas element of the given size. @@ -30,7 +29,7 @@ */ this.context = this.canvas.getContext('2d'); - this.resolution = resolution || RESOLUTION; + this.resolution = resolution || settings.RESOLUTION; this.resize(width, height); } 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 ce0e1c4..788ed6c 100644 --- a/src/core/renderers/webgl/WebGLRenderer.js +++ b/src/core/renderers/webgl/WebGLRenderer.js @@ -30,8 +30,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 @@ -49,10 +49,19 @@ * enable this if you need to call toDataUrl on the webgl context. * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when * rendering, stopping pixel interpolation. + * @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; + + if (this.legacy) + { + glCore.VertexArrayObject.FORCE_NATIVE = true; + } /** * The type of this renderer as a standardised const @@ -229,7 +238,7 @@ this.emit('context', gl); // setup the width/height properties and gl viewport - this.resize(this.width, this.height); + this.resize(this.screen.width, this.screen.height); } /** @@ -323,16 +332,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) { @@ -376,6 +385,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 @@ -440,9 +469,10 @@ * Changes the current shader to the one given in parameter * * @param {PIXI.Shader} shader - the new shader + * @param {boolean} [autoProject=true] - Whether automatically set the projection matrix * @return {PIXI.WebGLRenderer} Returns itself. */ - bindShader(shader) + bindShader(shader, autoProject) { // TODO cache if (this._activeShader !== shader) @@ -450,8 +480,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 bbab3b3..944f41d 100644 --- a/src/core/renderers/webgl/filters/Filter.js +++ b/src/core/renderers/webgl/filters/Filter.js @@ -1,6 +1,7 @@ import extractUniformsFromSrc from './extractUniformsFromSrc'; import { uid } from '../../../utils'; import { BLEND_MODES } from '../../../const'; +import settings from '../../../settings'; const SOURCE_KEY_MAP = {}; @@ -10,7 +11,7 @@ * @memberof PIXI * @extends PIXI.Shader */ -class Filter +export default class Filter { /** * @param {string} [vertexSrc] - The source of the vertex shader. @@ -35,8 +36,6 @@ this.blendMode = BLEND_MODES.NORMAL; - // pull out the vertex and shader uniforms if they are not specified.. - // currently this does not extract structs only default types this.uniformData = uniforms || extractUniformsFromSrc(this.vertexSrc, this.fragmentSrc, 'projectionMatrix|uSampler'); /** @@ -80,7 +79,7 @@ * * @member {number} */ - this.resolution = 1; + this.resolution = settings.RESOLUTION; /** * If enabled is true the filter is applied, if false it will not. @@ -97,8 +96,11 @@ * @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 { // --- // // this.uniforms.filterMatrix = filterManager.calculateSpriteMatrix(tempMatrix, window.panda ); @@ -170,5 +172,3 @@ ].join('\n'); } } - -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 dc7072a..6c7422d 100644 --- a/src/core/renderers/webgl/managers/FilterManager.js +++ b/src/core/renderers/webgl/managers/FilterManager.js @@ -154,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 @@ -176,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; @@ -184,7 +184,7 @@ flop = t; } - filters[i].apply(this, flip, lastState.renderTarget, true); + filters[i].apply(this, flip, lastState.renderTarget, false, currentState); this.freePotRenderTarget(flip); this.freePotRenderTarget(flop); @@ -291,9 +291,13 @@ let textureCount = 1; let currentState; - if (shader.uniforms.data.filterArea) + // 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; @@ -306,9 +310,9 @@ // use this to clamp displaced texture coords so they belong to filterArea // see displacementFilter fragment shader for an example - if (shader.uniforms.data.filterClamp) + if (shader.uniforms.filterClamp) { - currentState = this.filterData.stack[this.filterData.index]; + currentState = currentState || this.filterData.stack[this.filterData.index]; const filterClamp = shader.uniforms.filterClamp; @@ -485,7 +489,7 @@ */ destroy() { - this.shaderCache = []; + this.shaderCache = {}; this.emptyPool(); } diff --git a/src/core/renderers/webgl/utils/RenderTarget.js b/src/core/renderers/webgl/utils/RenderTarget.js index afe51ab..6d2467f 100644 --- a/src/core/renderers/webgl/utils/RenderTarget.js +++ b/src/core/renderers/webgl/utils/RenderTarget.js @@ -124,7 +124,7 @@ * @default PIXI.settings.SCALE_MODE * @see PIXI.SCALE_MODES */ - this.scaleMode = scaleMode || settings.SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; /** * Whether this object is the root element or not 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 36ea5bb..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; @@ -346,8 +346,8 @@ /** * Gets the local bounds of the sprite object. * - * @param {Rectangle} rect - The output rectangle. - * @return {Rectangle} The bounds. + * @param {PIXI.Rectangle} rect - The output rectangle. + * @return {PIXI.Rectangle} The bounds. */ getLocalBounds(rect) { @@ -440,7 +440,7 @@ * * @static * @param {number|string|PIXI.BaseTexture|HTMLCanvasElement|HTMLVideoElement} source Source to create texture from - * @return {PIXI.Texture} 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 e77b39a..90b08c0 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++) @@ -103,11 +102,18 @@ { const gl = this.renderer.gl; - // step 1: first check max textures the GPU can handle. - this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); + if (this.renderer.legacy) + { + this.MAX_TEXTURES = 1; + } + else + { + // step 1: first check max textures the GPU can handle. + this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); - // step 2: check the maximum number of if statements the shader can have too.. - this.MAX_TEXTURES = checkMaxIfStatmentsInShader(this.MAX_TEXTURES, gl); + // step 2: check the maximum number of if statements the shader can have too.. + this.MAX_TEXTURES = checkMaxIfStatmentsInShader(this.MAX_TEXTURES, gl); + } const shader = this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); @@ -130,8 +136,12 @@ .addIndex(this.indexBuffer) .addAttribute(this.vertexBuffers[i], shader.attributes.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) .addAttribute(this.vertexBuffers[i], shader.attributes.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) - .addAttribute(this.vertexBuffers[i], shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4) - .addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + .addAttribute(this.vertexBuffers[i], shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); + + if (shader.attributes.aTextureId) + { + this.vaos[i].addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + } /* eslint-enable max-len */ } @@ -378,8 +388,12 @@ .addIndex(this.indexBuffer) .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) - .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4) - .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); + + if (this.shader.attributes.aTextureId) + { + this.vaos[this.vertexCount].addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + } /* eslint-enable max-len */ } diff --git a/src/core/text/Text.js b/src/core/text/Text.js index 013625a..cec8c32 100644 --- a/src/core/text/Text.js +++ b/src/core/text/Text.js @@ -6,6 +6,7 @@ import { TEXT_GRADIENT } from '../const'; import settings from '../settings'; import TextStyle from './TextStyle'; +import trimCanvas from '../utils/trimCanvas'; const defaultDestroyOptions = { texture: true, @@ -57,7 +58,7 @@ /** * The canvas 2d context that everything is drawn with - * @member {HTMLCanvasElement} + * @member {CanvasRenderingContext2D} */ this.context = this.canvas.getContext('2d'); @@ -189,10 +190,12 @@ if (style.dropShadow) { + this.context.shadowBlur = style.dropShadowBlur; + this.context.globalAlpha = style.dropShadowAlpha; + if (style.dropShadowBlur > 0) { this.context.shadowColor = style.dropShadowColor; - this.context.shadowBlur = style.dropShadowBlur; } else { @@ -237,6 +240,10 @@ } } + // reset the shadow blur and alpha that was set by the drop shadow, for the regular text + this.context.shadowBlur = 0; + this.context.globalAlpha = 1; + // set canvas text styles this.context.fillStyle = this._generateFillStyle(style, lines); @@ -326,6 +333,15 @@ */ updateTexture() { + if (this._style.trim) + { + const trimmed = trimCanvas(this.canvas); + + this.canvas.width = trimmed.width; + this.canvas.height = trimmed.height; + this.context.putImageData(trimmed.data, 0, 0); + } + const texture = this._texture; const style = this._style; @@ -475,6 +491,19 @@ } /** + * Gets the local bounds of the text object. + * + * @param {Rectangle} rect - The output rectangle. + * @return {Rectangle} The bounds. + */ + getLocalBounds(rect) + { + this.updateText(true); + + return super.getLocalBounds.call(this, rect); + } + + /** * calculates the bounds of the Text as a rectangle. The bounds calculation takes the worldTransform into account. */ _calculateBounds() @@ -525,6 +554,29 @@ const width = this.canvas.width / this.resolution; const height = this.canvas.height / this.resolution; + // make a copy of the style settings, so we can manipulate them later + const fill = style.fill.slice(); + const fillGradientStops = style.fillGradientStops.slice(); + + // wanting to evenly distribute the fills. So an array of 4 colours should give fills of 0.25, 0.5 and 0.75 + if (!fillGradientStops.length) + { + const lengthPlus1 = fill.length + 1; + + for (let i = 1; i < lengthPlus1; ++i) + { + fillGradientStops.push(i / lengthPlus1); + } + } + + // stop the bleeding of the last gradient on the line above to the top gradient of the this line + // by hard defining the first gradient colour at point 0, and last gradient colour at point 1 + fill.unshift(style.fill[0]); + fillGradientStops.unshift(0); + + fill.push(style.fill[style.fill.length - 1]); + fillGradientStops.push(1); + if (style.fillGradientType === TEXT_GRADIENT.LINEAR_VERTICAL) { // start the gradient at the top center of the canvas, and end at the bottom middle of the canvas @@ -532,15 +584,22 @@ // we need to repeat the gradient so that each individual line of text has the same vertical gradient effect // ['#FF0000', '#00FF00', '#0000FF'] over 2 lines would create stops at 0.125, 0.25, 0.375, 0.625, 0.75, 0.875 - totalIterations = (style.fill.length + 1) * lines.length; + totalIterations = (fill.length + 1) * lines.length; currentIteration = 0; for (let i = 0; i < lines.length; i++) { currentIteration += 1; - for (let j = 0; j < style.fill.length; j++) + for (let j = 0; j < fill.length; j++) { - stop = (currentIteration / totalIterations); - gradient.addColorStop(stop, style.fill[j]); + if (fillGradientStops[j]) + { + stop = (fillGradientStops[j] / lines.length) + (i / lines.length); + } + else + { + stop = currentIteration / totalIterations; + } + gradient.addColorStop(stop, fill[j]); currentIteration++; } } @@ -552,13 +611,20 @@ // can just evenly space out the gradients in this case, as multiple lines makes no difference // to an even left to right gradient - totalIterations = style.fill.length + 1; + totalIterations = fill.length + 1; currentIteration = 1; - for (let i = 0; i < style.fill.length; i++) + for (let i = 0; i < fill.length; i++) { - stop = currentIteration / totalIterations; - gradient.addColorStop(stop, style.fill[i]); + if (fillGradientStops[i]) + { + stop = fillGradientStops[i]; + } + else + { + stop = currentIteration / totalIterations; + } + gradient.addColorStop(stop, fill[i]); currentIteration++; } } @@ -569,7 +635,7 @@ /** * Destroys this text object. * Note* Unlike a Sprite, a Text object will automatically destroy its baseTexture and texture as - * the majorety of the time the texture will not be shared with any other Sprites. + * the majority of the time the texture will not be shared with any other Sprites. * * @param {object|boolean} [options] - Options parameter. A boolean will act as if all options * have been set to that value @@ -680,7 +746,7 @@ set text(text) // eslint-disable-line require-jsdoc { - text = String(text || ' '); + text = String(text === '' || text === null || text === undefined ? ' ' : text); if (this._text === text) { diff --git a/src/core/text/TextStyle.js b/src/core/text/TextStyle.js index 3b25528..890950b 100644 --- a/src/core/text/TextStyle.js +++ b/src/core/text/TextStyle.js @@ -8,12 +8,14 @@ align: 'left', breakWords: false, dropShadow: false, + dropShadowAlpha: 1, dropShadowAngle: Math.PI / 6, dropShadowBlur: 0, dropShadowColor: '#000000', dropShadowDistance: 5, fill: 'black', fillGradientType: TEXT_GRADIENT.LINEAR_VERTICAL, + fillGradientStops: [], fontFamily: 'Arial', fontSize: 26, fontStyle: 'normal', @@ -27,6 +29,7 @@ stroke: 'black', strokeThickness: 0, textBaseline: 'alphabetic', + trim: false, wordWrap: false, wordWrapWidth: 100, }; @@ -47,6 +50,7 @@ * @param {boolean} [style.breakWords=false] - Indicates if lines can be wrapped within words, it * needs wordWrap to be set to true * @param {boolean} [style.dropShadow=false] - Set a drop shadow for the text + * @param {number} [style.dropShadowAlpha=1] - Set alpha for the drop shadow * @param {number} [style.dropShadowAngle=Math.PI/6] - Set a angle of the drop shadow * @param {number} [style.dropShadowBlur=0] - Set a shadow blur radius * @param {string} [style.dropShadowColor='#000000'] - A fill style to be used on the dropshadow e.g 'red', '#00FF00' @@ -55,8 +59,10 @@ * fillstyle that will be used on the text e.g 'red', '#00FF00'. Can be an array to create a gradient * eg ['#000000','#FFFFFF'] * {@link https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/fillStyle|MDN} - * @param {number} [style.fillGradientType=PIXI.TEXT_GRADIENT.LINEAR_VERTICAL] - If fills styles are - * supplied, this can change the type/direction of the gradient. See {@link PIXI.TEXT_GRADIENT} for possible values + * @param {number} [style.fillGradientType=PIXI.TEXT_GRADIENT.LINEAR_VERTICAL] - If fill is an array of colours + * to create a gradient, this can change the type/direction of the gradient. See {@link PIXI.TEXT_GRADIENT} + * @param {number[]} [style.fillGradientStops] - If fill is an array of colours to create a gradient, this array can set + * the stop points (numbers between 0 and 1) for the color, overriding the default behaviour of evenly spacing them. * @param {string|string[]} [style.fontFamily='Arial'] - The font family * @param {number|string} [style.fontSize=26] - The font size (as a number it converts to px, but as a string, * equivalents are '26px','20pt','160%' or '1.6em') @@ -76,6 +82,7 @@ * e.g 'blue', '#FCFF00' * @param {number} [style.strokeThickness=0] - A number that represents the thickness of the stroke. * Default is 0 (no stroke) + * @param {boolean} [style.trim=false] - Trim transparent borders * @param {string} [style.textBaseline='alphabetic'] - The baseline of the text that is rendered. * @param {boolean} [style.wordWrap=false] - Indicates if word wrap should be used * @param {number} [style.wordWrapWidth=100] - The width at which text will wrap, it needs wordWrap to be set to true @@ -152,6 +159,19 @@ } } + get dropShadowAlpha() + { + return this._dropShadowAlpha; + } + set dropShadowAlpha(dropShadowAlpha) + { + if (this._dropShadowAlpha !== dropShadowAlpha) + { + this._dropShadowAlpha = dropShadowAlpha; + this.styleID++; + } + } + get dropShadowAngle() { return this._dropShadowAngle; @@ -232,6 +252,19 @@ } } + get fillGradientStops() + { + return this._fillGradientStops; + } + set fillGradientStops(fillGradientStops) + { + if (!areArraysEqual(this._fillGradientStops,fillGradientStops)) + { + this._fillGradientStops = fillGradientStops; + this.styleID++; + } + } + get fontFamily() { return this._fontFamily; @@ -402,6 +435,19 @@ } } + get trim() + { + return this._trim; + } + set trim(trim) + { + if (this._trim !== trim) + { + this._trim = trim; + this.styleID++; + } + } + get wordWrap() { return this._wordWrap; @@ -475,3 +521,34 @@ return color; } } + +/** + * Utility function to convert hexadecimal colors to strings, and simply return the color if it's a string. + * This version can also convert array of colors + * + * @param {Array} array1 First array to compare + * @param {Array} array2 Second array to compare + * @return {boolean} Do the arrays contain the same values in the same order + */ +function areArraysEqual(array1, array2) +{ + if (!Array.isArray(array1) || !Array.isArray(array2)) + { + return false; + } + + if (array1.length !== array2.length) + { + return false; + } + + for (let i = 0; i < array1.length; ++i) + { + if (array1[i] !== array2[i]) + { + return false; + } + } + + return true; +} diff --git a/src/core/textures/BaseRenderTexture.js b/src/core/textures/BaseRenderTexture.js index 5d50146..aa727f0 100644 --- a/src/core/textures/BaseRenderTexture.js +++ b/src/core/textures/BaseRenderTexture.js @@ -1,8 +1,6 @@ import BaseTexture from './BaseTexture'; import settings from '../settings'; -const { RESOLUTION, SCALE_MODE } = settings; - /** * A BaseRenderTexture is a special texture that allows any Pixi display object to be rendered to it. * @@ -54,7 +52,7 @@ { super(null, scaleMode); - this.resolution = resolution || RESOLUTION; + this.resolution = resolution || settings.RESOLUTION; this.width = width; this.height = height; @@ -62,7 +60,7 @@ this.realWidth = this.width * this.resolution; this.realHeight = this.height * this.resolution; - this.scaleMode = scaleMode || SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; this.hasLoaded = true; /** diff --git a/src/core/textures/BaseTexture.js b/src/core/textures/BaseTexture.js index 676dca3..aeccf39 100644 --- a/src/core/textures/BaseTexture.js +++ b/src/core/textures/BaseTexture.js @@ -77,7 +77,7 @@ * @default PIXI.settings.SCALE_MODE * @see PIXI.SCALE_MODES */ - this.scaleMode = scaleMode || settings.SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; /** * Set to true once the base texture has successfully loaded. @@ -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/RenderTexture.js b/src/core/textures/RenderTexture.js index 4c5034f..b3c6297 100644 --- a/src/core/textures/RenderTexture.js +++ b/src/core/textures/RenderTexture.js @@ -54,8 +54,8 @@ /* eslint-disable prefer-rest-params, no-console */ const width = arguments[1]; const height = arguments[2]; - const scaleMode = arguments[3] || 0; - const resolution = arguments[4] || 1; + const scaleMode = arguments[3]; + const resolution = arguments[4]; // we have an old render texture.. console.warn(`Please use RenderTexture.create(${width}, ${height}) instead of the ctor directly.`); 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 ca31456..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 @@ -459,7 +497,9 @@ if (frame.x + frame.width > this.baseTexture.width || frame.y + frame.height > this.baseTexture.height) { - throw new Error(`Texture Error: frame does not fit inside the base Texture dimensions ${this}`); + throw new Error('Texture Error: frame does not fit inside the base Texture dimensions: ' + + `X: ${frame.x} + ${frame.width} > ${this.baseTexture.width} ` + + `Y: ${frame.y} + ${frame.height} > ${this.baseTexture.height}`); } // this.valid = frame && frame.width && frame.height && this.baseTexture.source && this.baseTexture.hasLoaded; @@ -520,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. @@ -528,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 2a7497b..99e585a 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)); } /** @@ -360,3 +362,43 @@ * @private */ export const BaseTextureCache = {}; + +/** + * 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/core/utils/trimCanvas.js b/src/core/utils/trimCanvas.js new file mode 100644 index 0000000..5af6de0 --- /dev/null +++ b/src/core/utils/trimCanvas.js @@ -0,0 +1,83 @@ +/** + * Trim transparent borders from a canvas + * + * @memberof PIXI + * @function trimCanvas + * @private + * @param {HTMLCanvasElement} canvas - the canvas to trim + * @returns {object} Trim data + */ +export default function trimCanvas(canvas) +{ + // https://gist.github.com/remy/784508 + + let width = canvas.width; + let height = canvas.height; + + const context = canvas.getContext('2d'); + const imageData = context.getImageData(0, 0, width, height); + const pixels = imageData.data; + const len = pixels.length; + + const bound = { + top: null, + left: null, + right: null, + bottom: null, + }; + let i; + let x; + let y; + + for (i = 0; i < len; i += 4) + { + if (pixels[i + 3] !== 0) + { + x = (i / 4) % width; + y = ~~((i / 4) / width); + + if (bound.top === null) + { + bound.top = y; + } + + if (bound.left === null) + { + bound.left = x; + } + else if (x < bound.left) + { + bound.left = x; + } + + if (bound.right === null) + { + bound.right = x + 1; + } + else if (bound.right < x) + { + bound.right = x + 1; + } + + if (bound.bottom === null) + { + bound.bottom = y; + } + else if (bound.bottom < y) + { + bound.bottom = y; + } + } + } + + width = bound.right - bound.left; + height = bound.bottom - bound.top + 1; + + const data = context.getImageData(bound.left, bound.top, width, height); + + return { + height, + width, + data, + }; +} diff --git a/src/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/extras/TilingSprite.js b/src/extras/TilingSprite.js index f90d248..fe82451 100644 --- a/src/extras/TilingSprite.js +++ b/src/extras/TilingSprite.js @@ -220,20 +220,33 @@ transform.tx * resolution, transform.ty * resolution); - // TODO - this should be rolled into the setTransform above.. - context.scale(this.tileScale.x / baseTextureResolution, this.tileScale.y / baseTextureResolution); - - context.translate(modX + (this.anchor.x * -this._width), - modY + (this.anchor.y * -this._height)); - renderer.setBlendMode(this.blendMode); // fill the pattern! context.fillStyle = this._canvasPattern; - context.fillRect(-modX, - -modY, - this._width / this.tileScale.x * baseTextureResolution, - this._height / this.tileScale.y * baseTextureResolution); + + // TODO - this should be rolled into the setTransform above.. + context.scale(this.tileScale.x / baseTextureResolution, this.tileScale.y / baseTextureResolution); + + const anchorX = this.anchor.x * -this._width; + const anchorY = this.anchor.y * -this._height; + + if (this.uvRespectAnchor) + { + context.translate(modX, modY); + + context.fillRect(-modX + anchorX, -modY + anchorY, + this._width / this.tileScale.x * baseTextureResolution, + this._height / this.tileScale.y * baseTextureResolution); + } + else + { + context.translate(modX + anchorX, modY + anchorY); + + context.fillRect(-modX, -modY, + this._width / this.tileScale.x * baseTextureResolution, + this._height / this.tileScale.y * baseTextureResolution); + } } /** diff --git a/src/extras/cacheAsBitmap.js b/src/extras/cacheAsBitmap.js index 18043a7..5dd5640 100644 --- a/src/extras/cacheAsBitmap.js +++ b/src/extras/cacheAsBitmap.js @@ -41,6 +41,9 @@ * provide a performance benefit for complex static displayObjects. * To remove simply set this property to 'false' * + * IMPORTANT GOTCHA - make sure that all your textures are preloaded BEFORE setting this property to true + * as it will take a snapshot of what is currently there. If the textures have not loaded then they will not appear. + * * @member {boolean} * @memberof PIXI.DisplayObject# */ diff --git a/src/filters/blur/BlurFilter.js b/src/filters/blur/BlurFilter.js index 340fc58..1575db9 100644 --- a/src/filters/blur/BlurFilter.js +++ b/src/filters/blur/BlurFilter.js @@ -24,10 +24,9 @@ this.blurXFilter = new BlurXFilter(strength, quality, resolution, kernelSize); this.blurYFilter = new BlurYFilter(strength, quality, resolution, kernelSize); - this.resolution = 1; this.padding = 0; - this.resolution = resolution || 1; + this.resolution = resolution || core.settings.RESOLUTION; this.quality = quality || 4; this.blur = strength || 8; } diff --git a/src/filters/blur/BlurXFilter.js b/src/filters/blur/BlurXFilter.js index def99c3..0f48889 100644 --- a/src/filters/blur/BlurXFilter.js +++ b/src/filters/blur/BlurXFilter.js @@ -31,7 +31,7 @@ fragSrc ); - this.resolution = resolution || 1; + this.resolution = resolution || core.settings.RESOLUTION; this._quality = 0; diff --git a/src/filters/blur/BlurYFilter.js b/src/filters/blur/BlurYFilter.js index 638e0f0..6d489c7 100644 --- a/src/filters/blur/BlurYFilter.js +++ b/src/filters/blur/BlurYFilter.js @@ -31,7 +31,7 @@ fragSrc ); - this.resolution = resolution || 1; + this.resolution = resolution || core.settings.RESOLUTION; this._quality = 0; diff --git a/src/filters/colormatrix/ColorMatrixFilter.js b/src/filters/colormatrix/ColorMatrixFilter.js index 8509505..679f8ec 100644 --- a/src/filters/colormatrix/ColorMatrixFilter.js +++ b/src/filters/colormatrix/ColorMatrixFilter.js @@ -75,28 +75,28 @@ out[1] = (a[0] * b[1]) + (a[1] * b[6]) + (a[2] * b[11]) + (a[3] * b[16]); out[2] = (a[0] * b[2]) + (a[1] * b[7]) + (a[2] * b[12]) + (a[3] * b[17]); out[3] = (a[0] * b[3]) + (a[1] * b[8]) + (a[2] * b[13]) + (a[3] * b[18]); - out[4] = (a[0] * b[4]) + (a[1] * b[9]) + (a[2] * b[14]) + (a[3] * b[19]); + out[4] = (a[0] * b[4]) + (a[1] * b[9]) + (a[2] * b[14]) + (a[3] * b[19]) + a[4]; // Green Channel out[5] = (a[5] * b[0]) + (a[6] * b[5]) + (a[7] * b[10]) + (a[8] * b[15]); out[6] = (a[5] * b[1]) + (a[6] * b[6]) + (a[7] * b[11]) + (a[8] * b[16]); out[7] = (a[5] * b[2]) + (a[6] * b[7]) + (a[7] * b[12]) + (a[8] * b[17]); out[8] = (a[5] * b[3]) + (a[6] * b[8]) + (a[7] * b[13]) + (a[8] * b[18]); - out[9] = (a[5] * b[4]) + (a[6] * b[9]) + (a[7] * b[14]) + (a[8] * b[19]); + out[9] = (a[5] * b[4]) + (a[6] * b[9]) + (a[7] * b[14]) + (a[8] * b[19]) + a[9]; // Blue Channel out[10] = (a[10] * b[0]) + (a[11] * b[5]) + (a[12] * b[10]) + (a[13] * b[15]); out[11] = (a[10] * b[1]) + (a[11] * b[6]) + (a[12] * b[11]) + (a[13] * b[16]); out[12] = (a[10] * b[2]) + (a[11] * b[7]) + (a[12] * b[12]) + (a[13] * b[17]); out[13] = (a[10] * b[3]) + (a[11] * b[8]) + (a[12] * b[13]) + (a[13] * b[18]); - out[14] = (a[10] * b[4]) + (a[11] * b[9]) + (a[12] * b[14]) + (a[13] * b[19]); + out[14] = (a[10] * b[4]) + (a[11] * b[9]) + (a[12] * b[14]) + (a[13] * b[19]) + a[14]; // Alpha Channel out[15] = (a[15] * b[0]) + (a[16] * b[5]) + (a[17] * b[10]) + (a[18] * b[15]); out[16] = (a[15] * b[1]) + (a[16] * b[6]) + (a[17] * b[11]) + (a[18] * b[16]); out[17] = (a[15] * b[2]) + (a[16] * b[7]) + (a[17] * b[12]) + (a[18] * b[17]); out[18] = (a[15] * b[3]) + (a[16] * b[8]) + (a[17] * b[13]) + (a[18] * b[18]); - out[19] = (a[15] * b[4]) + (a[16] * b[9]) + (a[17] * b[14]) + (a[18] * b[19]); + out[19] = (a[15] * b[4]) + (a[16] * b[9]) + (a[17] * b[14]) + (a[18] * b[19]) + a[19]; return out; } diff --git a/src/filters/colormatrix/colorMatrix.frag b/src/filters/colormatrix/colorMatrix.frag index 3aaf2b3..c73c0e9 100644 --- a/src/filters/colormatrix/colorMatrix.frag +++ b/src/filters/colormatrix/colorMatrix.frag @@ -4,32 +4,38 @@ void main(void) { - vec4 c = texture2D(uSampler, vTextureCoord); + // Un-premultiply alpha before applying the color matrix. See issue #3539. + if (c.a > 0.0) { + c.rgb /= c.a; + } + vec4 result; + result.r = (m[0] * c.r); + result.r += (m[1] * c.g); + result.r += (m[2] * c.b); + result.r += (m[3] * c.a); + result.r += m[4]; - gl_FragColor.r = (m[0] * c.r); - gl_FragColor.r += (m[1] * c.g); - gl_FragColor.r += (m[2] * c.b); - gl_FragColor.r += (m[3] * c.a); - gl_FragColor.r += m[4] * c.a; + result.g = (m[5] * c.r); + result.g += (m[6] * c.g); + result.g += (m[7] * c.b); + result.g += (m[8] * c.a); + result.g += m[9]; - gl_FragColor.g = (m[5] * c.r); - gl_FragColor.g += (m[6] * c.g); - gl_FragColor.g += (m[7] * c.b); - gl_FragColor.g += (m[8] * c.a); - gl_FragColor.g += m[9] * c.a; + result.b = (m[10] * c.r); + result.b += (m[11] * c.g); + result.b += (m[12] * c.b); + result.b += (m[13] * c.a); + result.b += m[14]; - gl_FragColor.b = (m[10] * c.r); - gl_FragColor.b += (m[11] * c.g); - gl_FragColor.b += (m[12] * c.b); - gl_FragColor.b += (m[13] * c.a); - gl_FragColor.b += m[14] * c.a; + result.a = (m[15] * c.r); + result.a += (m[16] * c.g); + result.a += (m[17] * c.b); + result.a += (m[18] * c.a); + result.a += m[19]; - gl_FragColor.a = (m[15] * c.r); - gl_FragColor.a += (m[16] * c.g); - gl_FragColor.a += (m[17] * c.b); - gl_FragColor.a += (m[18] * c.a); - gl_FragColor.a += m[19] * c.a; + // Premultiply alpha again. + result.rgb *= result.a; -// gl_FragColor = vec4(m[0]); + gl_FragColor = result; } diff --git a/src/filters/displacement/DisplacementFilter.js b/src/filters/displacement/DisplacementFilter.js index 112de0b..d5355c3 100644 --- a/src/filters/displacement/DisplacementFilter.js +++ b/src/filters/displacement/DisplacementFilter.js @@ -36,7 +36,7 @@ this.maskMatrix = maskMatrix; this.uniforms.mapSampler = sprite.texture; - this.uniforms.filterMatrix = maskMatrix.toArray(true); + this.uniforms.filterMatrix = maskMatrix; this.uniforms.scale = { x: 1, y: 1 }; if (scale === null || scale === undefined) diff --git a/src/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/InteractionData.js b/src/interaction/InteractionData.js index 02d53a3..818da35 100644 --- a/src/interaction/InteractionData.js +++ b/src/interaction/InteractionData.js @@ -21,9 +21,9 @@ this.global = new core.Point(); /** - * The target Sprite that was interacted with + * The target DisplayObject that was interacted with * - * @member {PIXI.Sprite} + * @member {PIXI.DisplayObject} */ this.target = null; @@ -33,6 +33,13 @@ * @member {Event} */ this.originalEvent = null; + + /** + * Unique identifier for this interaction + * + * @member {number} + */ + this.identifier = null; } /** diff --git a/src/interaction/InteractionEvent.js b/src/interaction/InteractionEvent.js index 75b2966..dc554f9 100644 --- a/src/interaction/InteractionEvent.js +++ b/src/interaction/InteractionEvent.js @@ -58,7 +58,7 @@ } /** - * Prevents event from reaching any objects other than the current object. + * Resets the event. * * @private */ diff --git a/src/interaction/InteractionManager.js b/src/interaction/InteractionManager.js index 7d51134..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 1271fef..71ade15 100644 --- a/package.json +++ b/package.json @@ -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/scripts/release.js b/scripts/release.js index c4c8e36..156620c 100644 --- a/scripts/release.js +++ b/scripts/release.js @@ -12,7 +12,8 @@ 'dist/**/*', 'lib/**/*', 'src/**/*', - 'scripts/**/*', + 'scripts/*', + 'scripts/renders/*', 'test/**/*', '*.json', '*.md', diff --git a/scripts/renders/client.js b/scripts/renders/client.js index 16c9565..6faee34 100755 --- a/scripts/renders/client.js +++ b/scripts/renders/client.js @@ -1,6 +1,6 @@ 'use strict'; -require('../../bin/pixi'); +require('../../dist/pixi'); PIXI.utils.skipHello(); const fs = require('fs'); 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 bc730db..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. @@ -33,8 +33,11 @@ * need to call toDataUrl on the webgl context * @param {number} [options.resolution=1] - The resolution / device pixel ratio of the renderer, retina would be 2 * @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 @@ -43,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. */ @@ -73,7 +99,7 @@ */ stop() { - this.ticker.stop(); + this._ticker.stop(); } /** @@ -81,7 +107,7 @@ */ start() { - this.ticker.start(); + this._ticker.start(); } /** @@ -95,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 index 81e076b..78b17e6 100644 --- a/src/core/Shader.js +++ b/src/core/Shader.js @@ -1,9 +1,7 @@ import { GLShader } from 'pixi-gl-core'; import settings from './settings'; -const { PRECISION } = settings; - -function checkPrecision(src) +function checkPrecision(src, def) { if (src instanceof Array) { @@ -11,14 +9,14 @@ { const copy = src.slice(0); - copy.unshift(`precision ${PRECISION} float;`); + copy.unshift(`precision ${def} float;`); return copy; } } else if (src.substring(0, 9) !== 'precision') { - return `precision ${PRECISION} float;\n${src}`; + return `precision ${def} float;\n${src}`; } return src; @@ -42,6 +40,7 @@ */ constructor(gl, vertexSrc, fragmentSrc) { - super(gl, checkPrecision(vertexSrc), checkPrecision(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 3425b41..f9235e4 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -181,7 +181,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 b3792ef..17fde8d 100644 --- a/src/core/display/Container.js +++ b/src/core/display/Container.js @@ -73,13 +73,14 @@ } child.parent = this; - - // ensure a transform will be recalculated.. - this.transform._parentID = -1; - this._boundsID++; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.push(child); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(this.children.length - 1); child.emit('added', this); @@ -108,9 +109,14 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.splice(index, 0, child); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(index); child.emit('added', this); @@ -174,6 +180,7 @@ removeItems(this.children, currentIndex, 1); // remove from old position this.children.splice(index, 0, child); // add at new position + this.onChildrenChange(index); } @@ -220,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! @@ -244,9 +252,14 @@ { const child = this.getChildAt(index); + // ensure child transform will be recalculated.. child.parent = null; + child.transform._parentID = -1; removeItems(this.children, index, 1); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(index); child.emit('removed', this); @@ -275,8 +288,14 @@ for (let i = 0; i < removed.length; ++i) { removed[i].parent = null; + if (removed[i].transform) + { + removed[i].transform._parentID = -1; + } } + this._boundsID++; + this.onChildrenChange(beginIndex); for (let i = 0; i < removed.length; ++i) diff --git a/src/core/display/DisplayObject.js b/src/core/display/DisplayObject.js index 3affe36..4e6c77d 100644 --- a/src/core/display/DisplayObject.js +++ b/src/core/display/DisplayObject.js @@ -109,7 +109,7 @@ /** * The original, cached mask of the object * - * @member {PIXI.Rectangle} + * @member {PIXI.Graphics|PIXI.Sprite} * @private */ this._mask = null; diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index 95f2af4..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 ); @@ -1065,10 +1061,15 @@ canvasRenderer = new CanvasRenderer(); } - tempMatrix.tx = -bounds.x; - tempMatrix.ty = -bounds.y; + this.transform.updateLocalTransform(); + this.transform.localTransform.copy(tempMatrix); - canvasRenderer.render(this, canvasBuffer, false, tempMatrix); + tempMatrix.invert(); + + tempMatrix.tx -= bounds.x; + tempMatrix.ty -= bounds.y; + + canvasRenderer.render(this, canvasBuffer, true, tempMatrix); const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode); 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 e67680f..867dde2 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/buildCircle.js b/src/core/graphics/webgl/utils/buildCircle.js index 0400d13..abf5d02 100644 --- a/src/core/graphics/webgl/utils/buildCircle.js +++ b/src/core/graphics/webgl/utils/buildCircle.js @@ -33,6 +33,11 @@ height = circleData.height; } + if (width === 0 || height === 0) + { + return; + } + const totalSegs = Math.floor(30 * Math.sqrt(circleData.radius)) || Math.floor(15 * Math.sqrt(circleData.width + circleData.height)); diff --git a/src/core/graphics/webgl/utils/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 90155f9..f03fe15 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -30,6 +30,7 @@ export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; export { default as CanvasGraphicsRenderer } from './graphics/canvas/CanvasGraphicsRenderer'; +export { default as Spritesheet } from './textures/Spritesheet'; export { default as Texture } from './textures/Texture'; export { default as BaseTexture } from './textures/BaseTexture'; export { default as RenderTexture } from './textures/RenderTexture'; diff --git a/src/core/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 ccce680..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); } /** @@ -162,6 +162,9 @@ if (transform) { transform.copy(tempWt); + + // lets not forget to flag the parent transform as dirty... + this._tempDisplayObjectParent.transform._worldID = -1; } else { @@ -169,6 +172,7 @@ } displayObject.parent = this._tempDisplayObjectParent; + displayObject.updateTransform(); displayObject.parent = cacheParent; // displayObject.hitArea = //TODO add a temp hit area @@ -279,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/canvas/utils/CanvasRenderTarget.js b/src/core/renderers/canvas/utils/CanvasRenderTarget.js index 3de0aef..b409891 100644 --- a/src/core/renderers/canvas/utils/CanvasRenderTarget.js +++ b/src/core/renderers/canvas/utils/CanvasRenderTarget.js @@ -1,5 +1,4 @@ import settings from '../../../settings'; -const { RESOLUTION } = settings; /** * Creates a Canvas element of the given size. @@ -30,7 +29,7 @@ */ this.context = this.canvas.getContext('2d'); - this.resolution = resolution || RESOLUTION; + this.resolution = resolution || settings.RESOLUTION; this.resize(width, height); } 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 ce0e1c4..788ed6c 100644 --- a/src/core/renderers/webgl/WebGLRenderer.js +++ b/src/core/renderers/webgl/WebGLRenderer.js @@ -30,8 +30,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 @@ -49,10 +49,19 @@ * enable this if you need to call toDataUrl on the webgl context. * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when * rendering, stopping pixel interpolation. + * @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; + + if (this.legacy) + { + glCore.VertexArrayObject.FORCE_NATIVE = true; + } /** * The type of this renderer as a standardised const @@ -229,7 +238,7 @@ this.emit('context', gl); // setup the width/height properties and gl viewport - this.resize(this.width, this.height); + this.resize(this.screen.width, this.screen.height); } /** @@ -323,16 +332,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) { @@ -376,6 +385,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 @@ -440,9 +469,10 @@ * Changes the current shader to the one given in parameter * * @param {PIXI.Shader} shader - the new shader + * @param {boolean} [autoProject=true] - Whether automatically set the projection matrix * @return {PIXI.WebGLRenderer} Returns itself. */ - bindShader(shader) + bindShader(shader, autoProject) { // TODO cache if (this._activeShader !== shader) @@ -450,8 +480,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 bbab3b3..944f41d 100644 --- a/src/core/renderers/webgl/filters/Filter.js +++ b/src/core/renderers/webgl/filters/Filter.js @@ -1,6 +1,7 @@ import extractUniformsFromSrc from './extractUniformsFromSrc'; import { uid } from '../../../utils'; import { BLEND_MODES } from '../../../const'; +import settings from '../../../settings'; const SOURCE_KEY_MAP = {}; @@ -10,7 +11,7 @@ * @memberof PIXI * @extends PIXI.Shader */ -class Filter +export default class Filter { /** * @param {string} [vertexSrc] - The source of the vertex shader. @@ -35,8 +36,6 @@ this.blendMode = BLEND_MODES.NORMAL; - // pull out the vertex and shader uniforms if they are not specified.. - // currently this does not extract structs only default types this.uniformData = uniforms || extractUniformsFromSrc(this.vertexSrc, this.fragmentSrc, 'projectionMatrix|uSampler'); /** @@ -80,7 +79,7 @@ * * @member {number} */ - this.resolution = 1; + this.resolution = settings.RESOLUTION; /** * If enabled is true the filter is applied, if false it will not. @@ -97,8 +96,11 @@ * @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 { // --- // // this.uniforms.filterMatrix = filterManager.calculateSpriteMatrix(tempMatrix, window.panda ); @@ -170,5 +172,3 @@ ].join('\n'); } } - -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 dc7072a..6c7422d 100644 --- a/src/core/renderers/webgl/managers/FilterManager.js +++ b/src/core/renderers/webgl/managers/FilterManager.js @@ -154,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 @@ -176,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; @@ -184,7 +184,7 @@ flop = t; } - filters[i].apply(this, flip, lastState.renderTarget, true); + filters[i].apply(this, flip, lastState.renderTarget, false, currentState); this.freePotRenderTarget(flip); this.freePotRenderTarget(flop); @@ -291,9 +291,13 @@ let textureCount = 1; let currentState; - if (shader.uniforms.data.filterArea) + // 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; @@ -306,9 +310,9 @@ // use this to clamp displaced texture coords so they belong to filterArea // see displacementFilter fragment shader for an example - if (shader.uniforms.data.filterClamp) + if (shader.uniforms.filterClamp) { - currentState = this.filterData.stack[this.filterData.index]; + currentState = currentState || this.filterData.stack[this.filterData.index]; const filterClamp = shader.uniforms.filterClamp; @@ -485,7 +489,7 @@ */ destroy() { - this.shaderCache = []; + this.shaderCache = {}; this.emptyPool(); } diff --git a/src/core/renderers/webgl/utils/RenderTarget.js b/src/core/renderers/webgl/utils/RenderTarget.js index afe51ab..6d2467f 100644 --- a/src/core/renderers/webgl/utils/RenderTarget.js +++ b/src/core/renderers/webgl/utils/RenderTarget.js @@ -124,7 +124,7 @@ * @default PIXI.settings.SCALE_MODE * @see PIXI.SCALE_MODES */ - this.scaleMode = scaleMode || settings.SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; /** * Whether this object is the root element or not 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 36ea5bb..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; @@ -346,8 +346,8 @@ /** * Gets the local bounds of the sprite object. * - * @param {Rectangle} rect - The output rectangle. - * @return {Rectangle} The bounds. + * @param {PIXI.Rectangle} rect - The output rectangle. + * @return {PIXI.Rectangle} The bounds. */ getLocalBounds(rect) { @@ -440,7 +440,7 @@ * * @static * @param {number|string|PIXI.BaseTexture|HTMLCanvasElement|HTMLVideoElement} source Source to create texture from - * @return {PIXI.Texture} 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 e77b39a..90b08c0 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++) @@ -103,11 +102,18 @@ { const gl = this.renderer.gl; - // step 1: first check max textures the GPU can handle. - this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); + if (this.renderer.legacy) + { + this.MAX_TEXTURES = 1; + } + else + { + // step 1: first check max textures the GPU can handle. + this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); - // step 2: check the maximum number of if statements the shader can have too.. - this.MAX_TEXTURES = checkMaxIfStatmentsInShader(this.MAX_TEXTURES, gl); + // step 2: check the maximum number of if statements the shader can have too.. + this.MAX_TEXTURES = checkMaxIfStatmentsInShader(this.MAX_TEXTURES, gl); + } const shader = this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); @@ -130,8 +136,12 @@ .addIndex(this.indexBuffer) .addAttribute(this.vertexBuffers[i], shader.attributes.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) .addAttribute(this.vertexBuffers[i], shader.attributes.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) - .addAttribute(this.vertexBuffers[i], shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4) - .addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + .addAttribute(this.vertexBuffers[i], shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); + + if (shader.attributes.aTextureId) + { + this.vaos[i].addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + } /* eslint-enable max-len */ } @@ -378,8 +388,12 @@ .addIndex(this.indexBuffer) .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) - .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4) - .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); + + if (this.shader.attributes.aTextureId) + { + this.vaos[this.vertexCount].addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + } /* eslint-enable max-len */ } diff --git a/src/core/text/Text.js b/src/core/text/Text.js index 013625a..cec8c32 100644 --- a/src/core/text/Text.js +++ b/src/core/text/Text.js @@ -6,6 +6,7 @@ import { TEXT_GRADIENT } from '../const'; import settings from '../settings'; import TextStyle from './TextStyle'; +import trimCanvas from '../utils/trimCanvas'; const defaultDestroyOptions = { texture: true, @@ -57,7 +58,7 @@ /** * The canvas 2d context that everything is drawn with - * @member {HTMLCanvasElement} + * @member {CanvasRenderingContext2D} */ this.context = this.canvas.getContext('2d'); @@ -189,10 +190,12 @@ if (style.dropShadow) { + this.context.shadowBlur = style.dropShadowBlur; + this.context.globalAlpha = style.dropShadowAlpha; + if (style.dropShadowBlur > 0) { this.context.shadowColor = style.dropShadowColor; - this.context.shadowBlur = style.dropShadowBlur; } else { @@ -237,6 +240,10 @@ } } + // reset the shadow blur and alpha that was set by the drop shadow, for the regular text + this.context.shadowBlur = 0; + this.context.globalAlpha = 1; + // set canvas text styles this.context.fillStyle = this._generateFillStyle(style, lines); @@ -326,6 +333,15 @@ */ updateTexture() { + if (this._style.trim) + { + const trimmed = trimCanvas(this.canvas); + + this.canvas.width = trimmed.width; + this.canvas.height = trimmed.height; + this.context.putImageData(trimmed.data, 0, 0); + } + const texture = this._texture; const style = this._style; @@ -475,6 +491,19 @@ } /** + * Gets the local bounds of the text object. + * + * @param {Rectangle} rect - The output rectangle. + * @return {Rectangle} The bounds. + */ + getLocalBounds(rect) + { + this.updateText(true); + + return super.getLocalBounds.call(this, rect); + } + + /** * calculates the bounds of the Text as a rectangle. The bounds calculation takes the worldTransform into account. */ _calculateBounds() @@ -525,6 +554,29 @@ const width = this.canvas.width / this.resolution; const height = this.canvas.height / this.resolution; + // make a copy of the style settings, so we can manipulate them later + const fill = style.fill.slice(); + const fillGradientStops = style.fillGradientStops.slice(); + + // wanting to evenly distribute the fills. So an array of 4 colours should give fills of 0.25, 0.5 and 0.75 + if (!fillGradientStops.length) + { + const lengthPlus1 = fill.length + 1; + + for (let i = 1; i < lengthPlus1; ++i) + { + fillGradientStops.push(i / lengthPlus1); + } + } + + // stop the bleeding of the last gradient on the line above to the top gradient of the this line + // by hard defining the first gradient colour at point 0, and last gradient colour at point 1 + fill.unshift(style.fill[0]); + fillGradientStops.unshift(0); + + fill.push(style.fill[style.fill.length - 1]); + fillGradientStops.push(1); + if (style.fillGradientType === TEXT_GRADIENT.LINEAR_VERTICAL) { // start the gradient at the top center of the canvas, and end at the bottom middle of the canvas @@ -532,15 +584,22 @@ // we need to repeat the gradient so that each individual line of text has the same vertical gradient effect // ['#FF0000', '#00FF00', '#0000FF'] over 2 lines would create stops at 0.125, 0.25, 0.375, 0.625, 0.75, 0.875 - totalIterations = (style.fill.length + 1) * lines.length; + totalIterations = (fill.length + 1) * lines.length; currentIteration = 0; for (let i = 0; i < lines.length; i++) { currentIteration += 1; - for (let j = 0; j < style.fill.length; j++) + for (let j = 0; j < fill.length; j++) { - stop = (currentIteration / totalIterations); - gradient.addColorStop(stop, style.fill[j]); + if (fillGradientStops[j]) + { + stop = (fillGradientStops[j] / lines.length) + (i / lines.length); + } + else + { + stop = currentIteration / totalIterations; + } + gradient.addColorStop(stop, fill[j]); currentIteration++; } } @@ -552,13 +611,20 @@ // can just evenly space out the gradients in this case, as multiple lines makes no difference // to an even left to right gradient - totalIterations = style.fill.length + 1; + totalIterations = fill.length + 1; currentIteration = 1; - for (let i = 0; i < style.fill.length; i++) + for (let i = 0; i < fill.length; i++) { - stop = currentIteration / totalIterations; - gradient.addColorStop(stop, style.fill[i]); + if (fillGradientStops[i]) + { + stop = fillGradientStops[i]; + } + else + { + stop = currentIteration / totalIterations; + } + gradient.addColorStop(stop, fill[i]); currentIteration++; } } @@ -569,7 +635,7 @@ /** * Destroys this text object. * Note* Unlike a Sprite, a Text object will automatically destroy its baseTexture and texture as - * the majorety of the time the texture will not be shared with any other Sprites. + * the majority of the time the texture will not be shared with any other Sprites. * * @param {object|boolean} [options] - Options parameter. A boolean will act as if all options * have been set to that value @@ -680,7 +746,7 @@ set text(text) // eslint-disable-line require-jsdoc { - text = String(text || ' '); + text = String(text === '' || text === null || text === undefined ? ' ' : text); if (this._text === text) { diff --git a/src/core/text/TextStyle.js b/src/core/text/TextStyle.js index 3b25528..890950b 100644 --- a/src/core/text/TextStyle.js +++ b/src/core/text/TextStyle.js @@ -8,12 +8,14 @@ align: 'left', breakWords: false, dropShadow: false, + dropShadowAlpha: 1, dropShadowAngle: Math.PI / 6, dropShadowBlur: 0, dropShadowColor: '#000000', dropShadowDistance: 5, fill: 'black', fillGradientType: TEXT_GRADIENT.LINEAR_VERTICAL, + fillGradientStops: [], fontFamily: 'Arial', fontSize: 26, fontStyle: 'normal', @@ -27,6 +29,7 @@ stroke: 'black', strokeThickness: 0, textBaseline: 'alphabetic', + trim: false, wordWrap: false, wordWrapWidth: 100, }; @@ -47,6 +50,7 @@ * @param {boolean} [style.breakWords=false] - Indicates if lines can be wrapped within words, it * needs wordWrap to be set to true * @param {boolean} [style.dropShadow=false] - Set a drop shadow for the text + * @param {number} [style.dropShadowAlpha=1] - Set alpha for the drop shadow * @param {number} [style.dropShadowAngle=Math.PI/6] - Set a angle of the drop shadow * @param {number} [style.dropShadowBlur=0] - Set a shadow blur radius * @param {string} [style.dropShadowColor='#000000'] - A fill style to be used on the dropshadow e.g 'red', '#00FF00' @@ -55,8 +59,10 @@ * fillstyle that will be used on the text e.g 'red', '#00FF00'. Can be an array to create a gradient * eg ['#000000','#FFFFFF'] * {@link https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/fillStyle|MDN} - * @param {number} [style.fillGradientType=PIXI.TEXT_GRADIENT.LINEAR_VERTICAL] - If fills styles are - * supplied, this can change the type/direction of the gradient. See {@link PIXI.TEXT_GRADIENT} for possible values + * @param {number} [style.fillGradientType=PIXI.TEXT_GRADIENT.LINEAR_VERTICAL] - If fill is an array of colours + * to create a gradient, this can change the type/direction of the gradient. See {@link PIXI.TEXT_GRADIENT} + * @param {number[]} [style.fillGradientStops] - If fill is an array of colours to create a gradient, this array can set + * the stop points (numbers between 0 and 1) for the color, overriding the default behaviour of evenly spacing them. * @param {string|string[]} [style.fontFamily='Arial'] - The font family * @param {number|string} [style.fontSize=26] - The font size (as a number it converts to px, but as a string, * equivalents are '26px','20pt','160%' or '1.6em') @@ -76,6 +82,7 @@ * e.g 'blue', '#FCFF00' * @param {number} [style.strokeThickness=0] - A number that represents the thickness of the stroke. * Default is 0 (no stroke) + * @param {boolean} [style.trim=false] - Trim transparent borders * @param {string} [style.textBaseline='alphabetic'] - The baseline of the text that is rendered. * @param {boolean} [style.wordWrap=false] - Indicates if word wrap should be used * @param {number} [style.wordWrapWidth=100] - The width at which text will wrap, it needs wordWrap to be set to true @@ -152,6 +159,19 @@ } } + get dropShadowAlpha() + { + return this._dropShadowAlpha; + } + set dropShadowAlpha(dropShadowAlpha) + { + if (this._dropShadowAlpha !== dropShadowAlpha) + { + this._dropShadowAlpha = dropShadowAlpha; + this.styleID++; + } + } + get dropShadowAngle() { return this._dropShadowAngle; @@ -232,6 +252,19 @@ } } + get fillGradientStops() + { + return this._fillGradientStops; + } + set fillGradientStops(fillGradientStops) + { + if (!areArraysEqual(this._fillGradientStops,fillGradientStops)) + { + this._fillGradientStops = fillGradientStops; + this.styleID++; + } + } + get fontFamily() { return this._fontFamily; @@ -402,6 +435,19 @@ } } + get trim() + { + return this._trim; + } + set trim(trim) + { + if (this._trim !== trim) + { + this._trim = trim; + this.styleID++; + } + } + get wordWrap() { return this._wordWrap; @@ -475,3 +521,34 @@ return color; } } + +/** + * Utility function to convert hexadecimal colors to strings, and simply return the color if it's a string. + * This version can also convert array of colors + * + * @param {Array} array1 First array to compare + * @param {Array} array2 Second array to compare + * @return {boolean} Do the arrays contain the same values in the same order + */ +function areArraysEqual(array1, array2) +{ + if (!Array.isArray(array1) || !Array.isArray(array2)) + { + return false; + } + + if (array1.length !== array2.length) + { + return false; + } + + for (let i = 0; i < array1.length; ++i) + { + if (array1[i] !== array2[i]) + { + return false; + } + } + + return true; +} diff --git a/src/core/textures/BaseRenderTexture.js b/src/core/textures/BaseRenderTexture.js index 5d50146..aa727f0 100644 --- a/src/core/textures/BaseRenderTexture.js +++ b/src/core/textures/BaseRenderTexture.js @@ -1,8 +1,6 @@ import BaseTexture from './BaseTexture'; import settings from '../settings'; -const { RESOLUTION, SCALE_MODE } = settings; - /** * A BaseRenderTexture is a special texture that allows any Pixi display object to be rendered to it. * @@ -54,7 +52,7 @@ { super(null, scaleMode); - this.resolution = resolution || RESOLUTION; + this.resolution = resolution || settings.RESOLUTION; this.width = width; this.height = height; @@ -62,7 +60,7 @@ this.realWidth = this.width * this.resolution; this.realHeight = this.height * this.resolution; - this.scaleMode = scaleMode || SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; this.hasLoaded = true; /** diff --git a/src/core/textures/BaseTexture.js b/src/core/textures/BaseTexture.js index 676dca3..aeccf39 100644 --- a/src/core/textures/BaseTexture.js +++ b/src/core/textures/BaseTexture.js @@ -77,7 +77,7 @@ * @default PIXI.settings.SCALE_MODE * @see PIXI.SCALE_MODES */ - this.scaleMode = scaleMode || settings.SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; /** * Set to true once the base texture has successfully loaded. @@ -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/RenderTexture.js b/src/core/textures/RenderTexture.js index 4c5034f..b3c6297 100644 --- a/src/core/textures/RenderTexture.js +++ b/src/core/textures/RenderTexture.js @@ -54,8 +54,8 @@ /* eslint-disable prefer-rest-params, no-console */ const width = arguments[1]; const height = arguments[2]; - const scaleMode = arguments[3] || 0; - const resolution = arguments[4] || 1; + const scaleMode = arguments[3]; + const resolution = arguments[4]; // we have an old render texture.. console.warn(`Please use RenderTexture.create(${width}, ${height}) instead of the ctor directly.`); 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 ca31456..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 @@ -459,7 +497,9 @@ if (frame.x + frame.width > this.baseTexture.width || frame.y + frame.height > this.baseTexture.height) { - throw new Error(`Texture Error: frame does not fit inside the base Texture dimensions ${this}`); + throw new Error('Texture Error: frame does not fit inside the base Texture dimensions: ' + + `X: ${frame.x} + ${frame.width} > ${this.baseTexture.width} ` + + `Y: ${frame.y} + ${frame.height} > ${this.baseTexture.height}`); } // this.valid = frame && frame.width && frame.height && this.baseTexture.source && this.baseTexture.hasLoaded; @@ -520,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. @@ -528,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 2a7497b..99e585a 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)); } /** @@ -360,3 +362,43 @@ * @private */ export const BaseTextureCache = {}; + +/** + * 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/core/utils/trimCanvas.js b/src/core/utils/trimCanvas.js new file mode 100644 index 0000000..5af6de0 --- /dev/null +++ b/src/core/utils/trimCanvas.js @@ -0,0 +1,83 @@ +/** + * Trim transparent borders from a canvas + * + * @memberof PIXI + * @function trimCanvas + * @private + * @param {HTMLCanvasElement} canvas - the canvas to trim + * @returns {object} Trim data + */ +export default function trimCanvas(canvas) +{ + // https://gist.github.com/remy/784508 + + let width = canvas.width; + let height = canvas.height; + + const context = canvas.getContext('2d'); + const imageData = context.getImageData(0, 0, width, height); + const pixels = imageData.data; + const len = pixels.length; + + const bound = { + top: null, + left: null, + right: null, + bottom: null, + }; + let i; + let x; + let y; + + for (i = 0; i < len; i += 4) + { + if (pixels[i + 3] !== 0) + { + x = (i / 4) % width; + y = ~~((i / 4) / width); + + if (bound.top === null) + { + bound.top = y; + } + + if (bound.left === null) + { + bound.left = x; + } + else if (x < bound.left) + { + bound.left = x; + } + + if (bound.right === null) + { + bound.right = x + 1; + } + else if (bound.right < x) + { + bound.right = x + 1; + } + + if (bound.bottom === null) + { + bound.bottom = y; + } + else if (bound.bottom < y) + { + bound.bottom = y; + } + } + } + + width = bound.right - bound.left; + height = bound.bottom - bound.top + 1; + + const data = context.getImageData(bound.left, bound.top, width, height); + + return { + height, + width, + data, + }; +} diff --git a/src/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/extras/TilingSprite.js b/src/extras/TilingSprite.js index f90d248..fe82451 100644 --- a/src/extras/TilingSprite.js +++ b/src/extras/TilingSprite.js @@ -220,20 +220,33 @@ transform.tx * resolution, transform.ty * resolution); - // TODO - this should be rolled into the setTransform above.. - context.scale(this.tileScale.x / baseTextureResolution, this.tileScale.y / baseTextureResolution); - - context.translate(modX + (this.anchor.x * -this._width), - modY + (this.anchor.y * -this._height)); - renderer.setBlendMode(this.blendMode); // fill the pattern! context.fillStyle = this._canvasPattern; - context.fillRect(-modX, - -modY, - this._width / this.tileScale.x * baseTextureResolution, - this._height / this.tileScale.y * baseTextureResolution); + + // TODO - this should be rolled into the setTransform above.. + context.scale(this.tileScale.x / baseTextureResolution, this.tileScale.y / baseTextureResolution); + + const anchorX = this.anchor.x * -this._width; + const anchorY = this.anchor.y * -this._height; + + if (this.uvRespectAnchor) + { + context.translate(modX, modY); + + context.fillRect(-modX + anchorX, -modY + anchorY, + this._width / this.tileScale.x * baseTextureResolution, + this._height / this.tileScale.y * baseTextureResolution); + } + else + { + context.translate(modX + anchorX, modY + anchorY); + + context.fillRect(-modX, -modY, + this._width / this.tileScale.x * baseTextureResolution, + this._height / this.tileScale.y * baseTextureResolution); + } } /** diff --git a/src/extras/cacheAsBitmap.js b/src/extras/cacheAsBitmap.js index 18043a7..5dd5640 100644 --- a/src/extras/cacheAsBitmap.js +++ b/src/extras/cacheAsBitmap.js @@ -41,6 +41,9 @@ * provide a performance benefit for complex static displayObjects. * To remove simply set this property to 'false' * + * IMPORTANT GOTCHA - make sure that all your textures are preloaded BEFORE setting this property to true + * as it will take a snapshot of what is currently there. If the textures have not loaded then they will not appear. + * * @member {boolean} * @memberof PIXI.DisplayObject# */ diff --git a/src/filters/blur/BlurFilter.js b/src/filters/blur/BlurFilter.js index 340fc58..1575db9 100644 --- a/src/filters/blur/BlurFilter.js +++ b/src/filters/blur/BlurFilter.js @@ -24,10 +24,9 @@ this.blurXFilter = new BlurXFilter(strength, quality, resolution, kernelSize); this.blurYFilter = new BlurYFilter(strength, quality, resolution, kernelSize); - this.resolution = 1; this.padding = 0; - this.resolution = resolution || 1; + this.resolution = resolution || core.settings.RESOLUTION; this.quality = quality || 4; this.blur = strength || 8; } diff --git a/src/filters/blur/BlurXFilter.js b/src/filters/blur/BlurXFilter.js index def99c3..0f48889 100644 --- a/src/filters/blur/BlurXFilter.js +++ b/src/filters/blur/BlurXFilter.js @@ -31,7 +31,7 @@ fragSrc ); - this.resolution = resolution || 1; + this.resolution = resolution || core.settings.RESOLUTION; this._quality = 0; diff --git a/src/filters/blur/BlurYFilter.js b/src/filters/blur/BlurYFilter.js index 638e0f0..6d489c7 100644 --- a/src/filters/blur/BlurYFilter.js +++ b/src/filters/blur/BlurYFilter.js @@ -31,7 +31,7 @@ fragSrc ); - this.resolution = resolution || 1; + this.resolution = resolution || core.settings.RESOLUTION; this._quality = 0; diff --git a/src/filters/colormatrix/ColorMatrixFilter.js b/src/filters/colormatrix/ColorMatrixFilter.js index 8509505..679f8ec 100644 --- a/src/filters/colormatrix/ColorMatrixFilter.js +++ b/src/filters/colormatrix/ColorMatrixFilter.js @@ -75,28 +75,28 @@ out[1] = (a[0] * b[1]) + (a[1] * b[6]) + (a[2] * b[11]) + (a[3] * b[16]); out[2] = (a[0] * b[2]) + (a[1] * b[7]) + (a[2] * b[12]) + (a[3] * b[17]); out[3] = (a[0] * b[3]) + (a[1] * b[8]) + (a[2] * b[13]) + (a[3] * b[18]); - out[4] = (a[0] * b[4]) + (a[1] * b[9]) + (a[2] * b[14]) + (a[3] * b[19]); + out[4] = (a[0] * b[4]) + (a[1] * b[9]) + (a[2] * b[14]) + (a[3] * b[19]) + a[4]; // Green Channel out[5] = (a[5] * b[0]) + (a[6] * b[5]) + (a[7] * b[10]) + (a[8] * b[15]); out[6] = (a[5] * b[1]) + (a[6] * b[6]) + (a[7] * b[11]) + (a[8] * b[16]); out[7] = (a[5] * b[2]) + (a[6] * b[7]) + (a[7] * b[12]) + (a[8] * b[17]); out[8] = (a[5] * b[3]) + (a[6] * b[8]) + (a[7] * b[13]) + (a[8] * b[18]); - out[9] = (a[5] * b[4]) + (a[6] * b[9]) + (a[7] * b[14]) + (a[8] * b[19]); + out[9] = (a[5] * b[4]) + (a[6] * b[9]) + (a[7] * b[14]) + (a[8] * b[19]) + a[9]; // Blue Channel out[10] = (a[10] * b[0]) + (a[11] * b[5]) + (a[12] * b[10]) + (a[13] * b[15]); out[11] = (a[10] * b[1]) + (a[11] * b[6]) + (a[12] * b[11]) + (a[13] * b[16]); out[12] = (a[10] * b[2]) + (a[11] * b[7]) + (a[12] * b[12]) + (a[13] * b[17]); out[13] = (a[10] * b[3]) + (a[11] * b[8]) + (a[12] * b[13]) + (a[13] * b[18]); - out[14] = (a[10] * b[4]) + (a[11] * b[9]) + (a[12] * b[14]) + (a[13] * b[19]); + out[14] = (a[10] * b[4]) + (a[11] * b[9]) + (a[12] * b[14]) + (a[13] * b[19]) + a[14]; // Alpha Channel out[15] = (a[15] * b[0]) + (a[16] * b[5]) + (a[17] * b[10]) + (a[18] * b[15]); out[16] = (a[15] * b[1]) + (a[16] * b[6]) + (a[17] * b[11]) + (a[18] * b[16]); out[17] = (a[15] * b[2]) + (a[16] * b[7]) + (a[17] * b[12]) + (a[18] * b[17]); out[18] = (a[15] * b[3]) + (a[16] * b[8]) + (a[17] * b[13]) + (a[18] * b[18]); - out[19] = (a[15] * b[4]) + (a[16] * b[9]) + (a[17] * b[14]) + (a[18] * b[19]); + out[19] = (a[15] * b[4]) + (a[16] * b[9]) + (a[17] * b[14]) + (a[18] * b[19]) + a[19]; return out; } diff --git a/src/filters/colormatrix/colorMatrix.frag b/src/filters/colormatrix/colorMatrix.frag index 3aaf2b3..c73c0e9 100644 --- a/src/filters/colormatrix/colorMatrix.frag +++ b/src/filters/colormatrix/colorMatrix.frag @@ -4,32 +4,38 @@ void main(void) { - vec4 c = texture2D(uSampler, vTextureCoord); + // Un-premultiply alpha before applying the color matrix. See issue #3539. + if (c.a > 0.0) { + c.rgb /= c.a; + } + vec4 result; + result.r = (m[0] * c.r); + result.r += (m[1] * c.g); + result.r += (m[2] * c.b); + result.r += (m[3] * c.a); + result.r += m[4]; - gl_FragColor.r = (m[0] * c.r); - gl_FragColor.r += (m[1] * c.g); - gl_FragColor.r += (m[2] * c.b); - gl_FragColor.r += (m[3] * c.a); - gl_FragColor.r += m[4] * c.a; + result.g = (m[5] * c.r); + result.g += (m[6] * c.g); + result.g += (m[7] * c.b); + result.g += (m[8] * c.a); + result.g += m[9]; - gl_FragColor.g = (m[5] * c.r); - gl_FragColor.g += (m[6] * c.g); - gl_FragColor.g += (m[7] * c.b); - gl_FragColor.g += (m[8] * c.a); - gl_FragColor.g += m[9] * c.a; + result.b = (m[10] * c.r); + result.b += (m[11] * c.g); + result.b += (m[12] * c.b); + result.b += (m[13] * c.a); + result.b += m[14]; - gl_FragColor.b = (m[10] * c.r); - gl_FragColor.b += (m[11] * c.g); - gl_FragColor.b += (m[12] * c.b); - gl_FragColor.b += (m[13] * c.a); - gl_FragColor.b += m[14] * c.a; + result.a = (m[15] * c.r); + result.a += (m[16] * c.g); + result.a += (m[17] * c.b); + result.a += (m[18] * c.a); + result.a += m[19]; - gl_FragColor.a = (m[15] * c.r); - gl_FragColor.a += (m[16] * c.g); - gl_FragColor.a += (m[17] * c.b); - gl_FragColor.a += (m[18] * c.a); - gl_FragColor.a += m[19] * c.a; + // Premultiply alpha again. + result.rgb *= result.a; -// gl_FragColor = vec4(m[0]); + gl_FragColor = result; } diff --git a/src/filters/displacement/DisplacementFilter.js b/src/filters/displacement/DisplacementFilter.js index 112de0b..d5355c3 100644 --- a/src/filters/displacement/DisplacementFilter.js +++ b/src/filters/displacement/DisplacementFilter.js @@ -36,7 +36,7 @@ this.maskMatrix = maskMatrix; this.uniforms.mapSampler = sprite.texture; - this.uniforms.filterMatrix = maskMatrix.toArray(true); + this.uniforms.filterMatrix = maskMatrix; this.uniforms.scale = { x: 1, y: 1 }; if (scale === null || scale === undefined) diff --git a/src/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/InteractionData.js b/src/interaction/InteractionData.js index 02d53a3..818da35 100644 --- a/src/interaction/InteractionData.js +++ b/src/interaction/InteractionData.js @@ -21,9 +21,9 @@ this.global = new core.Point(); /** - * The target Sprite that was interacted with + * The target DisplayObject that was interacted with * - * @member {PIXI.Sprite} + * @member {PIXI.DisplayObject} */ this.target = null; @@ -33,6 +33,13 @@ * @member {Event} */ this.originalEvent = null; + + /** + * Unique identifier for this interaction + * + * @member {number} + */ + this.identifier = null; } /** diff --git a/src/interaction/InteractionEvent.js b/src/interaction/InteractionEvent.js index 75b2966..dc554f9 100644 --- a/src/interaction/InteractionEvent.js +++ b/src/interaction/InteractionEvent.js @@ -58,7 +58,7 @@ } /** - * Prevents event from reaching any objects other than the current object. + * Resets the event. * * @private */ diff --git a/src/interaction/InteractionManager.js b/src/interaction/InteractionManager.js index 7d51134..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 1271fef..71ade15 100644 --- a/package.json +++ b/package.json @@ -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/scripts/release.js b/scripts/release.js index c4c8e36..156620c 100644 --- a/scripts/release.js +++ b/scripts/release.js @@ -12,7 +12,8 @@ 'dist/**/*', 'lib/**/*', 'src/**/*', - 'scripts/**/*', + 'scripts/*', + 'scripts/renders/*', 'test/**/*', '*.json', '*.md', diff --git a/scripts/renders/client.js b/scripts/renders/client.js index 16c9565..6faee34 100755 --- a/scripts/renders/client.js +++ b/scripts/renders/client.js @@ -1,6 +1,6 @@ 'use strict'; -require('../../bin/pixi'); +require('../../dist/pixi'); PIXI.utils.skipHello(); const fs = require('fs'); 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 bc730db..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. @@ -33,8 +33,11 @@ * need to call toDataUrl on the webgl context * @param {number} [options.resolution=1] - The resolution / device pixel ratio of the renderer, retina would be 2 * @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 @@ -43,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. */ @@ -73,7 +99,7 @@ */ stop() { - this.ticker.stop(); + this._ticker.stop(); } /** @@ -81,7 +107,7 @@ */ start() { - this.ticker.start(); + this._ticker.start(); } /** @@ -95,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 index 81e076b..78b17e6 100644 --- a/src/core/Shader.js +++ b/src/core/Shader.js @@ -1,9 +1,7 @@ import { GLShader } from 'pixi-gl-core'; import settings from './settings'; -const { PRECISION } = settings; - -function checkPrecision(src) +function checkPrecision(src, def) { if (src instanceof Array) { @@ -11,14 +9,14 @@ { const copy = src.slice(0); - copy.unshift(`precision ${PRECISION} float;`); + copy.unshift(`precision ${def} float;`); return copy; } } else if (src.substring(0, 9) !== 'precision') { - return `precision ${PRECISION} float;\n${src}`; + return `precision ${def} float;\n${src}`; } return src; @@ -42,6 +40,7 @@ */ constructor(gl, vertexSrc, fragmentSrc) { - super(gl, checkPrecision(vertexSrc), checkPrecision(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 3425b41..f9235e4 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -181,7 +181,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 b3792ef..17fde8d 100644 --- a/src/core/display/Container.js +++ b/src/core/display/Container.js @@ -73,13 +73,14 @@ } child.parent = this; - - // ensure a transform will be recalculated.. - this.transform._parentID = -1; - this._boundsID++; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.push(child); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(this.children.length - 1); child.emit('added', this); @@ -108,9 +109,14 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.splice(index, 0, child); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(index); child.emit('added', this); @@ -174,6 +180,7 @@ removeItems(this.children, currentIndex, 1); // remove from old position this.children.splice(index, 0, child); // add at new position + this.onChildrenChange(index); } @@ -220,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! @@ -244,9 +252,14 @@ { const child = this.getChildAt(index); + // ensure child transform will be recalculated.. child.parent = null; + child.transform._parentID = -1; removeItems(this.children, index, 1); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(index); child.emit('removed', this); @@ -275,8 +288,14 @@ for (let i = 0; i < removed.length; ++i) { removed[i].parent = null; + if (removed[i].transform) + { + removed[i].transform._parentID = -1; + } } + this._boundsID++; + this.onChildrenChange(beginIndex); for (let i = 0; i < removed.length; ++i) diff --git a/src/core/display/DisplayObject.js b/src/core/display/DisplayObject.js index 3affe36..4e6c77d 100644 --- a/src/core/display/DisplayObject.js +++ b/src/core/display/DisplayObject.js @@ -109,7 +109,7 @@ /** * The original, cached mask of the object * - * @member {PIXI.Rectangle} + * @member {PIXI.Graphics|PIXI.Sprite} * @private */ this._mask = null; diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index 95f2af4..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 ); @@ -1065,10 +1061,15 @@ canvasRenderer = new CanvasRenderer(); } - tempMatrix.tx = -bounds.x; - tempMatrix.ty = -bounds.y; + this.transform.updateLocalTransform(); + this.transform.localTransform.copy(tempMatrix); - canvasRenderer.render(this, canvasBuffer, false, tempMatrix); + tempMatrix.invert(); + + tempMatrix.tx -= bounds.x; + tempMatrix.ty -= bounds.y; + + canvasRenderer.render(this, canvasBuffer, true, tempMatrix); const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode); 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 e67680f..867dde2 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/buildCircle.js b/src/core/graphics/webgl/utils/buildCircle.js index 0400d13..abf5d02 100644 --- a/src/core/graphics/webgl/utils/buildCircle.js +++ b/src/core/graphics/webgl/utils/buildCircle.js @@ -33,6 +33,11 @@ height = circleData.height; } + if (width === 0 || height === 0) + { + return; + } + const totalSegs = Math.floor(30 * Math.sqrt(circleData.radius)) || Math.floor(15 * Math.sqrt(circleData.width + circleData.height)); diff --git a/src/core/graphics/webgl/utils/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 90155f9..f03fe15 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -30,6 +30,7 @@ export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; export { default as CanvasGraphicsRenderer } from './graphics/canvas/CanvasGraphicsRenderer'; +export { default as Spritesheet } from './textures/Spritesheet'; export { default as Texture } from './textures/Texture'; export { default as BaseTexture } from './textures/BaseTexture'; export { default as RenderTexture } from './textures/RenderTexture'; diff --git a/src/core/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 ccce680..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); } /** @@ -162,6 +162,9 @@ if (transform) { transform.copy(tempWt); + + // lets not forget to flag the parent transform as dirty... + this._tempDisplayObjectParent.transform._worldID = -1; } else { @@ -169,6 +172,7 @@ } displayObject.parent = this._tempDisplayObjectParent; + displayObject.updateTransform(); displayObject.parent = cacheParent; // displayObject.hitArea = //TODO add a temp hit area @@ -279,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/canvas/utils/CanvasRenderTarget.js b/src/core/renderers/canvas/utils/CanvasRenderTarget.js index 3de0aef..b409891 100644 --- a/src/core/renderers/canvas/utils/CanvasRenderTarget.js +++ b/src/core/renderers/canvas/utils/CanvasRenderTarget.js @@ -1,5 +1,4 @@ import settings from '../../../settings'; -const { RESOLUTION } = settings; /** * Creates a Canvas element of the given size. @@ -30,7 +29,7 @@ */ this.context = this.canvas.getContext('2d'); - this.resolution = resolution || RESOLUTION; + this.resolution = resolution || settings.RESOLUTION; this.resize(width, height); } 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 ce0e1c4..788ed6c 100644 --- a/src/core/renderers/webgl/WebGLRenderer.js +++ b/src/core/renderers/webgl/WebGLRenderer.js @@ -30,8 +30,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 @@ -49,10 +49,19 @@ * enable this if you need to call toDataUrl on the webgl context. * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when * rendering, stopping pixel interpolation. + * @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; + + if (this.legacy) + { + glCore.VertexArrayObject.FORCE_NATIVE = true; + } /** * The type of this renderer as a standardised const @@ -229,7 +238,7 @@ this.emit('context', gl); // setup the width/height properties and gl viewport - this.resize(this.width, this.height); + this.resize(this.screen.width, this.screen.height); } /** @@ -323,16 +332,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) { @@ -376,6 +385,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 @@ -440,9 +469,10 @@ * Changes the current shader to the one given in parameter * * @param {PIXI.Shader} shader - the new shader + * @param {boolean} [autoProject=true] - Whether automatically set the projection matrix * @return {PIXI.WebGLRenderer} Returns itself. */ - bindShader(shader) + bindShader(shader, autoProject) { // TODO cache if (this._activeShader !== shader) @@ -450,8 +480,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 bbab3b3..944f41d 100644 --- a/src/core/renderers/webgl/filters/Filter.js +++ b/src/core/renderers/webgl/filters/Filter.js @@ -1,6 +1,7 @@ import extractUniformsFromSrc from './extractUniformsFromSrc'; import { uid } from '../../../utils'; import { BLEND_MODES } from '../../../const'; +import settings from '../../../settings'; const SOURCE_KEY_MAP = {}; @@ -10,7 +11,7 @@ * @memberof PIXI * @extends PIXI.Shader */ -class Filter +export default class Filter { /** * @param {string} [vertexSrc] - The source of the vertex shader. @@ -35,8 +36,6 @@ this.blendMode = BLEND_MODES.NORMAL; - // pull out the vertex and shader uniforms if they are not specified.. - // currently this does not extract structs only default types this.uniformData = uniforms || extractUniformsFromSrc(this.vertexSrc, this.fragmentSrc, 'projectionMatrix|uSampler'); /** @@ -80,7 +79,7 @@ * * @member {number} */ - this.resolution = 1; + this.resolution = settings.RESOLUTION; /** * If enabled is true the filter is applied, if false it will not. @@ -97,8 +96,11 @@ * @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 { // --- // // this.uniforms.filterMatrix = filterManager.calculateSpriteMatrix(tempMatrix, window.panda ); @@ -170,5 +172,3 @@ ].join('\n'); } } - -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 dc7072a..6c7422d 100644 --- a/src/core/renderers/webgl/managers/FilterManager.js +++ b/src/core/renderers/webgl/managers/FilterManager.js @@ -154,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 @@ -176,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; @@ -184,7 +184,7 @@ flop = t; } - filters[i].apply(this, flip, lastState.renderTarget, true); + filters[i].apply(this, flip, lastState.renderTarget, false, currentState); this.freePotRenderTarget(flip); this.freePotRenderTarget(flop); @@ -291,9 +291,13 @@ let textureCount = 1; let currentState; - if (shader.uniforms.data.filterArea) + // 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; @@ -306,9 +310,9 @@ // use this to clamp displaced texture coords so they belong to filterArea // see displacementFilter fragment shader for an example - if (shader.uniforms.data.filterClamp) + if (shader.uniforms.filterClamp) { - currentState = this.filterData.stack[this.filterData.index]; + currentState = currentState || this.filterData.stack[this.filterData.index]; const filterClamp = shader.uniforms.filterClamp; @@ -485,7 +489,7 @@ */ destroy() { - this.shaderCache = []; + this.shaderCache = {}; this.emptyPool(); } diff --git a/src/core/renderers/webgl/utils/RenderTarget.js b/src/core/renderers/webgl/utils/RenderTarget.js index afe51ab..6d2467f 100644 --- a/src/core/renderers/webgl/utils/RenderTarget.js +++ b/src/core/renderers/webgl/utils/RenderTarget.js @@ -124,7 +124,7 @@ * @default PIXI.settings.SCALE_MODE * @see PIXI.SCALE_MODES */ - this.scaleMode = scaleMode || settings.SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; /** * Whether this object is the root element or not 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 36ea5bb..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; @@ -346,8 +346,8 @@ /** * Gets the local bounds of the sprite object. * - * @param {Rectangle} rect - The output rectangle. - * @return {Rectangle} The bounds. + * @param {PIXI.Rectangle} rect - The output rectangle. + * @return {PIXI.Rectangle} The bounds. */ getLocalBounds(rect) { @@ -440,7 +440,7 @@ * * @static * @param {number|string|PIXI.BaseTexture|HTMLCanvasElement|HTMLVideoElement} source Source to create texture from - * @return {PIXI.Texture} 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 e77b39a..90b08c0 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++) @@ -103,11 +102,18 @@ { const gl = this.renderer.gl; - // step 1: first check max textures the GPU can handle. - this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); + if (this.renderer.legacy) + { + this.MAX_TEXTURES = 1; + } + else + { + // step 1: first check max textures the GPU can handle. + this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); - // step 2: check the maximum number of if statements the shader can have too.. - this.MAX_TEXTURES = checkMaxIfStatmentsInShader(this.MAX_TEXTURES, gl); + // step 2: check the maximum number of if statements the shader can have too.. + this.MAX_TEXTURES = checkMaxIfStatmentsInShader(this.MAX_TEXTURES, gl); + } const shader = this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); @@ -130,8 +136,12 @@ .addIndex(this.indexBuffer) .addAttribute(this.vertexBuffers[i], shader.attributes.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) .addAttribute(this.vertexBuffers[i], shader.attributes.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) - .addAttribute(this.vertexBuffers[i], shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4) - .addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + .addAttribute(this.vertexBuffers[i], shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); + + if (shader.attributes.aTextureId) + { + this.vaos[i].addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + } /* eslint-enable max-len */ } @@ -378,8 +388,12 @@ .addIndex(this.indexBuffer) .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) - .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4) - .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); + + if (this.shader.attributes.aTextureId) + { + this.vaos[this.vertexCount].addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + } /* eslint-enable max-len */ } diff --git a/src/core/text/Text.js b/src/core/text/Text.js index 013625a..cec8c32 100644 --- a/src/core/text/Text.js +++ b/src/core/text/Text.js @@ -6,6 +6,7 @@ import { TEXT_GRADIENT } from '../const'; import settings from '../settings'; import TextStyle from './TextStyle'; +import trimCanvas from '../utils/trimCanvas'; const defaultDestroyOptions = { texture: true, @@ -57,7 +58,7 @@ /** * The canvas 2d context that everything is drawn with - * @member {HTMLCanvasElement} + * @member {CanvasRenderingContext2D} */ this.context = this.canvas.getContext('2d'); @@ -189,10 +190,12 @@ if (style.dropShadow) { + this.context.shadowBlur = style.dropShadowBlur; + this.context.globalAlpha = style.dropShadowAlpha; + if (style.dropShadowBlur > 0) { this.context.shadowColor = style.dropShadowColor; - this.context.shadowBlur = style.dropShadowBlur; } else { @@ -237,6 +240,10 @@ } } + // reset the shadow blur and alpha that was set by the drop shadow, for the regular text + this.context.shadowBlur = 0; + this.context.globalAlpha = 1; + // set canvas text styles this.context.fillStyle = this._generateFillStyle(style, lines); @@ -326,6 +333,15 @@ */ updateTexture() { + if (this._style.trim) + { + const trimmed = trimCanvas(this.canvas); + + this.canvas.width = trimmed.width; + this.canvas.height = trimmed.height; + this.context.putImageData(trimmed.data, 0, 0); + } + const texture = this._texture; const style = this._style; @@ -475,6 +491,19 @@ } /** + * Gets the local bounds of the text object. + * + * @param {Rectangle} rect - The output rectangle. + * @return {Rectangle} The bounds. + */ + getLocalBounds(rect) + { + this.updateText(true); + + return super.getLocalBounds.call(this, rect); + } + + /** * calculates the bounds of the Text as a rectangle. The bounds calculation takes the worldTransform into account. */ _calculateBounds() @@ -525,6 +554,29 @@ const width = this.canvas.width / this.resolution; const height = this.canvas.height / this.resolution; + // make a copy of the style settings, so we can manipulate them later + const fill = style.fill.slice(); + const fillGradientStops = style.fillGradientStops.slice(); + + // wanting to evenly distribute the fills. So an array of 4 colours should give fills of 0.25, 0.5 and 0.75 + if (!fillGradientStops.length) + { + const lengthPlus1 = fill.length + 1; + + for (let i = 1; i < lengthPlus1; ++i) + { + fillGradientStops.push(i / lengthPlus1); + } + } + + // stop the bleeding of the last gradient on the line above to the top gradient of the this line + // by hard defining the first gradient colour at point 0, and last gradient colour at point 1 + fill.unshift(style.fill[0]); + fillGradientStops.unshift(0); + + fill.push(style.fill[style.fill.length - 1]); + fillGradientStops.push(1); + if (style.fillGradientType === TEXT_GRADIENT.LINEAR_VERTICAL) { // start the gradient at the top center of the canvas, and end at the bottom middle of the canvas @@ -532,15 +584,22 @@ // we need to repeat the gradient so that each individual line of text has the same vertical gradient effect // ['#FF0000', '#00FF00', '#0000FF'] over 2 lines would create stops at 0.125, 0.25, 0.375, 0.625, 0.75, 0.875 - totalIterations = (style.fill.length + 1) * lines.length; + totalIterations = (fill.length + 1) * lines.length; currentIteration = 0; for (let i = 0; i < lines.length; i++) { currentIteration += 1; - for (let j = 0; j < style.fill.length; j++) + for (let j = 0; j < fill.length; j++) { - stop = (currentIteration / totalIterations); - gradient.addColorStop(stop, style.fill[j]); + if (fillGradientStops[j]) + { + stop = (fillGradientStops[j] / lines.length) + (i / lines.length); + } + else + { + stop = currentIteration / totalIterations; + } + gradient.addColorStop(stop, fill[j]); currentIteration++; } } @@ -552,13 +611,20 @@ // can just evenly space out the gradients in this case, as multiple lines makes no difference // to an even left to right gradient - totalIterations = style.fill.length + 1; + totalIterations = fill.length + 1; currentIteration = 1; - for (let i = 0; i < style.fill.length; i++) + for (let i = 0; i < fill.length; i++) { - stop = currentIteration / totalIterations; - gradient.addColorStop(stop, style.fill[i]); + if (fillGradientStops[i]) + { + stop = fillGradientStops[i]; + } + else + { + stop = currentIteration / totalIterations; + } + gradient.addColorStop(stop, fill[i]); currentIteration++; } } @@ -569,7 +635,7 @@ /** * Destroys this text object. * Note* Unlike a Sprite, a Text object will automatically destroy its baseTexture and texture as - * the majorety of the time the texture will not be shared with any other Sprites. + * the majority of the time the texture will not be shared with any other Sprites. * * @param {object|boolean} [options] - Options parameter. A boolean will act as if all options * have been set to that value @@ -680,7 +746,7 @@ set text(text) // eslint-disable-line require-jsdoc { - text = String(text || ' '); + text = String(text === '' || text === null || text === undefined ? ' ' : text); if (this._text === text) { diff --git a/src/core/text/TextStyle.js b/src/core/text/TextStyle.js index 3b25528..890950b 100644 --- a/src/core/text/TextStyle.js +++ b/src/core/text/TextStyle.js @@ -8,12 +8,14 @@ align: 'left', breakWords: false, dropShadow: false, + dropShadowAlpha: 1, dropShadowAngle: Math.PI / 6, dropShadowBlur: 0, dropShadowColor: '#000000', dropShadowDistance: 5, fill: 'black', fillGradientType: TEXT_GRADIENT.LINEAR_VERTICAL, + fillGradientStops: [], fontFamily: 'Arial', fontSize: 26, fontStyle: 'normal', @@ -27,6 +29,7 @@ stroke: 'black', strokeThickness: 0, textBaseline: 'alphabetic', + trim: false, wordWrap: false, wordWrapWidth: 100, }; @@ -47,6 +50,7 @@ * @param {boolean} [style.breakWords=false] - Indicates if lines can be wrapped within words, it * needs wordWrap to be set to true * @param {boolean} [style.dropShadow=false] - Set a drop shadow for the text + * @param {number} [style.dropShadowAlpha=1] - Set alpha for the drop shadow * @param {number} [style.dropShadowAngle=Math.PI/6] - Set a angle of the drop shadow * @param {number} [style.dropShadowBlur=0] - Set a shadow blur radius * @param {string} [style.dropShadowColor='#000000'] - A fill style to be used on the dropshadow e.g 'red', '#00FF00' @@ -55,8 +59,10 @@ * fillstyle that will be used on the text e.g 'red', '#00FF00'. Can be an array to create a gradient * eg ['#000000','#FFFFFF'] * {@link https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/fillStyle|MDN} - * @param {number} [style.fillGradientType=PIXI.TEXT_GRADIENT.LINEAR_VERTICAL] - If fills styles are - * supplied, this can change the type/direction of the gradient. See {@link PIXI.TEXT_GRADIENT} for possible values + * @param {number} [style.fillGradientType=PIXI.TEXT_GRADIENT.LINEAR_VERTICAL] - If fill is an array of colours + * to create a gradient, this can change the type/direction of the gradient. See {@link PIXI.TEXT_GRADIENT} + * @param {number[]} [style.fillGradientStops] - If fill is an array of colours to create a gradient, this array can set + * the stop points (numbers between 0 and 1) for the color, overriding the default behaviour of evenly spacing them. * @param {string|string[]} [style.fontFamily='Arial'] - The font family * @param {number|string} [style.fontSize=26] - The font size (as a number it converts to px, but as a string, * equivalents are '26px','20pt','160%' or '1.6em') @@ -76,6 +82,7 @@ * e.g 'blue', '#FCFF00' * @param {number} [style.strokeThickness=0] - A number that represents the thickness of the stroke. * Default is 0 (no stroke) + * @param {boolean} [style.trim=false] - Trim transparent borders * @param {string} [style.textBaseline='alphabetic'] - The baseline of the text that is rendered. * @param {boolean} [style.wordWrap=false] - Indicates if word wrap should be used * @param {number} [style.wordWrapWidth=100] - The width at which text will wrap, it needs wordWrap to be set to true @@ -152,6 +159,19 @@ } } + get dropShadowAlpha() + { + return this._dropShadowAlpha; + } + set dropShadowAlpha(dropShadowAlpha) + { + if (this._dropShadowAlpha !== dropShadowAlpha) + { + this._dropShadowAlpha = dropShadowAlpha; + this.styleID++; + } + } + get dropShadowAngle() { return this._dropShadowAngle; @@ -232,6 +252,19 @@ } } + get fillGradientStops() + { + return this._fillGradientStops; + } + set fillGradientStops(fillGradientStops) + { + if (!areArraysEqual(this._fillGradientStops,fillGradientStops)) + { + this._fillGradientStops = fillGradientStops; + this.styleID++; + } + } + get fontFamily() { return this._fontFamily; @@ -402,6 +435,19 @@ } } + get trim() + { + return this._trim; + } + set trim(trim) + { + if (this._trim !== trim) + { + this._trim = trim; + this.styleID++; + } + } + get wordWrap() { return this._wordWrap; @@ -475,3 +521,34 @@ return color; } } + +/** + * Utility function to convert hexadecimal colors to strings, and simply return the color if it's a string. + * This version can also convert array of colors + * + * @param {Array} array1 First array to compare + * @param {Array} array2 Second array to compare + * @return {boolean} Do the arrays contain the same values in the same order + */ +function areArraysEqual(array1, array2) +{ + if (!Array.isArray(array1) || !Array.isArray(array2)) + { + return false; + } + + if (array1.length !== array2.length) + { + return false; + } + + for (let i = 0; i < array1.length; ++i) + { + if (array1[i] !== array2[i]) + { + return false; + } + } + + return true; +} diff --git a/src/core/textures/BaseRenderTexture.js b/src/core/textures/BaseRenderTexture.js index 5d50146..aa727f0 100644 --- a/src/core/textures/BaseRenderTexture.js +++ b/src/core/textures/BaseRenderTexture.js @@ -1,8 +1,6 @@ import BaseTexture from './BaseTexture'; import settings from '../settings'; -const { RESOLUTION, SCALE_MODE } = settings; - /** * A BaseRenderTexture is a special texture that allows any Pixi display object to be rendered to it. * @@ -54,7 +52,7 @@ { super(null, scaleMode); - this.resolution = resolution || RESOLUTION; + this.resolution = resolution || settings.RESOLUTION; this.width = width; this.height = height; @@ -62,7 +60,7 @@ this.realWidth = this.width * this.resolution; this.realHeight = this.height * this.resolution; - this.scaleMode = scaleMode || SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; this.hasLoaded = true; /** diff --git a/src/core/textures/BaseTexture.js b/src/core/textures/BaseTexture.js index 676dca3..aeccf39 100644 --- a/src/core/textures/BaseTexture.js +++ b/src/core/textures/BaseTexture.js @@ -77,7 +77,7 @@ * @default PIXI.settings.SCALE_MODE * @see PIXI.SCALE_MODES */ - this.scaleMode = scaleMode || settings.SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; /** * Set to true once the base texture has successfully loaded. @@ -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/RenderTexture.js b/src/core/textures/RenderTexture.js index 4c5034f..b3c6297 100644 --- a/src/core/textures/RenderTexture.js +++ b/src/core/textures/RenderTexture.js @@ -54,8 +54,8 @@ /* eslint-disable prefer-rest-params, no-console */ const width = arguments[1]; const height = arguments[2]; - const scaleMode = arguments[3] || 0; - const resolution = arguments[4] || 1; + const scaleMode = arguments[3]; + const resolution = arguments[4]; // we have an old render texture.. console.warn(`Please use RenderTexture.create(${width}, ${height}) instead of the ctor directly.`); 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 ca31456..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 @@ -459,7 +497,9 @@ if (frame.x + frame.width > this.baseTexture.width || frame.y + frame.height > this.baseTexture.height) { - throw new Error(`Texture Error: frame does not fit inside the base Texture dimensions ${this}`); + throw new Error('Texture Error: frame does not fit inside the base Texture dimensions: ' + + `X: ${frame.x} + ${frame.width} > ${this.baseTexture.width} ` + + `Y: ${frame.y} + ${frame.height} > ${this.baseTexture.height}`); } // this.valid = frame && frame.width && frame.height && this.baseTexture.source && this.baseTexture.hasLoaded; @@ -520,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. @@ -528,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 2a7497b..99e585a 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)); } /** @@ -360,3 +362,43 @@ * @private */ export const BaseTextureCache = {}; + +/** + * 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/core/utils/trimCanvas.js b/src/core/utils/trimCanvas.js new file mode 100644 index 0000000..5af6de0 --- /dev/null +++ b/src/core/utils/trimCanvas.js @@ -0,0 +1,83 @@ +/** + * Trim transparent borders from a canvas + * + * @memberof PIXI + * @function trimCanvas + * @private + * @param {HTMLCanvasElement} canvas - the canvas to trim + * @returns {object} Trim data + */ +export default function trimCanvas(canvas) +{ + // https://gist.github.com/remy/784508 + + let width = canvas.width; + let height = canvas.height; + + const context = canvas.getContext('2d'); + const imageData = context.getImageData(0, 0, width, height); + const pixels = imageData.data; + const len = pixels.length; + + const bound = { + top: null, + left: null, + right: null, + bottom: null, + }; + let i; + let x; + let y; + + for (i = 0; i < len; i += 4) + { + if (pixels[i + 3] !== 0) + { + x = (i / 4) % width; + y = ~~((i / 4) / width); + + if (bound.top === null) + { + bound.top = y; + } + + if (bound.left === null) + { + bound.left = x; + } + else if (x < bound.left) + { + bound.left = x; + } + + if (bound.right === null) + { + bound.right = x + 1; + } + else if (bound.right < x) + { + bound.right = x + 1; + } + + if (bound.bottom === null) + { + bound.bottom = y; + } + else if (bound.bottom < y) + { + bound.bottom = y; + } + } + } + + width = bound.right - bound.left; + height = bound.bottom - bound.top + 1; + + const data = context.getImageData(bound.left, bound.top, width, height); + + return { + height, + width, + data, + }; +} diff --git a/src/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/extras/TilingSprite.js b/src/extras/TilingSprite.js index f90d248..fe82451 100644 --- a/src/extras/TilingSprite.js +++ b/src/extras/TilingSprite.js @@ -220,20 +220,33 @@ transform.tx * resolution, transform.ty * resolution); - // TODO - this should be rolled into the setTransform above.. - context.scale(this.tileScale.x / baseTextureResolution, this.tileScale.y / baseTextureResolution); - - context.translate(modX + (this.anchor.x * -this._width), - modY + (this.anchor.y * -this._height)); - renderer.setBlendMode(this.blendMode); // fill the pattern! context.fillStyle = this._canvasPattern; - context.fillRect(-modX, - -modY, - this._width / this.tileScale.x * baseTextureResolution, - this._height / this.tileScale.y * baseTextureResolution); + + // TODO - this should be rolled into the setTransform above.. + context.scale(this.tileScale.x / baseTextureResolution, this.tileScale.y / baseTextureResolution); + + const anchorX = this.anchor.x * -this._width; + const anchorY = this.anchor.y * -this._height; + + if (this.uvRespectAnchor) + { + context.translate(modX, modY); + + context.fillRect(-modX + anchorX, -modY + anchorY, + this._width / this.tileScale.x * baseTextureResolution, + this._height / this.tileScale.y * baseTextureResolution); + } + else + { + context.translate(modX + anchorX, modY + anchorY); + + context.fillRect(-modX, -modY, + this._width / this.tileScale.x * baseTextureResolution, + this._height / this.tileScale.y * baseTextureResolution); + } } /** diff --git a/src/extras/cacheAsBitmap.js b/src/extras/cacheAsBitmap.js index 18043a7..5dd5640 100644 --- a/src/extras/cacheAsBitmap.js +++ b/src/extras/cacheAsBitmap.js @@ -41,6 +41,9 @@ * provide a performance benefit for complex static displayObjects. * To remove simply set this property to 'false' * + * IMPORTANT GOTCHA - make sure that all your textures are preloaded BEFORE setting this property to true + * as it will take a snapshot of what is currently there. If the textures have not loaded then they will not appear. + * * @member {boolean} * @memberof PIXI.DisplayObject# */ diff --git a/src/filters/blur/BlurFilter.js b/src/filters/blur/BlurFilter.js index 340fc58..1575db9 100644 --- a/src/filters/blur/BlurFilter.js +++ b/src/filters/blur/BlurFilter.js @@ -24,10 +24,9 @@ this.blurXFilter = new BlurXFilter(strength, quality, resolution, kernelSize); this.blurYFilter = new BlurYFilter(strength, quality, resolution, kernelSize); - this.resolution = 1; this.padding = 0; - this.resolution = resolution || 1; + this.resolution = resolution || core.settings.RESOLUTION; this.quality = quality || 4; this.blur = strength || 8; } diff --git a/src/filters/blur/BlurXFilter.js b/src/filters/blur/BlurXFilter.js index def99c3..0f48889 100644 --- a/src/filters/blur/BlurXFilter.js +++ b/src/filters/blur/BlurXFilter.js @@ -31,7 +31,7 @@ fragSrc ); - this.resolution = resolution || 1; + this.resolution = resolution || core.settings.RESOLUTION; this._quality = 0; diff --git a/src/filters/blur/BlurYFilter.js b/src/filters/blur/BlurYFilter.js index 638e0f0..6d489c7 100644 --- a/src/filters/blur/BlurYFilter.js +++ b/src/filters/blur/BlurYFilter.js @@ -31,7 +31,7 @@ fragSrc ); - this.resolution = resolution || 1; + this.resolution = resolution || core.settings.RESOLUTION; this._quality = 0; diff --git a/src/filters/colormatrix/ColorMatrixFilter.js b/src/filters/colormatrix/ColorMatrixFilter.js index 8509505..679f8ec 100644 --- a/src/filters/colormatrix/ColorMatrixFilter.js +++ b/src/filters/colormatrix/ColorMatrixFilter.js @@ -75,28 +75,28 @@ out[1] = (a[0] * b[1]) + (a[1] * b[6]) + (a[2] * b[11]) + (a[3] * b[16]); out[2] = (a[0] * b[2]) + (a[1] * b[7]) + (a[2] * b[12]) + (a[3] * b[17]); out[3] = (a[0] * b[3]) + (a[1] * b[8]) + (a[2] * b[13]) + (a[3] * b[18]); - out[4] = (a[0] * b[4]) + (a[1] * b[9]) + (a[2] * b[14]) + (a[3] * b[19]); + out[4] = (a[0] * b[4]) + (a[1] * b[9]) + (a[2] * b[14]) + (a[3] * b[19]) + a[4]; // Green Channel out[5] = (a[5] * b[0]) + (a[6] * b[5]) + (a[7] * b[10]) + (a[8] * b[15]); out[6] = (a[5] * b[1]) + (a[6] * b[6]) + (a[7] * b[11]) + (a[8] * b[16]); out[7] = (a[5] * b[2]) + (a[6] * b[7]) + (a[7] * b[12]) + (a[8] * b[17]); out[8] = (a[5] * b[3]) + (a[6] * b[8]) + (a[7] * b[13]) + (a[8] * b[18]); - out[9] = (a[5] * b[4]) + (a[6] * b[9]) + (a[7] * b[14]) + (a[8] * b[19]); + out[9] = (a[5] * b[4]) + (a[6] * b[9]) + (a[7] * b[14]) + (a[8] * b[19]) + a[9]; // Blue Channel out[10] = (a[10] * b[0]) + (a[11] * b[5]) + (a[12] * b[10]) + (a[13] * b[15]); out[11] = (a[10] * b[1]) + (a[11] * b[6]) + (a[12] * b[11]) + (a[13] * b[16]); out[12] = (a[10] * b[2]) + (a[11] * b[7]) + (a[12] * b[12]) + (a[13] * b[17]); out[13] = (a[10] * b[3]) + (a[11] * b[8]) + (a[12] * b[13]) + (a[13] * b[18]); - out[14] = (a[10] * b[4]) + (a[11] * b[9]) + (a[12] * b[14]) + (a[13] * b[19]); + out[14] = (a[10] * b[4]) + (a[11] * b[9]) + (a[12] * b[14]) + (a[13] * b[19]) + a[14]; // Alpha Channel out[15] = (a[15] * b[0]) + (a[16] * b[5]) + (a[17] * b[10]) + (a[18] * b[15]); out[16] = (a[15] * b[1]) + (a[16] * b[6]) + (a[17] * b[11]) + (a[18] * b[16]); out[17] = (a[15] * b[2]) + (a[16] * b[7]) + (a[17] * b[12]) + (a[18] * b[17]); out[18] = (a[15] * b[3]) + (a[16] * b[8]) + (a[17] * b[13]) + (a[18] * b[18]); - out[19] = (a[15] * b[4]) + (a[16] * b[9]) + (a[17] * b[14]) + (a[18] * b[19]); + out[19] = (a[15] * b[4]) + (a[16] * b[9]) + (a[17] * b[14]) + (a[18] * b[19]) + a[19]; return out; } diff --git a/src/filters/colormatrix/colorMatrix.frag b/src/filters/colormatrix/colorMatrix.frag index 3aaf2b3..c73c0e9 100644 --- a/src/filters/colormatrix/colorMatrix.frag +++ b/src/filters/colormatrix/colorMatrix.frag @@ -4,32 +4,38 @@ void main(void) { - vec4 c = texture2D(uSampler, vTextureCoord); + // Un-premultiply alpha before applying the color matrix. See issue #3539. + if (c.a > 0.0) { + c.rgb /= c.a; + } + vec4 result; + result.r = (m[0] * c.r); + result.r += (m[1] * c.g); + result.r += (m[2] * c.b); + result.r += (m[3] * c.a); + result.r += m[4]; - gl_FragColor.r = (m[0] * c.r); - gl_FragColor.r += (m[1] * c.g); - gl_FragColor.r += (m[2] * c.b); - gl_FragColor.r += (m[3] * c.a); - gl_FragColor.r += m[4] * c.a; + result.g = (m[5] * c.r); + result.g += (m[6] * c.g); + result.g += (m[7] * c.b); + result.g += (m[8] * c.a); + result.g += m[9]; - gl_FragColor.g = (m[5] * c.r); - gl_FragColor.g += (m[6] * c.g); - gl_FragColor.g += (m[7] * c.b); - gl_FragColor.g += (m[8] * c.a); - gl_FragColor.g += m[9] * c.a; + result.b = (m[10] * c.r); + result.b += (m[11] * c.g); + result.b += (m[12] * c.b); + result.b += (m[13] * c.a); + result.b += m[14]; - gl_FragColor.b = (m[10] * c.r); - gl_FragColor.b += (m[11] * c.g); - gl_FragColor.b += (m[12] * c.b); - gl_FragColor.b += (m[13] * c.a); - gl_FragColor.b += m[14] * c.a; + result.a = (m[15] * c.r); + result.a += (m[16] * c.g); + result.a += (m[17] * c.b); + result.a += (m[18] * c.a); + result.a += m[19]; - gl_FragColor.a = (m[15] * c.r); - gl_FragColor.a += (m[16] * c.g); - gl_FragColor.a += (m[17] * c.b); - gl_FragColor.a += (m[18] * c.a); - gl_FragColor.a += m[19] * c.a; + // Premultiply alpha again. + result.rgb *= result.a; -// gl_FragColor = vec4(m[0]); + gl_FragColor = result; } diff --git a/src/filters/displacement/DisplacementFilter.js b/src/filters/displacement/DisplacementFilter.js index 112de0b..d5355c3 100644 --- a/src/filters/displacement/DisplacementFilter.js +++ b/src/filters/displacement/DisplacementFilter.js @@ -36,7 +36,7 @@ this.maskMatrix = maskMatrix; this.uniforms.mapSampler = sprite.texture; - this.uniforms.filterMatrix = maskMatrix.toArray(true); + this.uniforms.filterMatrix = maskMatrix; this.uniforms.scale = { x: 1, y: 1 }; if (scale === null || scale === undefined) diff --git a/src/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/InteractionData.js b/src/interaction/InteractionData.js index 02d53a3..818da35 100644 --- a/src/interaction/InteractionData.js +++ b/src/interaction/InteractionData.js @@ -21,9 +21,9 @@ this.global = new core.Point(); /** - * The target Sprite that was interacted with + * The target DisplayObject that was interacted with * - * @member {PIXI.Sprite} + * @member {PIXI.DisplayObject} */ this.target = null; @@ -33,6 +33,13 @@ * @member {Event} */ this.originalEvent = null; + + /** + * Unique identifier for this interaction + * + * @member {number} + */ + this.identifier = null; } /** diff --git a/src/interaction/InteractionEvent.js b/src/interaction/InteractionEvent.js index 75b2966..dc554f9 100644 --- a/src/interaction/InteractionEvent.js +++ b/src/interaction/InteractionEvent.js @@ -58,7 +58,7 @@ } /** - * Prevents event from reaching any objects other than the current object. + * Resets the event. * * @private */ diff --git a/src/interaction/InteractionManager.js b/src/interaction/InteractionManager.js index 7d51134..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 1271fef..71ade15 100644 --- a/package.json +++ b/package.json @@ -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/scripts/release.js b/scripts/release.js index c4c8e36..156620c 100644 --- a/scripts/release.js +++ b/scripts/release.js @@ -12,7 +12,8 @@ 'dist/**/*', 'lib/**/*', 'src/**/*', - 'scripts/**/*', + 'scripts/*', + 'scripts/renders/*', 'test/**/*', '*.json', '*.md', diff --git a/scripts/renders/client.js b/scripts/renders/client.js index 16c9565..6faee34 100755 --- a/scripts/renders/client.js +++ b/scripts/renders/client.js @@ -1,6 +1,6 @@ 'use strict'; -require('../../bin/pixi'); +require('../../dist/pixi'); PIXI.utils.skipHello(); const fs = require('fs'); 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 bc730db..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. @@ -33,8 +33,11 @@ * need to call toDataUrl on the webgl context * @param {number} [options.resolution=1] - The resolution / device pixel ratio of the renderer, retina would be 2 * @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 @@ -43,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. */ @@ -73,7 +99,7 @@ */ stop() { - this.ticker.stop(); + this._ticker.stop(); } /** @@ -81,7 +107,7 @@ */ start() { - this.ticker.start(); + this._ticker.start(); } /** @@ -95,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 index 81e076b..78b17e6 100644 --- a/src/core/Shader.js +++ b/src/core/Shader.js @@ -1,9 +1,7 @@ import { GLShader } from 'pixi-gl-core'; import settings from './settings'; -const { PRECISION } = settings; - -function checkPrecision(src) +function checkPrecision(src, def) { if (src instanceof Array) { @@ -11,14 +9,14 @@ { const copy = src.slice(0); - copy.unshift(`precision ${PRECISION} float;`); + copy.unshift(`precision ${def} float;`); return copy; } } else if (src.substring(0, 9) !== 'precision') { - return `precision ${PRECISION} float;\n${src}`; + return `precision ${def} float;\n${src}`; } return src; @@ -42,6 +40,7 @@ */ constructor(gl, vertexSrc, fragmentSrc) { - super(gl, checkPrecision(vertexSrc), checkPrecision(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 3425b41..f9235e4 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -181,7 +181,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 b3792ef..17fde8d 100644 --- a/src/core/display/Container.js +++ b/src/core/display/Container.js @@ -73,13 +73,14 @@ } child.parent = this; - - // ensure a transform will be recalculated.. - this.transform._parentID = -1; - this._boundsID++; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.push(child); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(this.children.length - 1); child.emit('added', this); @@ -108,9 +109,14 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.splice(index, 0, child); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(index); child.emit('added', this); @@ -174,6 +180,7 @@ removeItems(this.children, currentIndex, 1); // remove from old position this.children.splice(index, 0, child); // add at new position + this.onChildrenChange(index); } @@ -220,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! @@ -244,9 +252,14 @@ { const child = this.getChildAt(index); + // ensure child transform will be recalculated.. child.parent = null; + child.transform._parentID = -1; removeItems(this.children, index, 1); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(index); child.emit('removed', this); @@ -275,8 +288,14 @@ for (let i = 0; i < removed.length; ++i) { removed[i].parent = null; + if (removed[i].transform) + { + removed[i].transform._parentID = -1; + } } + this._boundsID++; + this.onChildrenChange(beginIndex); for (let i = 0; i < removed.length; ++i) diff --git a/src/core/display/DisplayObject.js b/src/core/display/DisplayObject.js index 3affe36..4e6c77d 100644 --- a/src/core/display/DisplayObject.js +++ b/src/core/display/DisplayObject.js @@ -109,7 +109,7 @@ /** * The original, cached mask of the object * - * @member {PIXI.Rectangle} + * @member {PIXI.Graphics|PIXI.Sprite} * @private */ this._mask = null; diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index 95f2af4..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 ); @@ -1065,10 +1061,15 @@ canvasRenderer = new CanvasRenderer(); } - tempMatrix.tx = -bounds.x; - tempMatrix.ty = -bounds.y; + this.transform.updateLocalTransform(); + this.transform.localTransform.copy(tempMatrix); - canvasRenderer.render(this, canvasBuffer, false, tempMatrix); + tempMatrix.invert(); + + tempMatrix.tx -= bounds.x; + tempMatrix.ty -= bounds.y; + + canvasRenderer.render(this, canvasBuffer, true, tempMatrix); const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode); 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 e67680f..867dde2 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/buildCircle.js b/src/core/graphics/webgl/utils/buildCircle.js index 0400d13..abf5d02 100644 --- a/src/core/graphics/webgl/utils/buildCircle.js +++ b/src/core/graphics/webgl/utils/buildCircle.js @@ -33,6 +33,11 @@ height = circleData.height; } + if (width === 0 || height === 0) + { + return; + } + const totalSegs = Math.floor(30 * Math.sqrt(circleData.radius)) || Math.floor(15 * Math.sqrt(circleData.width + circleData.height)); diff --git a/src/core/graphics/webgl/utils/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 90155f9..f03fe15 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -30,6 +30,7 @@ export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; export { default as CanvasGraphicsRenderer } from './graphics/canvas/CanvasGraphicsRenderer'; +export { default as Spritesheet } from './textures/Spritesheet'; export { default as Texture } from './textures/Texture'; export { default as BaseTexture } from './textures/BaseTexture'; export { default as RenderTexture } from './textures/RenderTexture'; diff --git a/src/core/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 ccce680..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); } /** @@ -162,6 +162,9 @@ if (transform) { transform.copy(tempWt); + + // lets not forget to flag the parent transform as dirty... + this._tempDisplayObjectParent.transform._worldID = -1; } else { @@ -169,6 +172,7 @@ } displayObject.parent = this._tempDisplayObjectParent; + displayObject.updateTransform(); displayObject.parent = cacheParent; // displayObject.hitArea = //TODO add a temp hit area @@ -279,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/canvas/utils/CanvasRenderTarget.js b/src/core/renderers/canvas/utils/CanvasRenderTarget.js index 3de0aef..b409891 100644 --- a/src/core/renderers/canvas/utils/CanvasRenderTarget.js +++ b/src/core/renderers/canvas/utils/CanvasRenderTarget.js @@ -1,5 +1,4 @@ import settings from '../../../settings'; -const { RESOLUTION } = settings; /** * Creates a Canvas element of the given size. @@ -30,7 +29,7 @@ */ this.context = this.canvas.getContext('2d'); - this.resolution = resolution || RESOLUTION; + this.resolution = resolution || settings.RESOLUTION; this.resize(width, height); } 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 ce0e1c4..788ed6c 100644 --- a/src/core/renderers/webgl/WebGLRenderer.js +++ b/src/core/renderers/webgl/WebGLRenderer.js @@ -30,8 +30,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 @@ -49,10 +49,19 @@ * enable this if you need to call toDataUrl on the webgl context. * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when * rendering, stopping pixel interpolation. + * @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; + + if (this.legacy) + { + glCore.VertexArrayObject.FORCE_NATIVE = true; + } /** * The type of this renderer as a standardised const @@ -229,7 +238,7 @@ this.emit('context', gl); // setup the width/height properties and gl viewport - this.resize(this.width, this.height); + this.resize(this.screen.width, this.screen.height); } /** @@ -323,16 +332,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) { @@ -376,6 +385,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 @@ -440,9 +469,10 @@ * Changes the current shader to the one given in parameter * * @param {PIXI.Shader} shader - the new shader + * @param {boolean} [autoProject=true] - Whether automatically set the projection matrix * @return {PIXI.WebGLRenderer} Returns itself. */ - bindShader(shader) + bindShader(shader, autoProject) { // TODO cache if (this._activeShader !== shader) @@ -450,8 +480,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 bbab3b3..944f41d 100644 --- a/src/core/renderers/webgl/filters/Filter.js +++ b/src/core/renderers/webgl/filters/Filter.js @@ -1,6 +1,7 @@ import extractUniformsFromSrc from './extractUniformsFromSrc'; import { uid } from '../../../utils'; import { BLEND_MODES } from '../../../const'; +import settings from '../../../settings'; const SOURCE_KEY_MAP = {}; @@ -10,7 +11,7 @@ * @memberof PIXI * @extends PIXI.Shader */ -class Filter +export default class Filter { /** * @param {string} [vertexSrc] - The source of the vertex shader. @@ -35,8 +36,6 @@ this.blendMode = BLEND_MODES.NORMAL; - // pull out the vertex and shader uniforms if they are not specified.. - // currently this does not extract structs only default types this.uniformData = uniforms || extractUniformsFromSrc(this.vertexSrc, this.fragmentSrc, 'projectionMatrix|uSampler'); /** @@ -80,7 +79,7 @@ * * @member {number} */ - this.resolution = 1; + this.resolution = settings.RESOLUTION; /** * If enabled is true the filter is applied, if false it will not. @@ -97,8 +96,11 @@ * @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 { // --- // // this.uniforms.filterMatrix = filterManager.calculateSpriteMatrix(tempMatrix, window.panda ); @@ -170,5 +172,3 @@ ].join('\n'); } } - -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 dc7072a..6c7422d 100644 --- a/src/core/renderers/webgl/managers/FilterManager.js +++ b/src/core/renderers/webgl/managers/FilterManager.js @@ -154,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 @@ -176,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; @@ -184,7 +184,7 @@ flop = t; } - filters[i].apply(this, flip, lastState.renderTarget, true); + filters[i].apply(this, flip, lastState.renderTarget, false, currentState); this.freePotRenderTarget(flip); this.freePotRenderTarget(flop); @@ -291,9 +291,13 @@ let textureCount = 1; let currentState; - if (shader.uniforms.data.filterArea) + // 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; @@ -306,9 +310,9 @@ // use this to clamp displaced texture coords so they belong to filterArea // see displacementFilter fragment shader for an example - if (shader.uniforms.data.filterClamp) + if (shader.uniforms.filterClamp) { - currentState = this.filterData.stack[this.filterData.index]; + currentState = currentState || this.filterData.stack[this.filterData.index]; const filterClamp = shader.uniforms.filterClamp; @@ -485,7 +489,7 @@ */ destroy() { - this.shaderCache = []; + this.shaderCache = {}; this.emptyPool(); } diff --git a/src/core/renderers/webgl/utils/RenderTarget.js b/src/core/renderers/webgl/utils/RenderTarget.js index afe51ab..6d2467f 100644 --- a/src/core/renderers/webgl/utils/RenderTarget.js +++ b/src/core/renderers/webgl/utils/RenderTarget.js @@ -124,7 +124,7 @@ * @default PIXI.settings.SCALE_MODE * @see PIXI.SCALE_MODES */ - this.scaleMode = scaleMode || settings.SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; /** * Whether this object is the root element or not 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 36ea5bb..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; @@ -346,8 +346,8 @@ /** * Gets the local bounds of the sprite object. * - * @param {Rectangle} rect - The output rectangle. - * @return {Rectangle} The bounds. + * @param {PIXI.Rectangle} rect - The output rectangle. + * @return {PIXI.Rectangle} The bounds. */ getLocalBounds(rect) { @@ -440,7 +440,7 @@ * * @static * @param {number|string|PIXI.BaseTexture|HTMLCanvasElement|HTMLVideoElement} source Source to create texture from - * @return {PIXI.Texture} 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 e77b39a..90b08c0 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++) @@ -103,11 +102,18 @@ { const gl = this.renderer.gl; - // step 1: first check max textures the GPU can handle. - this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); + if (this.renderer.legacy) + { + this.MAX_TEXTURES = 1; + } + else + { + // step 1: first check max textures the GPU can handle. + this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); - // step 2: check the maximum number of if statements the shader can have too.. - this.MAX_TEXTURES = checkMaxIfStatmentsInShader(this.MAX_TEXTURES, gl); + // step 2: check the maximum number of if statements the shader can have too.. + this.MAX_TEXTURES = checkMaxIfStatmentsInShader(this.MAX_TEXTURES, gl); + } const shader = this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); @@ -130,8 +136,12 @@ .addIndex(this.indexBuffer) .addAttribute(this.vertexBuffers[i], shader.attributes.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) .addAttribute(this.vertexBuffers[i], shader.attributes.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) - .addAttribute(this.vertexBuffers[i], shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4) - .addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + .addAttribute(this.vertexBuffers[i], shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); + + if (shader.attributes.aTextureId) + { + this.vaos[i].addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + } /* eslint-enable max-len */ } @@ -378,8 +388,12 @@ .addIndex(this.indexBuffer) .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) - .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4) - .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); + + if (this.shader.attributes.aTextureId) + { + this.vaos[this.vertexCount].addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + } /* eslint-enable max-len */ } diff --git a/src/core/text/Text.js b/src/core/text/Text.js index 013625a..cec8c32 100644 --- a/src/core/text/Text.js +++ b/src/core/text/Text.js @@ -6,6 +6,7 @@ import { TEXT_GRADIENT } from '../const'; import settings from '../settings'; import TextStyle from './TextStyle'; +import trimCanvas from '../utils/trimCanvas'; const defaultDestroyOptions = { texture: true, @@ -57,7 +58,7 @@ /** * The canvas 2d context that everything is drawn with - * @member {HTMLCanvasElement} + * @member {CanvasRenderingContext2D} */ this.context = this.canvas.getContext('2d'); @@ -189,10 +190,12 @@ if (style.dropShadow) { + this.context.shadowBlur = style.dropShadowBlur; + this.context.globalAlpha = style.dropShadowAlpha; + if (style.dropShadowBlur > 0) { this.context.shadowColor = style.dropShadowColor; - this.context.shadowBlur = style.dropShadowBlur; } else { @@ -237,6 +240,10 @@ } } + // reset the shadow blur and alpha that was set by the drop shadow, for the regular text + this.context.shadowBlur = 0; + this.context.globalAlpha = 1; + // set canvas text styles this.context.fillStyle = this._generateFillStyle(style, lines); @@ -326,6 +333,15 @@ */ updateTexture() { + if (this._style.trim) + { + const trimmed = trimCanvas(this.canvas); + + this.canvas.width = trimmed.width; + this.canvas.height = trimmed.height; + this.context.putImageData(trimmed.data, 0, 0); + } + const texture = this._texture; const style = this._style; @@ -475,6 +491,19 @@ } /** + * Gets the local bounds of the text object. + * + * @param {Rectangle} rect - The output rectangle. + * @return {Rectangle} The bounds. + */ + getLocalBounds(rect) + { + this.updateText(true); + + return super.getLocalBounds.call(this, rect); + } + + /** * calculates the bounds of the Text as a rectangle. The bounds calculation takes the worldTransform into account. */ _calculateBounds() @@ -525,6 +554,29 @@ const width = this.canvas.width / this.resolution; const height = this.canvas.height / this.resolution; + // make a copy of the style settings, so we can manipulate them later + const fill = style.fill.slice(); + const fillGradientStops = style.fillGradientStops.slice(); + + // wanting to evenly distribute the fills. So an array of 4 colours should give fills of 0.25, 0.5 and 0.75 + if (!fillGradientStops.length) + { + const lengthPlus1 = fill.length + 1; + + for (let i = 1; i < lengthPlus1; ++i) + { + fillGradientStops.push(i / lengthPlus1); + } + } + + // stop the bleeding of the last gradient on the line above to the top gradient of the this line + // by hard defining the first gradient colour at point 0, and last gradient colour at point 1 + fill.unshift(style.fill[0]); + fillGradientStops.unshift(0); + + fill.push(style.fill[style.fill.length - 1]); + fillGradientStops.push(1); + if (style.fillGradientType === TEXT_GRADIENT.LINEAR_VERTICAL) { // start the gradient at the top center of the canvas, and end at the bottom middle of the canvas @@ -532,15 +584,22 @@ // we need to repeat the gradient so that each individual line of text has the same vertical gradient effect // ['#FF0000', '#00FF00', '#0000FF'] over 2 lines would create stops at 0.125, 0.25, 0.375, 0.625, 0.75, 0.875 - totalIterations = (style.fill.length + 1) * lines.length; + totalIterations = (fill.length + 1) * lines.length; currentIteration = 0; for (let i = 0; i < lines.length; i++) { currentIteration += 1; - for (let j = 0; j < style.fill.length; j++) + for (let j = 0; j < fill.length; j++) { - stop = (currentIteration / totalIterations); - gradient.addColorStop(stop, style.fill[j]); + if (fillGradientStops[j]) + { + stop = (fillGradientStops[j] / lines.length) + (i / lines.length); + } + else + { + stop = currentIteration / totalIterations; + } + gradient.addColorStop(stop, fill[j]); currentIteration++; } } @@ -552,13 +611,20 @@ // can just evenly space out the gradients in this case, as multiple lines makes no difference // to an even left to right gradient - totalIterations = style.fill.length + 1; + totalIterations = fill.length + 1; currentIteration = 1; - for (let i = 0; i < style.fill.length; i++) + for (let i = 0; i < fill.length; i++) { - stop = currentIteration / totalIterations; - gradient.addColorStop(stop, style.fill[i]); + if (fillGradientStops[i]) + { + stop = fillGradientStops[i]; + } + else + { + stop = currentIteration / totalIterations; + } + gradient.addColorStop(stop, fill[i]); currentIteration++; } } @@ -569,7 +635,7 @@ /** * Destroys this text object. * Note* Unlike a Sprite, a Text object will automatically destroy its baseTexture and texture as - * the majorety of the time the texture will not be shared with any other Sprites. + * the majority of the time the texture will not be shared with any other Sprites. * * @param {object|boolean} [options] - Options parameter. A boolean will act as if all options * have been set to that value @@ -680,7 +746,7 @@ set text(text) // eslint-disable-line require-jsdoc { - text = String(text || ' '); + text = String(text === '' || text === null || text === undefined ? ' ' : text); if (this._text === text) { diff --git a/src/core/text/TextStyle.js b/src/core/text/TextStyle.js index 3b25528..890950b 100644 --- a/src/core/text/TextStyle.js +++ b/src/core/text/TextStyle.js @@ -8,12 +8,14 @@ align: 'left', breakWords: false, dropShadow: false, + dropShadowAlpha: 1, dropShadowAngle: Math.PI / 6, dropShadowBlur: 0, dropShadowColor: '#000000', dropShadowDistance: 5, fill: 'black', fillGradientType: TEXT_GRADIENT.LINEAR_VERTICAL, + fillGradientStops: [], fontFamily: 'Arial', fontSize: 26, fontStyle: 'normal', @@ -27,6 +29,7 @@ stroke: 'black', strokeThickness: 0, textBaseline: 'alphabetic', + trim: false, wordWrap: false, wordWrapWidth: 100, }; @@ -47,6 +50,7 @@ * @param {boolean} [style.breakWords=false] - Indicates if lines can be wrapped within words, it * needs wordWrap to be set to true * @param {boolean} [style.dropShadow=false] - Set a drop shadow for the text + * @param {number} [style.dropShadowAlpha=1] - Set alpha for the drop shadow * @param {number} [style.dropShadowAngle=Math.PI/6] - Set a angle of the drop shadow * @param {number} [style.dropShadowBlur=0] - Set a shadow blur radius * @param {string} [style.dropShadowColor='#000000'] - A fill style to be used on the dropshadow e.g 'red', '#00FF00' @@ -55,8 +59,10 @@ * fillstyle that will be used on the text e.g 'red', '#00FF00'. Can be an array to create a gradient * eg ['#000000','#FFFFFF'] * {@link https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/fillStyle|MDN} - * @param {number} [style.fillGradientType=PIXI.TEXT_GRADIENT.LINEAR_VERTICAL] - If fills styles are - * supplied, this can change the type/direction of the gradient. See {@link PIXI.TEXT_GRADIENT} for possible values + * @param {number} [style.fillGradientType=PIXI.TEXT_GRADIENT.LINEAR_VERTICAL] - If fill is an array of colours + * to create a gradient, this can change the type/direction of the gradient. See {@link PIXI.TEXT_GRADIENT} + * @param {number[]} [style.fillGradientStops] - If fill is an array of colours to create a gradient, this array can set + * the stop points (numbers between 0 and 1) for the color, overriding the default behaviour of evenly spacing them. * @param {string|string[]} [style.fontFamily='Arial'] - The font family * @param {number|string} [style.fontSize=26] - The font size (as a number it converts to px, but as a string, * equivalents are '26px','20pt','160%' or '1.6em') @@ -76,6 +82,7 @@ * e.g 'blue', '#FCFF00' * @param {number} [style.strokeThickness=0] - A number that represents the thickness of the stroke. * Default is 0 (no stroke) + * @param {boolean} [style.trim=false] - Trim transparent borders * @param {string} [style.textBaseline='alphabetic'] - The baseline of the text that is rendered. * @param {boolean} [style.wordWrap=false] - Indicates if word wrap should be used * @param {number} [style.wordWrapWidth=100] - The width at which text will wrap, it needs wordWrap to be set to true @@ -152,6 +159,19 @@ } } + get dropShadowAlpha() + { + return this._dropShadowAlpha; + } + set dropShadowAlpha(dropShadowAlpha) + { + if (this._dropShadowAlpha !== dropShadowAlpha) + { + this._dropShadowAlpha = dropShadowAlpha; + this.styleID++; + } + } + get dropShadowAngle() { return this._dropShadowAngle; @@ -232,6 +252,19 @@ } } + get fillGradientStops() + { + return this._fillGradientStops; + } + set fillGradientStops(fillGradientStops) + { + if (!areArraysEqual(this._fillGradientStops,fillGradientStops)) + { + this._fillGradientStops = fillGradientStops; + this.styleID++; + } + } + get fontFamily() { return this._fontFamily; @@ -402,6 +435,19 @@ } } + get trim() + { + return this._trim; + } + set trim(trim) + { + if (this._trim !== trim) + { + this._trim = trim; + this.styleID++; + } + } + get wordWrap() { return this._wordWrap; @@ -475,3 +521,34 @@ return color; } } + +/** + * Utility function to convert hexadecimal colors to strings, and simply return the color if it's a string. + * This version can also convert array of colors + * + * @param {Array} array1 First array to compare + * @param {Array} array2 Second array to compare + * @return {boolean} Do the arrays contain the same values in the same order + */ +function areArraysEqual(array1, array2) +{ + if (!Array.isArray(array1) || !Array.isArray(array2)) + { + return false; + } + + if (array1.length !== array2.length) + { + return false; + } + + for (let i = 0; i < array1.length; ++i) + { + if (array1[i] !== array2[i]) + { + return false; + } + } + + return true; +} diff --git a/src/core/textures/BaseRenderTexture.js b/src/core/textures/BaseRenderTexture.js index 5d50146..aa727f0 100644 --- a/src/core/textures/BaseRenderTexture.js +++ b/src/core/textures/BaseRenderTexture.js @@ -1,8 +1,6 @@ import BaseTexture from './BaseTexture'; import settings from '../settings'; -const { RESOLUTION, SCALE_MODE } = settings; - /** * A BaseRenderTexture is a special texture that allows any Pixi display object to be rendered to it. * @@ -54,7 +52,7 @@ { super(null, scaleMode); - this.resolution = resolution || RESOLUTION; + this.resolution = resolution || settings.RESOLUTION; this.width = width; this.height = height; @@ -62,7 +60,7 @@ this.realWidth = this.width * this.resolution; this.realHeight = this.height * this.resolution; - this.scaleMode = scaleMode || SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; this.hasLoaded = true; /** diff --git a/src/core/textures/BaseTexture.js b/src/core/textures/BaseTexture.js index 676dca3..aeccf39 100644 --- a/src/core/textures/BaseTexture.js +++ b/src/core/textures/BaseTexture.js @@ -77,7 +77,7 @@ * @default PIXI.settings.SCALE_MODE * @see PIXI.SCALE_MODES */ - this.scaleMode = scaleMode || settings.SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; /** * Set to true once the base texture has successfully loaded. @@ -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/RenderTexture.js b/src/core/textures/RenderTexture.js index 4c5034f..b3c6297 100644 --- a/src/core/textures/RenderTexture.js +++ b/src/core/textures/RenderTexture.js @@ -54,8 +54,8 @@ /* eslint-disable prefer-rest-params, no-console */ const width = arguments[1]; const height = arguments[2]; - const scaleMode = arguments[3] || 0; - const resolution = arguments[4] || 1; + const scaleMode = arguments[3]; + const resolution = arguments[4]; // we have an old render texture.. console.warn(`Please use RenderTexture.create(${width}, ${height}) instead of the ctor directly.`); 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 ca31456..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 @@ -459,7 +497,9 @@ if (frame.x + frame.width > this.baseTexture.width || frame.y + frame.height > this.baseTexture.height) { - throw new Error(`Texture Error: frame does not fit inside the base Texture dimensions ${this}`); + throw new Error('Texture Error: frame does not fit inside the base Texture dimensions: ' + + `X: ${frame.x} + ${frame.width} > ${this.baseTexture.width} ` + + `Y: ${frame.y} + ${frame.height} > ${this.baseTexture.height}`); } // this.valid = frame && frame.width && frame.height && this.baseTexture.source && this.baseTexture.hasLoaded; @@ -520,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. @@ -528,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 2a7497b..99e585a 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)); } /** @@ -360,3 +362,43 @@ * @private */ export const BaseTextureCache = {}; + +/** + * 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/core/utils/trimCanvas.js b/src/core/utils/trimCanvas.js new file mode 100644 index 0000000..5af6de0 --- /dev/null +++ b/src/core/utils/trimCanvas.js @@ -0,0 +1,83 @@ +/** + * Trim transparent borders from a canvas + * + * @memberof PIXI + * @function trimCanvas + * @private + * @param {HTMLCanvasElement} canvas - the canvas to trim + * @returns {object} Trim data + */ +export default function trimCanvas(canvas) +{ + // https://gist.github.com/remy/784508 + + let width = canvas.width; + let height = canvas.height; + + const context = canvas.getContext('2d'); + const imageData = context.getImageData(0, 0, width, height); + const pixels = imageData.data; + const len = pixels.length; + + const bound = { + top: null, + left: null, + right: null, + bottom: null, + }; + let i; + let x; + let y; + + for (i = 0; i < len; i += 4) + { + if (pixels[i + 3] !== 0) + { + x = (i / 4) % width; + y = ~~((i / 4) / width); + + if (bound.top === null) + { + bound.top = y; + } + + if (bound.left === null) + { + bound.left = x; + } + else if (x < bound.left) + { + bound.left = x; + } + + if (bound.right === null) + { + bound.right = x + 1; + } + else if (bound.right < x) + { + bound.right = x + 1; + } + + if (bound.bottom === null) + { + bound.bottom = y; + } + else if (bound.bottom < y) + { + bound.bottom = y; + } + } + } + + width = bound.right - bound.left; + height = bound.bottom - bound.top + 1; + + const data = context.getImageData(bound.left, bound.top, width, height); + + return { + height, + width, + data, + }; +} diff --git a/src/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/extras/TilingSprite.js b/src/extras/TilingSprite.js index f90d248..fe82451 100644 --- a/src/extras/TilingSprite.js +++ b/src/extras/TilingSprite.js @@ -220,20 +220,33 @@ transform.tx * resolution, transform.ty * resolution); - // TODO - this should be rolled into the setTransform above.. - context.scale(this.tileScale.x / baseTextureResolution, this.tileScale.y / baseTextureResolution); - - context.translate(modX + (this.anchor.x * -this._width), - modY + (this.anchor.y * -this._height)); - renderer.setBlendMode(this.blendMode); // fill the pattern! context.fillStyle = this._canvasPattern; - context.fillRect(-modX, - -modY, - this._width / this.tileScale.x * baseTextureResolution, - this._height / this.tileScale.y * baseTextureResolution); + + // TODO - this should be rolled into the setTransform above.. + context.scale(this.tileScale.x / baseTextureResolution, this.tileScale.y / baseTextureResolution); + + const anchorX = this.anchor.x * -this._width; + const anchorY = this.anchor.y * -this._height; + + if (this.uvRespectAnchor) + { + context.translate(modX, modY); + + context.fillRect(-modX + anchorX, -modY + anchorY, + this._width / this.tileScale.x * baseTextureResolution, + this._height / this.tileScale.y * baseTextureResolution); + } + else + { + context.translate(modX + anchorX, modY + anchorY); + + context.fillRect(-modX, -modY, + this._width / this.tileScale.x * baseTextureResolution, + this._height / this.tileScale.y * baseTextureResolution); + } } /** diff --git a/src/extras/cacheAsBitmap.js b/src/extras/cacheAsBitmap.js index 18043a7..5dd5640 100644 --- a/src/extras/cacheAsBitmap.js +++ b/src/extras/cacheAsBitmap.js @@ -41,6 +41,9 @@ * provide a performance benefit for complex static displayObjects. * To remove simply set this property to 'false' * + * IMPORTANT GOTCHA - make sure that all your textures are preloaded BEFORE setting this property to true + * as it will take a snapshot of what is currently there. If the textures have not loaded then they will not appear. + * * @member {boolean} * @memberof PIXI.DisplayObject# */ diff --git a/src/filters/blur/BlurFilter.js b/src/filters/blur/BlurFilter.js index 340fc58..1575db9 100644 --- a/src/filters/blur/BlurFilter.js +++ b/src/filters/blur/BlurFilter.js @@ -24,10 +24,9 @@ this.blurXFilter = new BlurXFilter(strength, quality, resolution, kernelSize); this.blurYFilter = new BlurYFilter(strength, quality, resolution, kernelSize); - this.resolution = 1; this.padding = 0; - this.resolution = resolution || 1; + this.resolution = resolution || core.settings.RESOLUTION; this.quality = quality || 4; this.blur = strength || 8; } diff --git a/src/filters/blur/BlurXFilter.js b/src/filters/blur/BlurXFilter.js index def99c3..0f48889 100644 --- a/src/filters/blur/BlurXFilter.js +++ b/src/filters/blur/BlurXFilter.js @@ -31,7 +31,7 @@ fragSrc ); - this.resolution = resolution || 1; + this.resolution = resolution || core.settings.RESOLUTION; this._quality = 0; diff --git a/src/filters/blur/BlurYFilter.js b/src/filters/blur/BlurYFilter.js index 638e0f0..6d489c7 100644 --- a/src/filters/blur/BlurYFilter.js +++ b/src/filters/blur/BlurYFilter.js @@ -31,7 +31,7 @@ fragSrc ); - this.resolution = resolution || 1; + this.resolution = resolution || core.settings.RESOLUTION; this._quality = 0; diff --git a/src/filters/colormatrix/ColorMatrixFilter.js b/src/filters/colormatrix/ColorMatrixFilter.js index 8509505..679f8ec 100644 --- a/src/filters/colormatrix/ColorMatrixFilter.js +++ b/src/filters/colormatrix/ColorMatrixFilter.js @@ -75,28 +75,28 @@ out[1] = (a[0] * b[1]) + (a[1] * b[6]) + (a[2] * b[11]) + (a[3] * b[16]); out[2] = (a[0] * b[2]) + (a[1] * b[7]) + (a[2] * b[12]) + (a[3] * b[17]); out[3] = (a[0] * b[3]) + (a[1] * b[8]) + (a[2] * b[13]) + (a[3] * b[18]); - out[4] = (a[0] * b[4]) + (a[1] * b[9]) + (a[2] * b[14]) + (a[3] * b[19]); + out[4] = (a[0] * b[4]) + (a[1] * b[9]) + (a[2] * b[14]) + (a[3] * b[19]) + a[4]; // Green Channel out[5] = (a[5] * b[0]) + (a[6] * b[5]) + (a[7] * b[10]) + (a[8] * b[15]); out[6] = (a[5] * b[1]) + (a[6] * b[6]) + (a[7] * b[11]) + (a[8] * b[16]); out[7] = (a[5] * b[2]) + (a[6] * b[7]) + (a[7] * b[12]) + (a[8] * b[17]); out[8] = (a[5] * b[3]) + (a[6] * b[8]) + (a[7] * b[13]) + (a[8] * b[18]); - out[9] = (a[5] * b[4]) + (a[6] * b[9]) + (a[7] * b[14]) + (a[8] * b[19]); + out[9] = (a[5] * b[4]) + (a[6] * b[9]) + (a[7] * b[14]) + (a[8] * b[19]) + a[9]; // Blue Channel out[10] = (a[10] * b[0]) + (a[11] * b[5]) + (a[12] * b[10]) + (a[13] * b[15]); out[11] = (a[10] * b[1]) + (a[11] * b[6]) + (a[12] * b[11]) + (a[13] * b[16]); out[12] = (a[10] * b[2]) + (a[11] * b[7]) + (a[12] * b[12]) + (a[13] * b[17]); out[13] = (a[10] * b[3]) + (a[11] * b[8]) + (a[12] * b[13]) + (a[13] * b[18]); - out[14] = (a[10] * b[4]) + (a[11] * b[9]) + (a[12] * b[14]) + (a[13] * b[19]); + out[14] = (a[10] * b[4]) + (a[11] * b[9]) + (a[12] * b[14]) + (a[13] * b[19]) + a[14]; // Alpha Channel out[15] = (a[15] * b[0]) + (a[16] * b[5]) + (a[17] * b[10]) + (a[18] * b[15]); out[16] = (a[15] * b[1]) + (a[16] * b[6]) + (a[17] * b[11]) + (a[18] * b[16]); out[17] = (a[15] * b[2]) + (a[16] * b[7]) + (a[17] * b[12]) + (a[18] * b[17]); out[18] = (a[15] * b[3]) + (a[16] * b[8]) + (a[17] * b[13]) + (a[18] * b[18]); - out[19] = (a[15] * b[4]) + (a[16] * b[9]) + (a[17] * b[14]) + (a[18] * b[19]); + out[19] = (a[15] * b[4]) + (a[16] * b[9]) + (a[17] * b[14]) + (a[18] * b[19]) + a[19]; return out; } diff --git a/src/filters/colormatrix/colorMatrix.frag b/src/filters/colormatrix/colorMatrix.frag index 3aaf2b3..c73c0e9 100644 --- a/src/filters/colormatrix/colorMatrix.frag +++ b/src/filters/colormatrix/colorMatrix.frag @@ -4,32 +4,38 @@ void main(void) { - vec4 c = texture2D(uSampler, vTextureCoord); + // Un-premultiply alpha before applying the color matrix. See issue #3539. + if (c.a > 0.0) { + c.rgb /= c.a; + } + vec4 result; + result.r = (m[0] * c.r); + result.r += (m[1] * c.g); + result.r += (m[2] * c.b); + result.r += (m[3] * c.a); + result.r += m[4]; - gl_FragColor.r = (m[0] * c.r); - gl_FragColor.r += (m[1] * c.g); - gl_FragColor.r += (m[2] * c.b); - gl_FragColor.r += (m[3] * c.a); - gl_FragColor.r += m[4] * c.a; + result.g = (m[5] * c.r); + result.g += (m[6] * c.g); + result.g += (m[7] * c.b); + result.g += (m[8] * c.a); + result.g += m[9]; - gl_FragColor.g = (m[5] * c.r); - gl_FragColor.g += (m[6] * c.g); - gl_FragColor.g += (m[7] * c.b); - gl_FragColor.g += (m[8] * c.a); - gl_FragColor.g += m[9] * c.a; + result.b = (m[10] * c.r); + result.b += (m[11] * c.g); + result.b += (m[12] * c.b); + result.b += (m[13] * c.a); + result.b += m[14]; - gl_FragColor.b = (m[10] * c.r); - gl_FragColor.b += (m[11] * c.g); - gl_FragColor.b += (m[12] * c.b); - gl_FragColor.b += (m[13] * c.a); - gl_FragColor.b += m[14] * c.a; + result.a = (m[15] * c.r); + result.a += (m[16] * c.g); + result.a += (m[17] * c.b); + result.a += (m[18] * c.a); + result.a += m[19]; - gl_FragColor.a = (m[15] * c.r); - gl_FragColor.a += (m[16] * c.g); - gl_FragColor.a += (m[17] * c.b); - gl_FragColor.a += (m[18] * c.a); - gl_FragColor.a += m[19] * c.a; + // Premultiply alpha again. + result.rgb *= result.a; -// gl_FragColor = vec4(m[0]); + gl_FragColor = result; } diff --git a/src/filters/displacement/DisplacementFilter.js b/src/filters/displacement/DisplacementFilter.js index 112de0b..d5355c3 100644 --- a/src/filters/displacement/DisplacementFilter.js +++ b/src/filters/displacement/DisplacementFilter.js @@ -36,7 +36,7 @@ this.maskMatrix = maskMatrix; this.uniforms.mapSampler = sprite.texture; - this.uniforms.filterMatrix = maskMatrix.toArray(true); + this.uniforms.filterMatrix = maskMatrix; this.uniforms.scale = { x: 1, y: 1 }; if (scale === null || scale === undefined) diff --git a/src/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/InteractionData.js b/src/interaction/InteractionData.js index 02d53a3..818da35 100644 --- a/src/interaction/InteractionData.js +++ b/src/interaction/InteractionData.js @@ -21,9 +21,9 @@ this.global = new core.Point(); /** - * The target Sprite that was interacted with + * The target DisplayObject that was interacted with * - * @member {PIXI.Sprite} + * @member {PIXI.DisplayObject} */ this.target = null; @@ -33,6 +33,13 @@ * @member {Event} */ this.originalEvent = null; + + /** + * Unique identifier for this interaction + * + * @member {number} + */ + this.identifier = null; } /** diff --git a/src/interaction/InteractionEvent.js b/src/interaction/InteractionEvent.js index 75b2966..dc554f9 100644 --- a/src/interaction/InteractionEvent.js +++ b/src/interaction/InteractionEvent.js @@ -58,7 +58,7 @@ } /** - * Prevents event from reaching any objects other than the current object. + * Resets the event. * * @private */ diff --git a/src/interaction/InteractionManager.js b/src/interaction/InteractionManager.js index 7d51134..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 1271fef..71ade15 100644 --- a/package.json +++ b/package.json @@ -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/scripts/release.js b/scripts/release.js index c4c8e36..156620c 100644 --- a/scripts/release.js +++ b/scripts/release.js @@ -12,7 +12,8 @@ 'dist/**/*', 'lib/**/*', 'src/**/*', - 'scripts/**/*', + 'scripts/*', + 'scripts/renders/*', 'test/**/*', '*.json', '*.md', diff --git a/scripts/renders/client.js b/scripts/renders/client.js index 16c9565..6faee34 100755 --- a/scripts/renders/client.js +++ b/scripts/renders/client.js @@ -1,6 +1,6 @@ 'use strict'; -require('../../bin/pixi'); +require('../../dist/pixi'); PIXI.utils.skipHello(); const fs = require('fs'); 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 bc730db..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. @@ -33,8 +33,11 @@ * need to call toDataUrl on the webgl context * @param {number} [options.resolution=1] - The resolution / device pixel ratio of the renderer, retina would be 2 * @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 @@ -43,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. */ @@ -73,7 +99,7 @@ */ stop() { - this.ticker.stop(); + this._ticker.stop(); } /** @@ -81,7 +107,7 @@ */ start() { - this.ticker.start(); + this._ticker.start(); } /** @@ -95,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 index 81e076b..78b17e6 100644 --- a/src/core/Shader.js +++ b/src/core/Shader.js @@ -1,9 +1,7 @@ import { GLShader } from 'pixi-gl-core'; import settings from './settings'; -const { PRECISION } = settings; - -function checkPrecision(src) +function checkPrecision(src, def) { if (src instanceof Array) { @@ -11,14 +9,14 @@ { const copy = src.slice(0); - copy.unshift(`precision ${PRECISION} float;`); + copy.unshift(`precision ${def} float;`); return copy; } } else if (src.substring(0, 9) !== 'precision') { - return `precision ${PRECISION} float;\n${src}`; + return `precision ${def} float;\n${src}`; } return src; @@ -42,6 +40,7 @@ */ constructor(gl, vertexSrc, fragmentSrc) { - super(gl, checkPrecision(vertexSrc), checkPrecision(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 3425b41..f9235e4 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -181,7 +181,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 b3792ef..17fde8d 100644 --- a/src/core/display/Container.js +++ b/src/core/display/Container.js @@ -73,13 +73,14 @@ } child.parent = this; - - // ensure a transform will be recalculated.. - this.transform._parentID = -1; - this._boundsID++; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.push(child); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(this.children.length - 1); child.emit('added', this); @@ -108,9 +109,14 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.splice(index, 0, child); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(index); child.emit('added', this); @@ -174,6 +180,7 @@ removeItems(this.children, currentIndex, 1); // remove from old position this.children.splice(index, 0, child); // add at new position + this.onChildrenChange(index); } @@ -220,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! @@ -244,9 +252,14 @@ { const child = this.getChildAt(index); + // ensure child transform will be recalculated.. child.parent = null; + child.transform._parentID = -1; removeItems(this.children, index, 1); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(index); child.emit('removed', this); @@ -275,8 +288,14 @@ for (let i = 0; i < removed.length; ++i) { removed[i].parent = null; + if (removed[i].transform) + { + removed[i].transform._parentID = -1; + } } + this._boundsID++; + this.onChildrenChange(beginIndex); for (let i = 0; i < removed.length; ++i) diff --git a/src/core/display/DisplayObject.js b/src/core/display/DisplayObject.js index 3affe36..4e6c77d 100644 --- a/src/core/display/DisplayObject.js +++ b/src/core/display/DisplayObject.js @@ -109,7 +109,7 @@ /** * The original, cached mask of the object * - * @member {PIXI.Rectangle} + * @member {PIXI.Graphics|PIXI.Sprite} * @private */ this._mask = null; diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index 95f2af4..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 ); @@ -1065,10 +1061,15 @@ canvasRenderer = new CanvasRenderer(); } - tempMatrix.tx = -bounds.x; - tempMatrix.ty = -bounds.y; + this.transform.updateLocalTransform(); + this.transform.localTransform.copy(tempMatrix); - canvasRenderer.render(this, canvasBuffer, false, tempMatrix); + tempMatrix.invert(); + + tempMatrix.tx -= bounds.x; + tempMatrix.ty -= bounds.y; + + canvasRenderer.render(this, canvasBuffer, true, tempMatrix); const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode); 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 e67680f..867dde2 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/buildCircle.js b/src/core/graphics/webgl/utils/buildCircle.js index 0400d13..abf5d02 100644 --- a/src/core/graphics/webgl/utils/buildCircle.js +++ b/src/core/graphics/webgl/utils/buildCircle.js @@ -33,6 +33,11 @@ height = circleData.height; } + if (width === 0 || height === 0) + { + return; + } + const totalSegs = Math.floor(30 * Math.sqrt(circleData.radius)) || Math.floor(15 * Math.sqrt(circleData.width + circleData.height)); diff --git a/src/core/graphics/webgl/utils/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 90155f9..f03fe15 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -30,6 +30,7 @@ export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; export { default as CanvasGraphicsRenderer } from './graphics/canvas/CanvasGraphicsRenderer'; +export { default as Spritesheet } from './textures/Spritesheet'; export { default as Texture } from './textures/Texture'; export { default as BaseTexture } from './textures/BaseTexture'; export { default as RenderTexture } from './textures/RenderTexture'; diff --git a/src/core/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 ccce680..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); } /** @@ -162,6 +162,9 @@ if (transform) { transform.copy(tempWt); + + // lets not forget to flag the parent transform as dirty... + this._tempDisplayObjectParent.transform._worldID = -1; } else { @@ -169,6 +172,7 @@ } displayObject.parent = this._tempDisplayObjectParent; + displayObject.updateTransform(); displayObject.parent = cacheParent; // displayObject.hitArea = //TODO add a temp hit area @@ -279,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/canvas/utils/CanvasRenderTarget.js b/src/core/renderers/canvas/utils/CanvasRenderTarget.js index 3de0aef..b409891 100644 --- a/src/core/renderers/canvas/utils/CanvasRenderTarget.js +++ b/src/core/renderers/canvas/utils/CanvasRenderTarget.js @@ -1,5 +1,4 @@ import settings from '../../../settings'; -const { RESOLUTION } = settings; /** * Creates a Canvas element of the given size. @@ -30,7 +29,7 @@ */ this.context = this.canvas.getContext('2d'); - this.resolution = resolution || RESOLUTION; + this.resolution = resolution || settings.RESOLUTION; this.resize(width, height); } 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 ce0e1c4..788ed6c 100644 --- a/src/core/renderers/webgl/WebGLRenderer.js +++ b/src/core/renderers/webgl/WebGLRenderer.js @@ -30,8 +30,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 @@ -49,10 +49,19 @@ * enable this if you need to call toDataUrl on the webgl context. * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when * rendering, stopping pixel interpolation. + * @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; + + if (this.legacy) + { + glCore.VertexArrayObject.FORCE_NATIVE = true; + } /** * The type of this renderer as a standardised const @@ -229,7 +238,7 @@ this.emit('context', gl); // setup the width/height properties and gl viewport - this.resize(this.width, this.height); + this.resize(this.screen.width, this.screen.height); } /** @@ -323,16 +332,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) { @@ -376,6 +385,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 @@ -440,9 +469,10 @@ * Changes the current shader to the one given in parameter * * @param {PIXI.Shader} shader - the new shader + * @param {boolean} [autoProject=true] - Whether automatically set the projection matrix * @return {PIXI.WebGLRenderer} Returns itself. */ - bindShader(shader) + bindShader(shader, autoProject) { // TODO cache if (this._activeShader !== shader) @@ -450,8 +480,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 bbab3b3..944f41d 100644 --- a/src/core/renderers/webgl/filters/Filter.js +++ b/src/core/renderers/webgl/filters/Filter.js @@ -1,6 +1,7 @@ import extractUniformsFromSrc from './extractUniformsFromSrc'; import { uid } from '../../../utils'; import { BLEND_MODES } from '../../../const'; +import settings from '../../../settings'; const SOURCE_KEY_MAP = {}; @@ -10,7 +11,7 @@ * @memberof PIXI * @extends PIXI.Shader */ -class Filter +export default class Filter { /** * @param {string} [vertexSrc] - The source of the vertex shader. @@ -35,8 +36,6 @@ this.blendMode = BLEND_MODES.NORMAL; - // pull out the vertex and shader uniforms if they are not specified.. - // currently this does not extract structs only default types this.uniformData = uniforms || extractUniformsFromSrc(this.vertexSrc, this.fragmentSrc, 'projectionMatrix|uSampler'); /** @@ -80,7 +79,7 @@ * * @member {number} */ - this.resolution = 1; + this.resolution = settings.RESOLUTION; /** * If enabled is true the filter is applied, if false it will not. @@ -97,8 +96,11 @@ * @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 { // --- // // this.uniforms.filterMatrix = filterManager.calculateSpriteMatrix(tempMatrix, window.panda ); @@ -170,5 +172,3 @@ ].join('\n'); } } - -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 dc7072a..6c7422d 100644 --- a/src/core/renderers/webgl/managers/FilterManager.js +++ b/src/core/renderers/webgl/managers/FilterManager.js @@ -154,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 @@ -176,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; @@ -184,7 +184,7 @@ flop = t; } - filters[i].apply(this, flip, lastState.renderTarget, true); + filters[i].apply(this, flip, lastState.renderTarget, false, currentState); this.freePotRenderTarget(flip); this.freePotRenderTarget(flop); @@ -291,9 +291,13 @@ let textureCount = 1; let currentState; - if (shader.uniforms.data.filterArea) + // 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; @@ -306,9 +310,9 @@ // use this to clamp displaced texture coords so they belong to filterArea // see displacementFilter fragment shader for an example - if (shader.uniforms.data.filterClamp) + if (shader.uniforms.filterClamp) { - currentState = this.filterData.stack[this.filterData.index]; + currentState = currentState || this.filterData.stack[this.filterData.index]; const filterClamp = shader.uniforms.filterClamp; @@ -485,7 +489,7 @@ */ destroy() { - this.shaderCache = []; + this.shaderCache = {}; this.emptyPool(); } diff --git a/src/core/renderers/webgl/utils/RenderTarget.js b/src/core/renderers/webgl/utils/RenderTarget.js index afe51ab..6d2467f 100644 --- a/src/core/renderers/webgl/utils/RenderTarget.js +++ b/src/core/renderers/webgl/utils/RenderTarget.js @@ -124,7 +124,7 @@ * @default PIXI.settings.SCALE_MODE * @see PIXI.SCALE_MODES */ - this.scaleMode = scaleMode || settings.SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; /** * Whether this object is the root element or not 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 36ea5bb..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; @@ -346,8 +346,8 @@ /** * Gets the local bounds of the sprite object. * - * @param {Rectangle} rect - The output rectangle. - * @return {Rectangle} The bounds. + * @param {PIXI.Rectangle} rect - The output rectangle. + * @return {PIXI.Rectangle} The bounds. */ getLocalBounds(rect) { @@ -440,7 +440,7 @@ * * @static * @param {number|string|PIXI.BaseTexture|HTMLCanvasElement|HTMLVideoElement} source Source to create texture from - * @return {PIXI.Texture} 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 e77b39a..90b08c0 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++) @@ -103,11 +102,18 @@ { const gl = this.renderer.gl; - // step 1: first check max textures the GPU can handle. - this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); + if (this.renderer.legacy) + { + this.MAX_TEXTURES = 1; + } + else + { + // step 1: first check max textures the GPU can handle. + this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); - // step 2: check the maximum number of if statements the shader can have too.. - this.MAX_TEXTURES = checkMaxIfStatmentsInShader(this.MAX_TEXTURES, gl); + // step 2: check the maximum number of if statements the shader can have too.. + this.MAX_TEXTURES = checkMaxIfStatmentsInShader(this.MAX_TEXTURES, gl); + } const shader = this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); @@ -130,8 +136,12 @@ .addIndex(this.indexBuffer) .addAttribute(this.vertexBuffers[i], shader.attributes.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) .addAttribute(this.vertexBuffers[i], shader.attributes.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) - .addAttribute(this.vertexBuffers[i], shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4) - .addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + .addAttribute(this.vertexBuffers[i], shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); + + if (shader.attributes.aTextureId) + { + this.vaos[i].addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + } /* eslint-enable max-len */ } @@ -378,8 +388,12 @@ .addIndex(this.indexBuffer) .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) - .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4) - .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); + + if (this.shader.attributes.aTextureId) + { + this.vaos[this.vertexCount].addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + } /* eslint-enable max-len */ } diff --git a/src/core/text/Text.js b/src/core/text/Text.js index 013625a..cec8c32 100644 --- a/src/core/text/Text.js +++ b/src/core/text/Text.js @@ -6,6 +6,7 @@ import { TEXT_GRADIENT } from '../const'; import settings from '../settings'; import TextStyle from './TextStyle'; +import trimCanvas from '../utils/trimCanvas'; const defaultDestroyOptions = { texture: true, @@ -57,7 +58,7 @@ /** * The canvas 2d context that everything is drawn with - * @member {HTMLCanvasElement} + * @member {CanvasRenderingContext2D} */ this.context = this.canvas.getContext('2d'); @@ -189,10 +190,12 @@ if (style.dropShadow) { + this.context.shadowBlur = style.dropShadowBlur; + this.context.globalAlpha = style.dropShadowAlpha; + if (style.dropShadowBlur > 0) { this.context.shadowColor = style.dropShadowColor; - this.context.shadowBlur = style.dropShadowBlur; } else { @@ -237,6 +240,10 @@ } } + // reset the shadow blur and alpha that was set by the drop shadow, for the regular text + this.context.shadowBlur = 0; + this.context.globalAlpha = 1; + // set canvas text styles this.context.fillStyle = this._generateFillStyle(style, lines); @@ -326,6 +333,15 @@ */ updateTexture() { + if (this._style.trim) + { + const trimmed = trimCanvas(this.canvas); + + this.canvas.width = trimmed.width; + this.canvas.height = trimmed.height; + this.context.putImageData(trimmed.data, 0, 0); + } + const texture = this._texture; const style = this._style; @@ -475,6 +491,19 @@ } /** + * Gets the local bounds of the text object. + * + * @param {Rectangle} rect - The output rectangle. + * @return {Rectangle} The bounds. + */ + getLocalBounds(rect) + { + this.updateText(true); + + return super.getLocalBounds.call(this, rect); + } + + /** * calculates the bounds of the Text as a rectangle. The bounds calculation takes the worldTransform into account. */ _calculateBounds() @@ -525,6 +554,29 @@ const width = this.canvas.width / this.resolution; const height = this.canvas.height / this.resolution; + // make a copy of the style settings, so we can manipulate them later + const fill = style.fill.slice(); + const fillGradientStops = style.fillGradientStops.slice(); + + // wanting to evenly distribute the fills. So an array of 4 colours should give fills of 0.25, 0.5 and 0.75 + if (!fillGradientStops.length) + { + const lengthPlus1 = fill.length + 1; + + for (let i = 1; i < lengthPlus1; ++i) + { + fillGradientStops.push(i / lengthPlus1); + } + } + + // stop the bleeding of the last gradient on the line above to the top gradient of the this line + // by hard defining the first gradient colour at point 0, and last gradient colour at point 1 + fill.unshift(style.fill[0]); + fillGradientStops.unshift(0); + + fill.push(style.fill[style.fill.length - 1]); + fillGradientStops.push(1); + if (style.fillGradientType === TEXT_GRADIENT.LINEAR_VERTICAL) { // start the gradient at the top center of the canvas, and end at the bottom middle of the canvas @@ -532,15 +584,22 @@ // we need to repeat the gradient so that each individual line of text has the same vertical gradient effect // ['#FF0000', '#00FF00', '#0000FF'] over 2 lines would create stops at 0.125, 0.25, 0.375, 0.625, 0.75, 0.875 - totalIterations = (style.fill.length + 1) * lines.length; + totalIterations = (fill.length + 1) * lines.length; currentIteration = 0; for (let i = 0; i < lines.length; i++) { currentIteration += 1; - for (let j = 0; j < style.fill.length; j++) + for (let j = 0; j < fill.length; j++) { - stop = (currentIteration / totalIterations); - gradient.addColorStop(stop, style.fill[j]); + if (fillGradientStops[j]) + { + stop = (fillGradientStops[j] / lines.length) + (i / lines.length); + } + else + { + stop = currentIteration / totalIterations; + } + gradient.addColorStop(stop, fill[j]); currentIteration++; } } @@ -552,13 +611,20 @@ // can just evenly space out the gradients in this case, as multiple lines makes no difference // to an even left to right gradient - totalIterations = style.fill.length + 1; + totalIterations = fill.length + 1; currentIteration = 1; - for (let i = 0; i < style.fill.length; i++) + for (let i = 0; i < fill.length; i++) { - stop = currentIteration / totalIterations; - gradient.addColorStop(stop, style.fill[i]); + if (fillGradientStops[i]) + { + stop = fillGradientStops[i]; + } + else + { + stop = currentIteration / totalIterations; + } + gradient.addColorStop(stop, fill[i]); currentIteration++; } } @@ -569,7 +635,7 @@ /** * Destroys this text object. * Note* Unlike a Sprite, a Text object will automatically destroy its baseTexture and texture as - * the majorety of the time the texture will not be shared with any other Sprites. + * the majority of the time the texture will not be shared with any other Sprites. * * @param {object|boolean} [options] - Options parameter. A boolean will act as if all options * have been set to that value @@ -680,7 +746,7 @@ set text(text) // eslint-disable-line require-jsdoc { - text = String(text || ' '); + text = String(text === '' || text === null || text === undefined ? ' ' : text); if (this._text === text) { diff --git a/src/core/text/TextStyle.js b/src/core/text/TextStyle.js index 3b25528..890950b 100644 --- a/src/core/text/TextStyle.js +++ b/src/core/text/TextStyle.js @@ -8,12 +8,14 @@ align: 'left', breakWords: false, dropShadow: false, + dropShadowAlpha: 1, dropShadowAngle: Math.PI / 6, dropShadowBlur: 0, dropShadowColor: '#000000', dropShadowDistance: 5, fill: 'black', fillGradientType: TEXT_GRADIENT.LINEAR_VERTICAL, + fillGradientStops: [], fontFamily: 'Arial', fontSize: 26, fontStyle: 'normal', @@ -27,6 +29,7 @@ stroke: 'black', strokeThickness: 0, textBaseline: 'alphabetic', + trim: false, wordWrap: false, wordWrapWidth: 100, }; @@ -47,6 +50,7 @@ * @param {boolean} [style.breakWords=false] - Indicates if lines can be wrapped within words, it * needs wordWrap to be set to true * @param {boolean} [style.dropShadow=false] - Set a drop shadow for the text + * @param {number} [style.dropShadowAlpha=1] - Set alpha for the drop shadow * @param {number} [style.dropShadowAngle=Math.PI/6] - Set a angle of the drop shadow * @param {number} [style.dropShadowBlur=0] - Set a shadow blur radius * @param {string} [style.dropShadowColor='#000000'] - A fill style to be used on the dropshadow e.g 'red', '#00FF00' @@ -55,8 +59,10 @@ * fillstyle that will be used on the text e.g 'red', '#00FF00'. Can be an array to create a gradient * eg ['#000000','#FFFFFF'] * {@link https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/fillStyle|MDN} - * @param {number} [style.fillGradientType=PIXI.TEXT_GRADIENT.LINEAR_VERTICAL] - If fills styles are - * supplied, this can change the type/direction of the gradient. See {@link PIXI.TEXT_GRADIENT} for possible values + * @param {number} [style.fillGradientType=PIXI.TEXT_GRADIENT.LINEAR_VERTICAL] - If fill is an array of colours + * to create a gradient, this can change the type/direction of the gradient. See {@link PIXI.TEXT_GRADIENT} + * @param {number[]} [style.fillGradientStops] - If fill is an array of colours to create a gradient, this array can set + * the stop points (numbers between 0 and 1) for the color, overriding the default behaviour of evenly spacing them. * @param {string|string[]} [style.fontFamily='Arial'] - The font family * @param {number|string} [style.fontSize=26] - The font size (as a number it converts to px, but as a string, * equivalents are '26px','20pt','160%' or '1.6em') @@ -76,6 +82,7 @@ * e.g 'blue', '#FCFF00' * @param {number} [style.strokeThickness=0] - A number that represents the thickness of the stroke. * Default is 0 (no stroke) + * @param {boolean} [style.trim=false] - Trim transparent borders * @param {string} [style.textBaseline='alphabetic'] - The baseline of the text that is rendered. * @param {boolean} [style.wordWrap=false] - Indicates if word wrap should be used * @param {number} [style.wordWrapWidth=100] - The width at which text will wrap, it needs wordWrap to be set to true @@ -152,6 +159,19 @@ } } + get dropShadowAlpha() + { + return this._dropShadowAlpha; + } + set dropShadowAlpha(dropShadowAlpha) + { + if (this._dropShadowAlpha !== dropShadowAlpha) + { + this._dropShadowAlpha = dropShadowAlpha; + this.styleID++; + } + } + get dropShadowAngle() { return this._dropShadowAngle; @@ -232,6 +252,19 @@ } } + get fillGradientStops() + { + return this._fillGradientStops; + } + set fillGradientStops(fillGradientStops) + { + if (!areArraysEqual(this._fillGradientStops,fillGradientStops)) + { + this._fillGradientStops = fillGradientStops; + this.styleID++; + } + } + get fontFamily() { return this._fontFamily; @@ -402,6 +435,19 @@ } } + get trim() + { + return this._trim; + } + set trim(trim) + { + if (this._trim !== trim) + { + this._trim = trim; + this.styleID++; + } + } + get wordWrap() { return this._wordWrap; @@ -475,3 +521,34 @@ return color; } } + +/** + * Utility function to convert hexadecimal colors to strings, and simply return the color if it's a string. + * This version can also convert array of colors + * + * @param {Array} array1 First array to compare + * @param {Array} array2 Second array to compare + * @return {boolean} Do the arrays contain the same values in the same order + */ +function areArraysEqual(array1, array2) +{ + if (!Array.isArray(array1) || !Array.isArray(array2)) + { + return false; + } + + if (array1.length !== array2.length) + { + return false; + } + + for (let i = 0; i < array1.length; ++i) + { + if (array1[i] !== array2[i]) + { + return false; + } + } + + return true; +} diff --git a/src/core/textures/BaseRenderTexture.js b/src/core/textures/BaseRenderTexture.js index 5d50146..aa727f0 100644 --- a/src/core/textures/BaseRenderTexture.js +++ b/src/core/textures/BaseRenderTexture.js @@ -1,8 +1,6 @@ import BaseTexture from './BaseTexture'; import settings from '../settings'; -const { RESOLUTION, SCALE_MODE } = settings; - /** * A BaseRenderTexture is a special texture that allows any Pixi display object to be rendered to it. * @@ -54,7 +52,7 @@ { super(null, scaleMode); - this.resolution = resolution || RESOLUTION; + this.resolution = resolution || settings.RESOLUTION; this.width = width; this.height = height; @@ -62,7 +60,7 @@ this.realWidth = this.width * this.resolution; this.realHeight = this.height * this.resolution; - this.scaleMode = scaleMode || SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; this.hasLoaded = true; /** diff --git a/src/core/textures/BaseTexture.js b/src/core/textures/BaseTexture.js index 676dca3..aeccf39 100644 --- a/src/core/textures/BaseTexture.js +++ b/src/core/textures/BaseTexture.js @@ -77,7 +77,7 @@ * @default PIXI.settings.SCALE_MODE * @see PIXI.SCALE_MODES */ - this.scaleMode = scaleMode || settings.SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; /** * Set to true once the base texture has successfully loaded. @@ -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/RenderTexture.js b/src/core/textures/RenderTexture.js index 4c5034f..b3c6297 100644 --- a/src/core/textures/RenderTexture.js +++ b/src/core/textures/RenderTexture.js @@ -54,8 +54,8 @@ /* eslint-disable prefer-rest-params, no-console */ const width = arguments[1]; const height = arguments[2]; - const scaleMode = arguments[3] || 0; - const resolution = arguments[4] || 1; + const scaleMode = arguments[3]; + const resolution = arguments[4]; // we have an old render texture.. console.warn(`Please use RenderTexture.create(${width}, ${height}) instead of the ctor directly.`); 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 ca31456..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 @@ -459,7 +497,9 @@ if (frame.x + frame.width > this.baseTexture.width || frame.y + frame.height > this.baseTexture.height) { - throw new Error(`Texture Error: frame does not fit inside the base Texture dimensions ${this}`); + throw new Error('Texture Error: frame does not fit inside the base Texture dimensions: ' + + `X: ${frame.x} + ${frame.width} > ${this.baseTexture.width} ` + + `Y: ${frame.y} + ${frame.height} > ${this.baseTexture.height}`); } // this.valid = frame && frame.width && frame.height && this.baseTexture.source && this.baseTexture.hasLoaded; @@ -520,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. @@ -528,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 2a7497b..99e585a 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)); } /** @@ -360,3 +362,43 @@ * @private */ export const BaseTextureCache = {}; + +/** + * 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/core/utils/trimCanvas.js b/src/core/utils/trimCanvas.js new file mode 100644 index 0000000..5af6de0 --- /dev/null +++ b/src/core/utils/trimCanvas.js @@ -0,0 +1,83 @@ +/** + * Trim transparent borders from a canvas + * + * @memberof PIXI + * @function trimCanvas + * @private + * @param {HTMLCanvasElement} canvas - the canvas to trim + * @returns {object} Trim data + */ +export default function trimCanvas(canvas) +{ + // https://gist.github.com/remy/784508 + + let width = canvas.width; + let height = canvas.height; + + const context = canvas.getContext('2d'); + const imageData = context.getImageData(0, 0, width, height); + const pixels = imageData.data; + const len = pixels.length; + + const bound = { + top: null, + left: null, + right: null, + bottom: null, + }; + let i; + let x; + let y; + + for (i = 0; i < len; i += 4) + { + if (pixels[i + 3] !== 0) + { + x = (i / 4) % width; + y = ~~((i / 4) / width); + + if (bound.top === null) + { + bound.top = y; + } + + if (bound.left === null) + { + bound.left = x; + } + else if (x < bound.left) + { + bound.left = x; + } + + if (bound.right === null) + { + bound.right = x + 1; + } + else if (bound.right < x) + { + bound.right = x + 1; + } + + if (bound.bottom === null) + { + bound.bottom = y; + } + else if (bound.bottom < y) + { + bound.bottom = y; + } + } + } + + width = bound.right - bound.left; + height = bound.bottom - bound.top + 1; + + const data = context.getImageData(bound.left, bound.top, width, height); + + return { + height, + width, + data, + }; +} diff --git a/src/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/extras/TilingSprite.js b/src/extras/TilingSprite.js index f90d248..fe82451 100644 --- a/src/extras/TilingSprite.js +++ b/src/extras/TilingSprite.js @@ -220,20 +220,33 @@ transform.tx * resolution, transform.ty * resolution); - // TODO - this should be rolled into the setTransform above.. - context.scale(this.tileScale.x / baseTextureResolution, this.tileScale.y / baseTextureResolution); - - context.translate(modX + (this.anchor.x * -this._width), - modY + (this.anchor.y * -this._height)); - renderer.setBlendMode(this.blendMode); // fill the pattern! context.fillStyle = this._canvasPattern; - context.fillRect(-modX, - -modY, - this._width / this.tileScale.x * baseTextureResolution, - this._height / this.tileScale.y * baseTextureResolution); + + // TODO - this should be rolled into the setTransform above.. + context.scale(this.tileScale.x / baseTextureResolution, this.tileScale.y / baseTextureResolution); + + const anchorX = this.anchor.x * -this._width; + const anchorY = this.anchor.y * -this._height; + + if (this.uvRespectAnchor) + { + context.translate(modX, modY); + + context.fillRect(-modX + anchorX, -modY + anchorY, + this._width / this.tileScale.x * baseTextureResolution, + this._height / this.tileScale.y * baseTextureResolution); + } + else + { + context.translate(modX + anchorX, modY + anchorY); + + context.fillRect(-modX, -modY, + this._width / this.tileScale.x * baseTextureResolution, + this._height / this.tileScale.y * baseTextureResolution); + } } /** diff --git a/src/extras/cacheAsBitmap.js b/src/extras/cacheAsBitmap.js index 18043a7..5dd5640 100644 --- a/src/extras/cacheAsBitmap.js +++ b/src/extras/cacheAsBitmap.js @@ -41,6 +41,9 @@ * provide a performance benefit for complex static displayObjects. * To remove simply set this property to 'false' * + * IMPORTANT GOTCHA - make sure that all your textures are preloaded BEFORE setting this property to true + * as it will take a snapshot of what is currently there. If the textures have not loaded then they will not appear. + * * @member {boolean} * @memberof PIXI.DisplayObject# */ diff --git a/src/filters/blur/BlurFilter.js b/src/filters/blur/BlurFilter.js index 340fc58..1575db9 100644 --- a/src/filters/blur/BlurFilter.js +++ b/src/filters/blur/BlurFilter.js @@ -24,10 +24,9 @@ this.blurXFilter = new BlurXFilter(strength, quality, resolution, kernelSize); this.blurYFilter = new BlurYFilter(strength, quality, resolution, kernelSize); - this.resolution = 1; this.padding = 0; - this.resolution = resolution || 1; + this.resolution = resolution || core.settings.RESOLUTION; this.quality = quality || 4; this.blur = strength || 8; } diff --git a/src/filters/blur/BlurXFilter.js b/src/filters/blur/BlurXFilter.js index def99c3..0f48889 100644 --- a/src/filters/blur/BlurXFilter.js +++ b/src/filters/blur/BlurXFilter.js @@ -31,7 +31,7 @@ fragSrc ); - this.resolution = resolution || 1; + this.resolution = resolution || core.settings.RESOLUTION; this._quality = 0; diff --git a/src/filters/blur/BlurYFilter.js b/src/filters/blur/BlurYFilter.js index 638e0f0..6d489c7 100644 --- a/src/filters/blur/BlurYFilter.js +++ b/src/filters/blur/BlurYFilter.js @@ -31,7 +31,7 @@ fragSrc ); - this.resolution = resolution || 1; + this.resolution = resolution || core.settings.RESOLUTION; this._quality = 0; diff --git a/src/filters/colormatrix/ColorMatrixFilter.js b/src/filters/colormatrix/ColorMatrixFilter.js index 8509505..679f8ec 100644 --- a/src/filters/colormatrix/ColorMatrixFilter.js +++ b/src/filters/colormatrix/ColorMatrixFilter.js @@ -75,28 +75,28 @@ out[1] = (a[0] * b[1]) + (a[1] * b[6]) + (a[2] * b[11]) + (a[3] * b[16]); out[2] = (a[0] * b[2]) + (a[1] * b[7]) + (a[2] * b[12]) + (a[3] * b[17]); out[3] = (a[0] * b[3]) + (a[1] * b[8]) + (a[2] * b[13]) + (a[3] * b[18]); - out[4] = (a[0] * b[4]) + (a[1] * b[9]) + (a[2] * b[14]) + (a[3] * b[19]); + out[4] = (a[0] * b[4]) + (a[1] * b[9]) + (a[2] * b[14]) + (a[3] * b[19]) + a[4]; // Green Channel out[5] = (a[5] * b[0]) + (a[6] * b[5]) + (a[7] * b[10]) + (a[8] * b[15]); out[6] = (a[5] * b[1]) + (a[6] * b[6]) + (a[7] * b[11]) + (a[8] * b[16]); out[7] = (a[5] * b[2]) + (a[6] * b[7]) + (a[7] * b[12]) + (a[8] * b[17]); out[8] = (a[5] * b[3]) + (a[6] * b[8]) + (a[7] * b[13]) + (a[8] * b[18]); - out[9] = (a[5] * b[4]) + (a[6] * b[9]) + (a[7] * b[14]) + (a[8] * b[19]); + out[9] = (a[5] * b[4]) + (a[6] * b[9]) + (a[7] * b[14]) + (a[8] * b[19]) + a[9]; // Blue Channel out[10] = (a[10] * b[0]) + (a[11] * b[5]) + (a[12] * b[10]) + (a[13] * b[15]); out[11] = (a[10] * b[1]) + (a[11] * b[6]) + (a[12] * b[11]) + (a[13] * b[16]); out[12] = (a[10] * b[2]) + (a[11] * b[7]) + (a[12] * b[12]) + (a[13] * b[17]); out[13] = (a[10] * b[3]) + (a[11] * b[8]) + (a[12] * b[13]) + (a[13] * b[18]); - out[14] = (a[10] * b[4]) + (a[11] * b[9]) + (a[12] * b[14]) + (a[13] * b[19]); + out[14] = (a[10] * b[4]) + (a[11] * b[9]) + (a[12] * b[14]) + (a[13] * b[19]) + a[14]; // Alpha Channel out[15] = (a[15] * b[0]) + (a[16] * b[5]) + (a[17] * b[10]) + (a[18] * b[15]); out[16] = (a[15] * b[1]) + (a[16] * b[6]) + (a[17] * b[11]) + (a[18] * b[16]); out[17] = (a[15] * b[2]) + (a[16] * b[7]) + (a[17] * b[12]) + (a[18] * b[17]); out[18] = (a[15] * b[3]) + (a[16] * b[8]) + (a[17] * b[13]) + (a[18] * b[18]); - out[19] = (a[15] * b[4]) + (a[16] * b[9]) + (a[17] * b[14]) + (a[18] * b[19]); + out[19] = (a[15] * b[4]) + (a[16] * b[9]) + (a[17] * b[14]) + (a[18] * b[19]) + a[19]; return out; } diff --git a/src/filters/colormatrix/colorMatrix.frag b/src/filters/colormatrix/colorMatrix.frag index 3aaf2b3..c73c0e9 100644 --- a/src/filters/colormatrix/colorMatrix.frag +++ b/src/filters/colormatrix/colorMatrix.frag @@ -4,32 +4,38 @@ void main(void) { - vec4 c = texture2D(uSampler, vTextureCoord); + // Un-premultiply alpha before applying the color matrix. See issue #3539. + if (c.a > 0.0) { + c.rgb /= c.a; + } + vec4 result; + result.r = (m[0] * c.r); + result.r += (m[1] * c.g); + result.r += (m[2] * c.b); + result.r += (m[3] * c.a); + result.r += m[4]; - gl_FragColor.r = (m[0] * c.r); - gl_FragColor.r += (m[1] * c.g); - gl_FragColor.r += (m[2] * c.b); - gl_FragColor.r += (m[3] * c.a); - gl_FragColor.r += m[4] * c.a; + result.g = (m[5] * c.r); + result.g += (m[6] * c.g); + result.g += (m[7] * c.b); + result.g += (m[8] * c.a); + result.g += m[9]; - gl_FragColor.g = (m[5] * c.r); - gl_FragColor.g += (m[6] * c.g); - gl_FragColor.g += (m[7] * c.b); - gl_FragColor.g += (m[8] * c.a); - gl_FragColor.g += m[9] * c.a; + result.b = (m[10] * c.r); + result.b += (m[11] * c.g); + result.b += (m[12] * c.b); + result.b += (m[13] * c.a); + result.b += m[14]; - gl_FragColor.b = (m[10] * c.r); - gl_FragColor.b += (m[11] * c.g); - gl_FragColor.b += (m[12] * c.b); - gl_FragColor.b += (m[13] * c.a); - gl_FragColor.b += m[14] * c.a; + result.a = (m[15] * c.r); + result.a += (m[16] * c.g); + result.a += (m[17] * c.b); + result.a += (m[18] * c.a); + result.a += m[19]; - gl_FragColor.a = (m[15] * c.r); - gl_FragColor.a += (m[16] * c.g); - gl_FragColor.a += (m[17] * c.b); - gl_FragColor.a += (m[18] * c.a); - gl_FragColor.a += m[19] * c.a; + // Premultiply alpha again. + result.rgb *= result.a; -// gl_FragColor = vec4(m[0]); + gl_FragColor = result; } diff --git a/src/filters/displacement/DisplacementFilter.js b/src/filters/displacement/DisplacementFilter.js index 112de0b..d5355c3 100644 --- a/src/filters/displacement/DisplacementFilter.js +++ b/src/filters/displacement/DisplacementFilter.js @@ -36,7 +36,7 @@ this.maskMatrix = maskMatrix; this.uniforms.mapSampler = sprite.texture; - this.uniforms.filterMatrix = maskMatrix.toArray(true); + this.uniforms.filterMatrix = maskMatrix; this.uniforms.scale = { x: 1, y: 1 }; if (scale === null || scale === undefined) diff --git a/src/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/InteractionData.js b/src/interaction/InteractionData.js index 02d53a3..818da35 100644 --- a/src/interaction/InteractionData.js +++ b/src/interaction/InteractionData.js @@ -21,9 +21,9 @@ this.global = new core.Point(); /** - * The target Sprite that was interacted with + * The target DisplayObject that was interacted with * - * @member {PIXI.Sprite} + * @member {PIXI.DisplayObject} */ this.target = null; @@ -33,6 +33,13 @@ * @member {Event} */ this.originalEvent = null; + + /** + * Unique identifier for this interaction + * + * @member {number} + */ + this.identifier = null; } /** diff --git a/src/interaction/InteractionEvent.js b/src/interaction/InteractionEvent.js index 75b2966..dc554f9 100644 --- a/src/interaction/InteractionEvent.js +++ b/src/interaction/InteractionEvent.js @@ -58,7 +58,7 @@ } /** - * Prevents event from reaching any objects other than the current object. + * Resets the event. * * @private */ diff --git a/src/interaction/InteractionManager.js b/src/interaction/InteractionManager.js index 7d51134..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 1271fef..71ade15 100644 --- a/package.json +++ b/package.json @@ -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/scripts/release.js b/scripts/release.js index c4c8e36..156620c 100644 --- a/scripts/release.js +++ b/scripts/release.js @@ -12,7 +12,8 @@ 'dist/**/*', 'lib/**/*', 'src/**/*', - 'scripts/**/*', + 'scripts/*', + 'scripts/renders/*', 'test/**/*', '*.json', '*.md', diff --git a/scripts/renders/client.js b/scripts/renders/client.js index 16c9565..6faee34 100755 --- a/scripts/renders/client.js +++ b/scripts/renders/client.js @@ -1,6 +1,6 @@ 'use strict'; -require('../../bin/pixi'); +require('../../dist/pixi'); PIXI.utils.skipHello(); const fs = require('fs'); 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 bc730db..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. @@ -33,8 +33,11 @@ * need to call toDataUrl on the webgl context * @param {number} [options.resolution=1] - The resolution / device pixel ratio of the renderer, retina would be 2 * @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 @@ -43,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. */ @@ -73,7 +99,7 @@ */ stop() { - this.ticker.stop(); + this._ticker.stop(); } /** @@ -81,7 +107,7 @@ */ start() { - this.ticker.start(); + this._ticker.start(); } /** @@ -95,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 index 81e076b..78b17e6 100644 --- a/src/core/Shader.js +++ b/src/core/Shader.js @@ -1,9 +1,7 @@ import { GLShader } from 'pixi-gl-core'; import settings from './settings'; -const { PRECISION } = settings; - -function checkPrecision(src) +function checkPrecision(src, def) { if (src instanceof Array) { @@ -11,14 +9,14 @@ { const copy = src.slice(0); - copy.unshift(`precision ${PRECISION} float;`); + copy.unshift(`precision ${def} float;`); return copy; } } else if (src.substring(0, 9) !== 'precision') { - return `precision ${PRECISION} float;\n${src}`; + return `precision ${def} float;\n${src}`; } return src; @@ -42,6 +40,7 @@ */ constructor(gl, vertexSrc, fragmentSrc) { - super(gl, checkPrecision(vertexSrc), checkPrecision(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 3425b41..f9235e4 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -181,7 +181,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 b3792ef..17fde8d 100644 --- a/src/core/display/Container.js +++ b/src/core/display/Container.js @@ -73,13 +73,14 @@ } child.parent = this; - - // ensure a transform will be recalculated.. - this.transform._parentID = -1; - this._boundsID++; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.push(child); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(this.children.length - 1); child.emit('added', this); @@ -108,9 +109,14 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.splice(index, 0, child); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(index); child.emit('added', this); @@ -174,6 +180,7 @@ removeItems(this.children, currentIndex, 1); // remove from old position this.children.splice(index, 0, child); // add at new position + this.onChildrenChange(index); } @@ -220,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! @@ -244,9 +252,14 @@ { const child = this.getChildAt(index); + // ensure child transform will be recalculated.. child.parent = null; + child.transform._parentID = -1; removeItems(this.children, index, 1); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(index); child.emit('removed', this); @@ -275,8 +288,14 @@ for (let i = 0; i < removed.length; ++i) { removed[i].parent = null; + if (removed[i].transform) + { + removed[i].transform._parentID = -1; + } } + this._boundsID++; + this.onChildrenChange(beginIndex); for (let i = 0; i < removed.length; ++i) diff --git a/src/core/display/DisplayObject.js b/src/core/display/DisplayObject.js index 3affe36..4e6c77d 100644 --- a/src/core/display/DisplayObject.js +++ b/src/core/display/DisplayObject.js @@ -109,7 +109,7 @@ /** * The original, cached mask of the object * - * @member {PIXI.Rectangle} + * @member {PIXI.Graphics|PIXI.Sprite} * @private */ this._mask = null; diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index 95f2af4..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 ); @@ -1065,10 +1061,15 @@ canvasRenderer = new CanvasRenderer(); } - tempMatrix.tx = -bounds.x; - tempMatrix.ty = -bounds.y; + this.transform.updateLocalTransform(); + this.transform.localTransform.copy(tempMatrix); - canvasRenderer.render(this, canvasBuffer, false, tempMatrix); + tempMatrix.invert(); + + tempMatrix.tx -= bounds.x; + tempMatrix.ty -= bounds.y; + + canvasRenderer.render(this, canvasBuffer, true, tempMatrix); const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode); 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 e67680f..867dde2 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/buildCircle.js b/src/core/graphics/webgl/utils/buildCircle.js index 0400d13..abf5d02 100644 --- a/src/core/graphics/webgl/utils/buildCircle.js +++ b/src/core/graphics/webgl/utils/buildCircle.js @@ -33,6 +33,11 @@ height = circleData.height; } + if (width === 0 || height === 0) + { + return; + } + const totalSegs = Math.floor(30 * Math.sqrt(circleData.radius)) || Math.floor(15 * Math.sqrt(circleData.width + circleData.height)); diff --git a/src/core/graphics/webgl/utils/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 90155f9..f03fe15 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -30,6 +30,7 @@ export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; export { default as CanvasGraphicsRenderer } from './graphics/canvas/CanvasGraphicsRenderer'; +export { default as Spritesheet } from './textures/Spritesheet'; export { default as Texture } from './textures/Texture'; export { default as BaseTexture } from './textures/BaseTexture'; export { default as RenderTexture } from './textures/RenderTexture'; diff --git a/src/core/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 ccce680..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); } /** @@ -162,6 +162,9 @@ if (transform) { transform.copy(tempWt); + + // lets not forget to flag the parent transform as dirty... + this._tempDisplayObjectParent.transform._worldID = -1; } else { @@ -169,6 +172,7 @@ } displayObject.parent = this._tempDisplayObjectParent; + displayObject.updateTransform(); displayObject.parent = cacheParent; // displayObject.hitArea = //TODO add a temp hit area @@ -279,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/canvas/utils/CanvasRenderTarget.js b/src/core/renderers/canvas/utils/CanvasRenderTarget.js index 3de0aef..b409891 100644 --- a/src/core/renderers/canvas/utils/CanvasRenderTarget.js +++ b/src/core/renderers/canvas/utils/CanvasRenderTarget.js @@ -1,5 +1,4 @@ import settings from '../../../settings'; -const { RESOLUTION } = settings; /** * Creates a Canvas element of the given size. @@ -30,7 +29,7 @@ */ this.context = this.canvas.getContext('2d'); - this.resolution = resolution || RESOLUTION; + this.resolution = resolution || settings.RESOLUTION; this.resize(width, height); } 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 ce0e1c4..788ed6c 100644 --- a/src/core/renderers/webgl/WebGLRenderer.js +++ b/src/core/renderers/webgl/WebGLRenderer.js @@ -30,8 +30,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 @@ -49,10 +49,19 @@ * enable this if you need to call toDataUrl on the webgl context. * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when * rendering, stopping pixel interpolation. + * @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; + + if (this.legacy) + { + glCore.VertexArrayObject.FORCE_NATIVE = true; + } /** * The type of this renderer as a standardised const @@ -229,7 +238,7 @@ this.emit('context', gl); // setup the width/height properties and gl viewport - this.resize(this.width, this.height); + this.resize(this.screen.width, this.screen.height); } /** @@ -323,16 +332,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) { @@ -376,6 +385,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 @@ -440,9 +469,10 @@ * Changes the current shader to the one given in parameter * * @param {PIXI.Shader} shader - the new shader + * @param {boolean} [autoProject=true] - Whether automatically set the projection matrix * @return {PIXI.WebGLRenderer} Returns itself. */ - bindShader(shader) + bindShader(shader, autoProject) { // TODO cache if (this._activeShader !== shader) @@ -450,8 +480,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 bbab3b3..944f41d 100644 --- a/src/core/renderers/webgl/filters/Filter.js +++ b/src/core/renderers/webgl/filters/Filter.js @@ -1,6 +1,7 @@ import extractUniformsFromSrc from './extractUniformsFromSrc'; import { uid } from '../../../utils'; import { BLEND_MODES } from '../../../const'; +import settings from '../../../settings'; const SOURCE_KEY_MAP = {}; @@ -10,7 +11,7 @@ * @memberof PIXI * @extends PIXI.Shader */ -class Filter +export default class Filter { /** * @param {string} [vertexSrc] - The source of the vertex shader. @@ -35,8 +36,6 @@ this.blendMode = BLEND_MODES.NORMAL; - // pull out the vertex and shader uniforms if they are not specified.. - // currently this does not extract structs only default types this.uniformData = uniforms || extractUniformsFromSrc(this.vertexSrc, this.fragmentSrc, 'projectionMatrix|uSampler'); /** @@ -80,7 +79,7 @@ * * @member {number} */ - this.resolution = 1; + this.resolution = settings.RESOLUTION; /** * If enabled is true the filter is applied, if false it will not. @@ -97,8 +96,11 @@ * @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 { // --- // // this.uniforms.filterMatrix = filterManager.calculateSpriteMatrix(tempMatrix, window.panda ); @@ -170,5 +172,3 @@ ].join('\n'); } } - -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 dc7072a..6c7422d 100644 --- a/src/core/renderers/webgl/managers/FilterManager.js +++ b/src/core/renderers/webgl/managers/FilterManager.js @@ -154,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 @@ -176,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; @@ -184,7 +184,7 @@ flop = t; } - filters[i].apply(this, flip, lastState.renderTarget, true); + filters[i].apply(this, flip, lastState.renderTarget, false, currentState); this.freePotRenderTarget(flip); this.freePotRenderTarget(flop); @@ -291,9 +291,13 @@ let textureCount = 1; let currentState; - if (shader.uniforms.data.filterArea) + // 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; @@ -306,9 +310,9 @@ // use this to clamp displaced texture coords so they belong to filterArea // see displacementFilter fragment shader for an example - if (shader.uniforms.data.filterClamp) + if (shader.uniforms.filterClamp) { - currentState = this.filterData.stack[this.filterData.index]; + currentState = currentState || this.filterData.stack[this.filterData.index]; const filterClamp = shader.uniforms.filterClamp; @@ -485,7 +489,7 @@ */ destroy() { - this.shaderCache = []; + this.shaderCache = {}; this.emptyPool(); } diff --git a/src/core/renderers/webgl/utils/RenderTarget.js b/src/core/renderers/webgl/utils/RenderTarget.js index afe51ab..6d2467f 100644 --- a/src/core/renderers/webgl/utils/RenderTarget.js +++ b/src/core/renderers/webgl/utils/RenderTarget.js @@ -124,7 +124,7 @@ * @default PIXI.settings.SCALE_MODE * @see PIXI.SCALE_MODES */ - this.scaleMode = scaleMode || settings.SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; /** * Whether this object is the root element or not 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 36ea5bb..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; @@ -346,8 +346,8 @@ /** * Gets the local bounds of the sprite object. * - * @param {Rectangle} rect - The output rectangle. - * @return {Rectangle} The bounds. + * @param {PIXI.Rectangle} rect - The output rectangle. + * @return {PIXI.Rectangle} The bounds. */ getLocalBounds(rect) { @@ -440,7 +440,7 @@ * * @static * @param {number|string|PIXI.BaseTexture|HTMLCanvasElement|HTMLVideoElement} source Source to create texture from - * @return {PIXI.Texture} 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 e77b39a..90b08c0 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++) @@ -103,11 +102,18 @@ { const gl = this.renderer.gl; - // step 1: first check max textures the GPU can handle. - this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); + if (this.renderer.legacy) + { + this.MAX_TEXTURES = 1; + } + else + { + // step 1: first check max textures the GPU can handle. + this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); - // step 2: check the maximum number of if statements the shader can have too.. - this.MAX_TEXTURES = checkMaxIfStatmentsInShader(this.MAX_TEXTURES, gl); + // step 2: check the maximum number of if statements the shader can have too.. + this.MAX_TEXTURES = checkMaxIfStatmentsInShader(this.MAX_TEXTURES, gl); + } const shader = this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); @@ -130,8 +136,12 @@ .addIndex(this.indexBuffer) .addAttribute(this.vertexBuffers[i], shader.attributes.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) .addAttribute(this.vertexBuffers[i], shader.attributes.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) - .addAttribute(this.vertexBuffers[i], shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4) - .addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + .addAttribute(this.vertexBuffers[i], shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); + + if (shader.attributes.aTextureId) + { + this.vaos[i].addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + } /* eslint-enable max-len */ } @@ -378,8 +388,12 @@ .addIndex(this.indexBuffer) .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) - .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4) - .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); + + if (this.shader.attributes.aTextureId) + { + this.vaos[this.vertexCount].addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + } /* eslint-enable max-len */ } diff --git a/src/core/text/Text.js b/src/core/text/Text.js index 013625a..cec8c32 100644 --- a/src/core/text/Text.js +++ b/src/core/text/Text.js @@ -6,6 +6,7 @@ import { TEXT_GRADIENT } from '../const'; import settings from '../settings'; import TextStyle from './TextStyle'; +import trimCanvas from '../utils/trimCanvas'; const defaultDestroyOptions = { texture: true, @@ -57,7 +58,7 @@ /** * The canvas 2d context that everything is drawn with - * @member {HTMLCanvasElement} + * @member {CanvasRenderingContext2D} */ this.context = this.canvas.getContext('2d'); @@ -189,10 +190,12 @@ if (style.dropShadow) { + this.context.shadowBlur = style.dropShadowBlur; + this.context.globalAlpha = style.dropShadowAlpha; + if (style.dropShadowBlur > 0) { this.context.shadowColor = style.dropShadowColor; - this.context.shadowBlur = style.dropShadowBlur; } else { @@ -237,6 +240,10 @@ } } + // reset the shadow blur and alpha that was set by the drop shadow, for the regular text + this.context.shadowBlur = 0; + this.context.globalAlpha = 1; + // set canvas text styles this.context.fillStyle = this._generateFillStyle(style, lines); @@ -326,6 +333,15 @@ */ updateTexture() { + if (this._style.trim) + { + const trimmed = trimCanvas(this.canvas); + + this.canvas.width = trimmed.width; + this.canvas.height = trimmed.height; + this.context.putImageData(trimmed.data, 0, 0); + } + const texture = this._texture; const style = this._style; @@ -475,6 +491,19 @@ } /** + * Gets the local bounds of the text object. + * + * @param {Rectangle} rect - The output rectangle. + * @return {Rectangle} The bounds. + */ + getLocalBounds(rect) + { + this.updateText(true); + + return super.getLocalBounds.call(this, rect); + } + + /** * calculates the bounds of the Text as a rectangle. The bounds calculation takes the worldTransform into account. */ _calculateBounds() @@ -525,6 +554,29 @@ const width = this.canvas.width / this.resolution; const height = this.canvas.height / this.resolution; + // make a copy of the style settings, so we can manipulate them later + const fill = style.fill.slice(); + const fillGradientStops = style.fillGradientStops.slice(); + + // wanting to evenly distribute the fills. So an array of 4 colours should give fills of 0.25, 0.5 and 0.75 + if (!fillGradientStops.length) + { + const lengthPlus1 = fill.length + 1; + + for (let i = 1; i < lengthPlus1; ++i) + { + fillGradientStops.push(i / lengthPlus1); + } + } + + // stop the bleeding of the last gradient on the line above to the top gradient of the this line + // by hard defining the first gradient colour at point 0, and last gradient colour at point 1 + fill.unshift(style.fill[0]); + fillGradientStops.unshift(0); + + fill.push(style.fill[style.fill.length - 1]); + fillGradientStops.push(1); + if (style.fillGradientType === TEXT_GRADIENT.LINEAR_VERTICAL) { // start the gradient at the top center of the canvas, and end at the bottom middle of the canvas @@ -532,15 +584,22 @@ // we need to repeat the gradient so that each individual line of text has the same vertical gradient effect // ['#FF0000', '#00FF00', '#0000FF'] over 2 lines would create stops at 0.125, 0.25, 0.375, 0.625, 0.75, 0.875 - totalIterations = (style.fill.length + 1) * lines.length; + totalIterations = (fill.length + 1) * lines.length; currentIteration = 0; for (let i = 0; i < lines.length; i++) { currentIteration += 1; - for (let j = 0; j < style.fill.length; j++) + for (let j = 0; j < fill.length; j++) { - stop = (currentIteration / totalIterations); - gradient.addColorStop(stop, style.fill[j]); + if (fillGradientStops[j]) + { + stop = (fillGradientStops[j] / lines.length) + (i / lines.length); + } + else + { + stop = currentIteration / totalIterations; + } + gradient.addColorStop(stop, fill[j]); currentIteration++; } } @@ -552,13 +611,20 @@ // can just evenly space out the gradients in this case, as multiple lines makes no difference // to an even left to right gradient - totalIterations = style.fill.length + 1; + totalIterations = fill.length + 1; currentIteration = 1; - for (let i = 0; i < style.fill.length; i++) + for (let i = 0; i < fill.length; i++) { - stop = currentIteration / totalIterations; - gradient.addColorStop(stop, style.fill[i]); + if (fillGradientStops[i]) + { + stop = fillGradientStops[i]; + } + else + { + stop = currentIteration / totalIterations; + } + gradient.addColorStop(stop, fill[i]); currentIteration++; } } @@ -569,7 +635,7 @@ /** * Destroys this text object. * Note* Unlike a Sprite, a Text object will automatically destroy its baseTexture and texture as - * the majorety of the time the texture will not be shared with any other Sprites. + * the majority of the time the texture will not be shared with any other Sprites. * * @param {object|boolean} [options] - Options parameter. A boolean will act as if all options * have been set to that value @@ -680,7 +746,7 @@ set text(text) // eslint-disable-line require-jsdoc { - text = String(text || ' '); + text = String(text === '' || text === null || text === undefined ? ' ' : text); if (this._text === text) { diff --git a/src/core/text/TextStyle.js b/src/core/text/TextStyle.js index 3b25528..890950b 100644 --- a/src/core/text/TextStyle.js +++ b/src/core/text/TextStyle.js @@ -8,12 +8,14 @@ align: 'left', breakWords: false, dropShadow: false, + dropShadowAlpha: 1, dropShadowAngle: Math.PI / 6, dropShadowBlur: 0, dropShadowColor: '#000000', dropShadowDistance: 5, fill: 'black', fillGradientType: TEXT_GRADIENT.LINEAR_VERTICAL, + fillGradientStops: [], fontFamily: 'Arial', fontSize: 26, fontStyle: 'normal', @@ -27,6 +29,7 @@ stroke: 'black', strokeThickness: 0, textBaseline: 'alphabetic', + trim: false, wordWrap: false, wordWrapWidth: 100, }; @@ -47,6 +50,7 @@ * @param {boolean} [style.breakWords=false] - Indicates if lines can be wrapped within words, it * needs wordWrap to be set to true * @param {boolean} [style.dropShadow=false] - Set a drop shadow for the text + * @param {number} [style.dropShadowAlpha=1] - Set alpha for the drop shadow * @param {number} [style.dropShadowAngle=Math.PI/6] - Set a angle of the drop shadow * @param {number} [style.dropShadowBlur=0] - Set a shadow blur radius * @param {string} [style.dropShadowColor='#000000'] - A fill style to be used on the dropshadow e.g 'red', '#00FF00' @@ -55,8 +59,10 @@ * fillstyle that will be used on the text e.g 'red', '#00FF00'. Can be an array to create a gradient * eg ['#000000','#FFFFFF'] * {@link https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/fillStyle|MDN} - * @param {number} [style.fillGradientType=PIXI.TEXT_GRADIENT.LINEAR_VERTICAL] - If fills styles are - * supplied, this can change the type/direction of the gradient. See {@link PIXI.TEXT_GRADIENT} for possible values + * @param {number} [style.fillGradientType=PIXI.TEXT_GRADIENT.LINEAR_VERTICAL] - If fill is an array of colours + * to create a gradient, this can change the type/direction of the gradient. See {@link PIXI.TEXT_GRADIENT} + * @param {number[]} [style.fillGradientStops] - If fill is an array of colours to create a gradient, this array can set + * the stop points (numbers between 0 and 1) for the color, overriding the default behaviour of evenly spacing them. * @param {string|string[]} [style.fontFamily='Arial'] - The font family * @param {number|string} [style.fontSize=26] - The font size (as a number it converts to px, but as a string, * equivalents are '26px','20pt','160%' or '1.6em') @@ -76,6 +82,7 @@ * e.g 'blue', '#FCFF00' * @param {number} [style.strokeThickness=0] - A number that represents the thickness of the stroke. * Default is 0 (no stroke) + * @param {boolean} [style.trim=false] - Trim transparent borders * @param {string} [style.textBaseline='alphabetic'] - The baseline of the text that is rendered. * @param {boolean} [style.wordWrap=false] - Indicates if word wrap should be used * @param {number} [style.wordWrapWidth=100] - The width at which text will wrap, it needs wordWrap to be set to true @@ -152,6 +159,19 @@ } } + get dropShadowAlpha() + { + return this._dropShadowAlpha; + } + set dropShadowAlpha(dropShadowAlpha) + { + if (this._dropShadowAlpha !== dropShadowAlpha) + { + this._dropShadowAlpha = dropShadowAlpha; + this.styleID++; + } + } + get dropShadowAngle() { return this._dropShadowAngle; @@ -232,6 +252,19 @@ } } + get fillGradientStops() + { + return this._fillGradientStops; + } + set fillGradientStops(fillGradientStops) + { + if (!areArraysEqual(this._fillGradientStops,fillGradientStops)) + { + this._fillGradientStops = fillGradientStops; + this.styleID++; + } + } + get fontFamily() { return this._fontFamily; @@ -402,6 +435,19 @@ } } + get trim() + { + return this._trim; + } + set trim(trim) + { + if (this._trim !== trim) + { + this._trim = trim; + this.styleID++; + } + } + get wordWrap() { return this._wordWrap; @@ -475,3 +521,34 @@ return color; } } + +/** + * Utility function to convert hexadecimal colors to strings, and simply return the color if it's a string. + * This version can also convert array of colors + * + * @param {Array} array1 First array to compare + * @param {Array} array2 Second array to compare + * @return {boolean} Do the arrays contain the same values in the same order + */ +function areArraysEqual(array1, array2) +{ + if (!Array.isArray(array1) || !Array.isArray(array2)) + { + return false; + } + + if (array1.length !== array2.length) + { + return false; + } + + for (let i = 0; i < array1.length; ++i) + { + if (array1[i] !== array2[i]) + { + return false; + } + } + + return true; +} diff --git a/src/core/textures/BaseRenderTexture.js b/src/core/textures/BaseRenderTexture.js index 5d50146..aa727f0 100644 --- a/src/core/textures/BaseRenderTexture.js +++ b/src/core/textures/BaseRenderTexture.js @@ -1,8 +1,6 @@ import BaseTexture from './BaseTexture'; import settings from '../settings'; -const { RESOLUTION, SCALE_MODE } = settings; - /** * A BaseRenderTexture is a special texture that allows any Pixi display object to be rendered to it. * @@ -54,7 +52,7 @@ { super(null, scaleMode); - this.resolution = resolution || RESOLUTION; + this.resolution = resolution || settings.RESOLUTION; this.width = width; this.height = height; @@ -62,7 +60,7 @@ this.realWidth = this.width * this.resolution; this.realHeight = this.height * this.resolution; - this.scaleMode = scaleMode || SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; this.hasLoaded = true; /** diff --git a/src/core/textures/BaseTexture.js b/src/core/textures/BaseTexture.js index 676dca3..aeccf39 100644 --- a/src/core/textures/BaseTexture.js +++ b/src/core/textures/BaseTexture.js @@ -77,7 +77,7 @@ * @default PIXI.settings.SCALE_MODE * @see PIXI.SCALE_MODES */ - this.scaleMode = scaleMode || settings.SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; /** * Set to true once the base texture has successfully loaded. @@ -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/RenderTexture.js b/src/core/textures/RenderTexture.js index 4c5034f..b3c6297 100644 --- a/src/core/textures/RenderTexture.js +++ b/src/core/textures/RenderTexture.js @@ -54,8 +54,8 @@ /* eslint-disable prefer-rest-params, no-console */ const width = arguments[1]; const height = arguments[2]; - const scaleMode = arguments[3] || 0; - const resolution = arguments[4] || 1; + const scaleMode = arguments[3]; + const resolution = arguments[4]; // we have an old render texture.. console.warn(`Please use RenderTexture.create(${width}, ${height}) instead of the ctor directly.`); 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 ca31456..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 @@ -459,7 +497,9 @@ if (frame.x + frame.width > this.baseTexture.width || frame.y + frame.height > this.baseTexture.height) { - throw new Error(`Texture Error: frame does not fit inside the base Texture dimensions ${this}`); + throw new Error('Texture Error: frame does not fit inside the base Texture dimensions: ' + + `X: ${frame.x} + ${frame.width} > ${this.baseTexture.width} ` + + `Y: ${frame.y} + ${frame.height} > ${this.baseTexture.height}`); } // this.valid = frame && frame.width && frame.height && this.baseTexture.source && this.baseTexture.hasLoaded; @@ -520,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. @@ -528,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 2a7497b..99e585a 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)); } /** @@ -360,3 +362,43 @@ * @private */ export const BaseTextureCache = {}; + +/** + * 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/core/utils/trimCanvas.js b/src/core/utils/trimCanvas.js new file mode 100644 index 0000000..5af6de0 --- /dev/null +++ b/src/core/utils/trimCanvas.js @@ -0,0 +1,83 @@ +/** + * Trim transparent borders from a canvas + * + * @memberof PIXI + * @function trimCanvas + * @private + * @param {HTMLCanvasElement} canvas - the canvas to trim + * @returns {object} Trim data + */ +export default function trimCanvas(canvas) +{ + // https://gist.github.com/remy/784508 + + let width = canvas.width; + let height = canvas.height; + + const context = canvas.getContext('2d'); + const imageData = context.getImageData(0, 0, width, height); + const pixels = imageData.data; + const len = pixels.length; + + const bound = { + top: null, + left: null, + right: null, + bottom: null, + }; + let i; + let x; + let y; + + for (i = 0; i < len; i += 4) + { + if (pixels[i + 3] !== 0) + { + x = (i / 4) % width; + y = ~~((i / 4) / width); + + if (bound.top === null) + { + bound.top = y; + } + + if (bound.left === null) + { + bound.left = x; + } + else if (x < bound.left) + { + bound.left = x; + } + + if (bound.right === null) + { + bound.right = x + 1; + } + else if (bound.right < x) + { + bound.right = x + 1; + } + + if (bound.bottom === null) + { + bound.bottom = y; + } + else if (bound.bottom < y) + { + bound.bottom = y; + } + } + } + + width = bound.right - bound.left; + height = bound.bottom - bound.top + 1; + + const data = context.getImageData(bound.left, bound.top, width, height); + + return { + height, + width, + data, + }; +} diff --git a/src/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/extras/TilingSprite.js b/src/extras/TilingSprite.js index f90d248..fe82451 100644 --- a/src/extras/TilingSprite.js +++ b/src/extras/TilingSprite.js @@ -220,20 +220,33 @@ transform.tx * resolution, transform.ty * resolution); - // TODO - this should be rolled into the setTransform above.. - context.scale(this.tileScale.x / baseTextureResolution, this.tileScale.y / baseTextureResolution); - - context.translate(modX + (this.anchor.x * -this._width), - modY + (this.anchor.y * -this._height)); - renderer.setBlendMode(this.blendMode); // fill the pattern! context.fillStyle = this._canvasPattern; - context.fillRect(-modX, - -modY, - this._width / this.tileScale.x * baseTextureResolution, - this._height / this.tileScale.y * baseTextureResolution); + + // TODO - this should be rolled into the setTransform above.. + context.scale(this.tileScale.x / baseTextureResolution, this.tileScale.y / baseTextureResolution); + + const anchorX = this.anchor.x * -this._width; + const anchorY = this.anchor.y * -this._height; + + if (this.uvRespectAnchor) + { + context.translate(modX, modY); + + context.fillRect(-modX + anchorX, -modY + anchorY, + this._width / this.tileScale.x * baseTextureResolution, + this._height / this.tileScale.y * baseTextureResolution); + } + else + { + context.translate(modX + anchorX, modY + anchorY); + + context.fillRect(-modX, -modY, + this._width / this.tileScale.x * baseTextureResolution, + this._height / this.tileScale.y * baseTextureResolution); + } } /** diff --git a/src/extras/cacheAsBitmap.js b/src/extras/cacheAsBitmap.js index 18043a7..5dd5640 100644 --- a/src/extras/cacheAsBitmap.js +++ b/src/extras/cacheAsBitmap.js @@ -41,6 +41,9 @@ * provide a performance benefit for complex static displayObjects. * To remove simply set this property to 'false' * + * IMPORTANT GOTCHA - make sure that all your textures are preloaded BEFORE setting this property to true + * as it will take a snapshot of what is currently there. If the textures have not loaded then they will not appear. + * * @member {boolean} * @memberof PIXI.DisplayObject# */ diff --git a/src/filters/blur/BlurFilter.js b/src/filters/blur/BlurFilter.js index 340fc58..1575db9 100644 --- a/src/filters/blur/BlurFilter.js +++ b/src/filters/blur/BlurFilter.js @@ -24,10 +24,9 @@ this.blurXFilter = new BlurXFilter(strength, quality, resolution, kernelSize); this.blurYFilter = new BlurYFilter(strength, quality, resolution, kernelSize); - this.resolution = 1; this.padding = 0; - this.resolution = resolution || 1; + this.resolution = resolution || core.settings.RESOLUTION; this.quality = quality || 4; this.blur = strength || 8; } diff --git a/src/filters/blur/BlurXFilter.js b/src/filters/blur/BlurXFilter.js index def99c3..0f48889 100644 --- a/src/filters/blur/BlurXFilter.js +++ b/src/filters/blur/BlurXFilter.js @@ -31,7 +31,7 @@ fragSrc ); - this.resolution = resolution || 1; + this.resolution = resolution || core.settings.RESOLUTION; this._quality = 0; diff --git a/src/filters/blur/BlurYFilter.js b/src/filters/blur/BlurYFilter.js index 638e0f0..6d489c7 100644 --- a/src/filters/blur/BlurYFilter.js +++ b/src/filters/blur/BlurYFilter.js @@ -31,7 +31,7 @@ fragSrc ); - this.resolution = resolution || 1; + this.resolution = resolution || core.settings.RESOLUTION; this._quality = 0; diff --git a/src/filters/colormatrix/ColorMatrixFilter.js b/src/filters/colormatrix/ColorMatrixFilter.js index 8509505..679f8ec 100644 --- a/src/filters/colormatrix/ColorMatrixFilter.js +++ b/src/filters/colormatrix/ColorMatrixFilter.js @@ -75,28 +75,28 @@ out[1] = (a[0] * b[1]) + (a[1] * b[6]) + (a[2] * b[11]) + (a[3] * b[16]); out[2] = (a[0] * b[2]) + (a[1] * b[7]) + (a[2] * b[12]) + (a[3] * b[17]); out[3] = (a[0] * b[3]) + (a[1] * b[8]) + (a[2] * b[13]) + (a[3] * b[18]); - out[4] = (a[0] * b[4]) + (a[1] * b[9]) + (a[2] * b[14]) + (a[3] * b[19]); + out[4] = (a[0] * b[4]) + (a[1] * b[9]) + (a[2] * b[14]) + (a[3] * b[19]) + a[4]; // Green Channel out[5] = (a[5] * b[0]) + (a[6] * b[5]) + (a[7] * b[10]) + (a[8] * b[15]); out[6] = (a[5] * b[1]) + (a[6] * b[6]) + (a[7] * b[11]) + (a[8] * b[16]); out[7] = (a[5] * b[2]) + (a[6] * b[7]) + (a[7] * b[12]) + (a[8] * b[17]); out[8] = (a[5] * b[3]) + (a[6] * b[8]) + (a[7] * b[13]) + (a[8] * b[18]); - out[9] = (a[5] * b[4]) + (a[6] * b[9]) + (a[7] * b[14]) + (a[8] * b[19]); + out[9] = (a[5] * b[4]) + (a[6] * b[9]) + (a[7] * b[14]) + (a[8] * b[19]) + a[9]; // Blue Channel out[10] = (a[10] * b[0]) + (a[11] * b[5]) + (a[12] * b[10]) + (a[13] * b[15]); out[11] = (a[10] * b[1]) + (a[11] * b[6]) + (a[12] * b[11]) + (a[13] * b[16]); out[12] = (a[10] * b[2]) + (a[11] * b[7]) + (a[12] * b[12]) + (a[13] * b[17]); out[13] = (a[10] * b[3]) + (a[11] * b[8]) + (a[12] * b[13]) + (a[13] * b[18]); - out[14] = (a[10] * b[4]) + (a[11] * b[9]) + (a[12] * b[14]) + (a[13] * b[19]); + out[14] = (a[10] * b[4]) + (a[11] * b[9]) + (a[12] * b[14]) + (a[13] * b[19]) + a[14]; // Alpha Channel out[15] = (a[15] * b[0]) + (a[16] * b[5]) + (a[17] * b[10]) + (a[18] * b[15]); out[16] = (a[15] * b[1]) + (a[16] * b[6]) + (a[17] * b[11]) + (a[18] * b[16]); out[17] = (a[15] * b[2]) + (a[16] * b[7]) + (a[17] * b[12]) + (a[18] * b[17]); out[18] = (a[15] * b[3]) + (a[16] * b[8]) + (a[17] * b[13]) + (a[18] * b[18]); - out[19] = (a[15] * b[4]) + (a[16] * b[9]) + (a[17] * b[14]) + (a[18] * b[19]); + out[19] = (a[15] * b[4]) + (a[16] * b[9]) + (a[17] * b[14]) + (a[18] * b[19]) + a[19]; return out; } diff --git a/src/filters/colormatrix/colorMatrix.frag b/src/filters/colormatrix/colorMatrix.frag index 3aaf2b3..c73c0e9 100644 --- a/src/filters/colormatrix/colorMatrix.frag +++ b/src/filters/colormatrix/colorMatrix.frag @@ -4,32 +4,38 @@ void main(void) { - vec4 c = texture2D(uSampler, vTextureCoord); + // Un-premultiply alpha before applying the color matrix. See issue #3539. + if (c.a > 0.0) { + c.rgb /= c.a; + } + vec4 result; + result.r = (m[0] * c.r); + result.r += (m[1] * c.g); + result.r += (m[2] * c.b); + result.r += (m[3] * c.a); + result.r += m[4]; - gl_FragColor.r = (m[0] * c.r); - gl_FragColor.r += (m[1] * c.g); - gl_FragColor.r += (m[2] * c.b); - gl_FragColor.r += (m[3] * c.a); - gl_FragColor.r += m[4] * c.a; + result.g = (m[5] * c.r); + result.g += (m[6] * c.g); + result.g += (m[7] * c.b); + result.g += (m[8] * c.a); + result.g += m[9]; - gl_FragColor.g = (m[5] * c.r); - gl_FragColor.g += (m[6] * c.g); - gl_FragColor.g += (m[7] * c.b); - gl_FragColor.g += (m[8] * c.a); - gl_FragColor.g += m[9] * c.a; + result.b = (m[10] * c.r); + result.b += (m[11] * c.g); + result.b += (m[12] * c.b); + result.b += (m[13] * c.a); + result.b += m[14]; - gl_FragColor.b = (m[10] * c.r); - gl_FragColor.b += (m[11] * c.g); - gl_FragColor.b += (m[12] * c.b); - gl_FragColor.b += (m[13] * c.a); - gl_FragColor.b += m[14] * c.a; + result.a = (m[15] * c.r); + result.a += (m[16] * c.g); + result.a += (m[17] * c.b); + result.a += (m[18] * c.a); + result.a += m[19]; - gl_FragColor.a = (m[15] * c.r); - gl_FragColor.a += (m[16] * c.g); - gl_FragColor.a += (m[17] * c.b); - gl_FragColor.a += (m[18] * c.a); - gl_FragColor.a += m[19] * c.a; + // Premultiply alpha again. + result.rgb *= result.a; -// gl_FragColor = vec4(m[0]); + gl_FragColor = result; } diff --git a/src/filters/displacement/DisplacementFilter.js b/src/filters/displacement/DisplacementFilter.js index 112de0b..d5355c3 100644 --- a/src/filters/displacement/DisplacementFilter.js +++ b/src/filters/displacement/DisplacementFilter.js @@ -36,7 +36,7 @@ this.maskMatrix = maskMatrix; this.uniforms.mapSampler = sprite.texture; - this.uniforms.filterMatrix = maskMatrix.toArray(true); + this.uniforms.filterMatrix = maskMatrix; this.uniforms.scale = { x: 1, y: 1 }; if (scale === null || scale === undefined) diff --git a/src/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/InteractionData.js b/src/interaction/InteractionData.js index 02d53a3..818da35 100644 --- a/src/interaction/InteractionData.js +++ b/src/interaction/InteractionData.js @@ -21,9 +21,9 @@ this.global = new core.Point(); /** - * The target Sprite that was interacted with + * The target DisplayObject that was interacted with * - * @member {PIXI.Sprite} + * @member {PIXI.DisplayObject} */ this.target = null; @@ -33,6 +33,13 @@ * @member {Event} */ this.originalEvent = null; + + /** + * Unique identifier for this interaction + * + * @member {number} + */ + this.identifier = null; } /** diff --git a/src/interaction/InteractionEvent.js b/src/interaction/InteractionEvent.js index 75b2966..dc554f9 100644 --- a/src/interaction/InteractionEvent.js +++ b/src/interaction/InteractionEvent.js @@ -58,7 +58,7 @@ } /** - * Prevents event from reaching any objects other than the current object. + * Resets the event. * * @private */ diff --git a/src/interaction/InteractionManager.js b/src/interaction/InteractionManager.js index 7d51134..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/src/prepare/BasePrepare.js b/src/prepare/BasePrepare.js index a2005f5..09eb5e5 100644 --- a/src/prepare/BasePrepare.js +++ b/src/prepare/BasePrepare.js @@ -20,7 +20,7 @@ * * @abstract * @class - * @memberof PIXI + * @memberof PIXI.prepare */ export default class BasePrepare { @@ -107,9 +107,9 @@ /** * Upload all the textures and graphics to the GPU. * - * @param {Function|PIXI.DisplayObject|PIXI.Container} item - Either - * the container or display object to search for items to upload or - * the callback function, if items have been added using `prepare.add`. + * @param {Function|PIXI.DisplayObject|PIXI.Container|PIXI.BaseTexture|PIXI.Texture|PIXI.Graphics|PIXI.Text} item - + * Either the container or display object to search for items to upload, the items to upload themselves, + * or the callback function, if items have been added using `prepare.add`. * @param {Function} [done] - Optional callback when all queued uploads have completed */ upload(item, done) @@ -236,7 +236,8 @@ /** * Manually add an item to the uploading queue. * - * @param {PIXI.DisplayObject|PIXI.Container|*} item - Object to add to the queue + * @param {PIXI.DisplayObject|PIXI.Container|PIXI.BaseTexture|PIXI.Texture|PIXI.Graphics|PIXI.Text|*} item - Object to + * add to the queue * @return {PIXI.CanvasPrepare} Instance of plugin for chaining. */ add(item) diff --git a/package.json b/package.json index 1271fef..71ade15 100644 --- a/package.json +++ b/package.json @@ -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/scripts/release.js b/scripts/release.js index c4c8e36..156620c 100644 --- a/scripts/release.js +++ b/scripts/release.js @@ -12,7 +12,8 @@ 'dist/**/*', 'lib/**/*', 'src/**/*', - 'scripts/**/*', + 'scripts/*', + 'scripts/renders/*', 'test/**/*', '*.json', '*.md', diff --git a/scripts/renders/client.js b/scripts/renders/client.js index 16c9565..6faee34 100755 --- a/scripts/renders/client.js +++ b/scripts/renders/client.js @@ -1,6 +1,6 @@ 'use strict'; -require('../../bin/pixi'); +require('../../dist/pixi'); PIXI.utils.skipHello(); const fs = require('fs'); 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 bc730db..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. @@ -33,8 +33,11 @@ * need to call toDataUrl on the webgl context * @param {number} [options.resolution=1] - The resolution / device pixel ratio of the renderer, retina would be 2 * @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 @@ -43,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. */ @@ -73,7 +99,7 @@ */ stop() { - this.ticker.stop(); + this._ticker.stop(); } /** @@ -81,7 +107,7 @@ */ start() { - this.ticker.start(); + this._ticker.start(); } /** @@ -95,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 index 81e076b..78b17e6 100644 --- a/src/core/Shader.js +++ b/src/core/Shader.js @@ -1,9 +1,7 @@ import { GLShader } from 'pixi-gl-core'; import settings from './settings'; -const { PRECISION } = settings; - -function checkPrecision(src) +function checkPrecision(src, def) { if (src instanceof Array) { @@ -11,14 +9,14 @@ { const copy = src.slice(0); - copy.unshift(`precision ${PRECISION} float;`); + copy.unshift(`precision ${def} float;`); return copy; } } else if (src.substring(0, 9) !== 'precision') { - return `precision ${PRECISION} float;\n${src}`; + return `precision ${def} float;\n${src}`; } return src; @@ -42,6 +40,7 @@ */ constructor(gl, vertexSrc, fragmentSrc) { - super(gl, checkPrecision(vertexSrc), checkPrecision(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 3425b41..f9235e4 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -181,7 +181,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 b3792ef..17fde8d 100644 --- a/src/core/display/Container.js +++ b/src/core/display/Container.js @@ -73,13 +73,14 @@ } child.parent = this; - - // ensure a transform will be recalculated.. - this.transform._parentID = -1; - this._boundsID++; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.push(child); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(this.children.length - 1); child.emit('added', this); @@ -108,9 +109,14 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.splice(index, 0, child); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(index); child.emit('added', this); @@ -174,6 +180,7 @@ removeItems(this.children, currentIndex, 1); // remove from old position this.children.splice(index, 0, child); // add at new position + this.onChildrenChange(index); } @@ -220,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! @@ -244,9 +252,14 @@ { const child = this.getChildAt(index); + // ensure child transform will be recalculated.. child.parent = null; + child.transform._parentID = -1; removeItems(this.children, index, 1); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(index); child.emit('removed', this); @@ -275,8 +288,14 @@ for (let i = 0; i < removed.length; ++i) { removed[i].parent = null; + if (removed[i].transform) + { + removed[i].transform._parentID = -1; + } } + this._boundsID++; + this.onChildrenChange(beginIndex); for (let i = 0; i < removed.length; ++i) diff --git a/src/core/display/DisplayObject.js b/src/core/display/DisplayObject.js index 3affe36..4e6c77d 100644 --- a/src/core/display/DisplayObject.js +++ b/src/core/display/DisplayObject.js @@ -109,7 +109,7 @@ /** * The original, cached mask of the object * - * @member {PIXI.Rectangle} + * @member {PIXI.Graphics|PIXI.Sprite} * @private */ this._mask = null; diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index 95f2af4..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 ); @@ -1065,10 +1061,15 @@ canvasRenderer = new CanvasRenderer(); } - tempMatrix.tx = -bounds.x; - tempMatrix.ty = -bounds.y; + this.transform.updateLocalTransform(); + this.transform.localTransform.copy(tempMatrix); - canvasRenderer.render(this, canvasBuffer, false, tempMatrix); + tempMatrix.invert(); + + tempMatrix.tx -= bounds.x; + tempMatrix.ty -= bounds.y; + + canvasRenderer.render(this, canvasBuffer, true, tempMatrix); const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode); 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 e67680f..867dde2 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/buildCircle.js b/src/core/graphics/webgl/utils/buildCircle.js index 0400d13..abf5d02 100644 --- a/src/core/graphics/webgl/utils/buildCircle.js +++ b/src/core/graphics/webgl/utils/buildCircle.js @@ -33,6 +33,11 @@ height = circleData.height; } + if (width === 0 || height === 0) + { + return; + } + const totalSegs = Math.floor(30 * Math.sqrt(circleData.radius)) || Math.floor(15 * Math.sqrt(circleData.width + circleData.height)); diff --git a/src/core/graphics/webgl/utils/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 90155f9..f03fe15 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -30,6 +30,7 @@ export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; export { default as CanvasGraphicsRenderer } from './graphics/canvas/CanvasGraphicsRenderer'; +export { default as Spritesheet } from './textures/Spritesheet'; export { default as Texture } from './textures/Texture'; export { default as BaseTexture } from './textures/BaseTexture'; export { default as RenderTexture } from './textures/RenderTexture'; diff --git a/src/core/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 ccce680..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); } /** @@ -162,6 +162,9 @@ if (transform) { transform.copy(tempWt); + + // lets not forget to flag the parent transform as dirty... + this._tempDisplayObjectParent.transform._worldID = -1; } else { @@ -169,6 +172,7 @@ } displayObject.parent = this._tempDisplayObjectParent; + displayObject.updateTransform(); displayObject.parent = cacheParent; // displayObject.hitArea = //TODO add a temp hit area @@ -279,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/canvas/utils/CanvasRenderTarget.js b/src/core/renderers/canvas/utils/CanvasRenderTarget.js index 3de0aef..b409891 100644 --- a/src/core/renderers/canvas/utils/CanvasRenderTarget.js +++ b/src/core/renderers/canvas/utils/CanvasRenderTarget.js @@ -1,5 +1,4 @@ import settings from '../../../settings'; -const { RESOLUTION } = settings; /** * Creates a Canvas element of the given size. @@ -30,7 +29,7 @@ */ this.context = this.canvas.getContext('2d'); - this.resolution = resolution || RESOLUTION; + this.resolution = resolution || settings.RESOLUTION; this.resize(width, height); } 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 ce0e1c4..788ed6c 100644 --- a/src/core/renderers/webgl/WebGLRenderer.js +++ b/src/core/renderers/webgl/WebGLRenderer.js @@ -30,8 +30,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 @@ -49,10 +49,19 @@ * enable this if you need to call toDataUrl on the webgl context. * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when * rendering, stopping pixel interpolation. + * @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; + + if (this.legacy) + { + glCore.VertexArrayObject.FORCE_NATIVE = true; + } /** * The type of this renderer as a standardised const @@ -229,7 +238,7 @@ this.emit('context', gl); // setup the width/height properties and gl viewport - this.resize(this.width, this.height); + this.resize(this.screen.width, this.screen.height); } /** @@ -323,16 +332,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) { @@ -376,6 +385,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 @@ -440,9 +469,10 @@ * Changes the current shader to the one given in parameter * * @param {PIXI.Shader} shader - the new shader + * @param {boolean} [autoProject=true] - Whether automatically set the projection matrix * @return {PIXI.WebGLRenderer} Returns itself. */ - bindShader(shader) + bindShader(shader, autoProject) { // TODO cache if (this._activeShader !== shader) @@ -450,8 +480,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 bbab3b3..944f41d 100644 --- a/src/core/renderers/webgl/filters/Filter.js +++ b/src/core/renderers/webgl/filters/Filter.js @@ -1,6 +1,7 @@ import extractUniformsFromSrc from './extractUniformsFromSrc'; import { uid } from '../../../utils'; import { BLEND_MODES } from '../../../const'; +import settings from '../../../settings'; const SOURCE_KEY_MAP = {}; @@ -10,7 +11,7 @@ * @memberof PIXI * @extends PIXI.Shader */ -class Filter +export default class Filter { /** * @param {string} [vertexSrc] - The source of the vertex shader. @@ -35,8 +36,6 @@ this.blendMode = BLEND_MODES.NORMAL; - // pull out the vertex and shader uniforms if they are not specified.. - // currently this does not extract structs only default types this.uniformData = uniforms || extractUniformsFromSrc(this.vertexSrc, this.fragmentSrc, 'projectionMatrix|uSampler'); /** @@ -80,7 +79,7 @@ * * @member {number} */ - this.resolution = 1; + this.resolution = settings.RESOLUTION; /** * If enabled is true the filter is applied, if false it will not. @@ -97,8 +96,11 @@ * @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 { // --- // // this.uniforms.filterMatrix = filterManager.calculateSpriteMatrix(tempMatrix, window.panda ); @@ -170,5 +172,3 @@ ].join('\n'); } } - -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 dc7072a..6c7422d 100644 --- a/src/core/renderers/webgl/managers/FilterManager.js +++ b/src/core/renderers/webgl/managers/FilterManager.js @@ -154,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 @@ -176,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; @@ -184,7 +184,7 @@ flop = t; } - filters[i].apply(this, flip, lastState.renderTarget, true); + filters[i].apply(this, flip, lastState.renderTarget, false, currentState); this.freePotRenderTarget(flip); this.freePotRenderTarget(flop); @@ -291,9 +291,13 @@ let textureCount = 1; let currentState; - if (shader.uniforms.data.filterArea) + // 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; @@ -306,9 +310,9 @@ // use this to clamp displaced texture coords so they belong to filterArea // see displacementFilter fragment shader for an example - if (shader.uniforms.data.filterClamp) + if (shader.uniforms.filterClamp) { - currentState = this.filterData.stack[this.filterData.index]; + currentState = currentState || this.filterData.stack[this.filterData.index]; const filterClamp = shader.uniforms.filterClamp; @@ -485,7 +489,7 @@ */ destroy() { - this.shaderCache = []; + this.shaderCache = {}; this.emptyPool(); } diff --git a/src/core/renderers/webgl/utils/RenderTarget.js b/src/core/renderers/webgl/utils/RenderTarget.js index afe51ab..6d2467f 100644 --- a/src/core/renderers/webgl/utils/RenderTarget.js +++ b/src/core/renderers/webgl/utils/RenderTarget.js @@ -124,7 +124,7 @@ * @default PIXI.settings.SCALE_MODE * @see PIXI.SCALE_MODES */ - this.scaleMode = scaleMode || settings.SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; /** * Whether this object is the root element or not 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 36ea5bb..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; @@ -346,8 +346,8 @@ /** * Gets the local bounds of the sprite object. * - * @param {Rectangle} rect - The output rectangle. - * @return {Rectangle} The bounds. + * @param {PIXI.Rectangle} rect - The output rectangle. + * @return {PIXI.Rectangle} The bounds. */ getLocalBounds(rect) { @@ -440,7 +440,7 @@ * * @static * @param {number|string|PIXI.BaseTexture|HTMLCanvasElement|HTMLVideoElement} source Source to create texture from - * @return {PIXI.Texture} 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 e77b39a..90b08c0 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++) @@ -103,11 +102,18 @@ { const gl = this.renderer.gl; - // step 1: first check max textures the GPU can handle. - this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); + if (this.renderer.legacy) + { + this.MAX_TEXTURES = 1; + } + else + { + // step 1: first check max textures the GPU can handle. + this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); - // step 2: check the maximum number of if statements the shader can have too.. - this.MAX_TEXTURES = checkMaxIfStatmentsInShader(this.MAX_TEXTURES, gl); + // step 2: check the maximum number of if statements the shader can have too.. + this.MAX_TEXTURES = checkMaxIfStatmentsInShader(this.MAX_TEXTURES, gl); + } const shader = this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); @@ -130,8 +136,12 @@ .addIndex(this.indexBuffer) .addAttribute(this.vertexBuffers[i], shader.attributes.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) .addAttribute(this.vertexBuffers[i], shader.attributes.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) - .addAttribute(this.vertexBuffers[i], shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4) - .addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + .addAttribute(this.vertexBuffers[i], shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); + + if (shader.attributes.aTextureId) + { + this.vaos[i].addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + } /* eslint-enable max-len */ } @@ -378,8 +388,12 @@ .addIndex(this.indexBuffer) .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) - .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4) - .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); + + if (this.shader.attributes.aTextureId) + { + this.vaos[this.vertexCount].addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + } /* eslint-enable max-len */ } diff --git a/src/core/text/Text.js b/src/core/text/Text.js index 013625a..cec8c32 100644 --- a/src/core/text/Text.js +++ b/src/core/text/Text.js @@ -6,6 +6,7 @@ import { TEXT_GRADIENT } from '../const'; import settings from '../settings'; import TextStyle from './TextStyle'; +import trimCanvas from '../utils/trimCanvas'; const defaultDestroyOptions = { texture: true, @@ -57,7 +58,7 @@ /** * The canvas 2d context that everything is drawn with - * @member {HTMLCanvasElement} + * @member {CanvasRenderingContext2D} */ this.context = this.canvas.getContext('2d'); @@ -189,10 +190,12 @@ if (style.dropShadow) { + this.context.shadowBlur = style.dropShadowBlur; + this.context.globalAlpha = style.dropShadowAlpha; + if (style.dropShadowBlur > 0) { this.context.shadowColor = style.dropShadowColor; - this.context.shadowBlur = style.dropShadowBlur; } else { @@ -237,6 +240,10 @@ } } + // reset the shadow blur and alpha that was set by the drop shadow, for the regular text + this.context.shadowBlur = 0; + this.context.globalAlpha = 1; + // set canvas text styles this.context.fillStyle = this._generateFillStyle(style, lines); @@ -326,6 +333,15 @@ */ updateTexture() { + if (this._style.trim) + { + const trimmed = trimCanvas(this.canvas); + + this.canvas.width = trimmed.width; + this.canvas.height = trimmed.height; + this.context.putImageData(trimmed.data, 0, 0); + } + const texture = this._texture; const style = this._style; @@ -475,6 +491,19 @@ } /** + * Gets the local bounds of the text object. + * + * @param {Rectangle} rect - The output rectangle. + * @return {Rectangle} The bounds. + */ + getLocalBounds(rect) + { + this.updateText(true); + + return super.getLocalBounds.call(this, rect); + } + + /** * calculates the bounds of the Text as a rectangle. The bounds calculation takes the worldTransform into account. */ _calculateBounds() @@ -525,6 +554,29 @@ const width = this.canvas.width / this.resolution; const height = this.canvas.height / this.resolution; + // make a copy of the style settings, so we can manipulate them later + const fill = style.fill.slice(); + const fillGradientStops = style.fillGradientStops.slice(); + + // wanting to evenly distribute the fills. So an array of 4 colours should give fills of 0.25, 0.5 and 0.75 + if (!fillGradientStops.length) + { + const lengthPlus1 = fill.length + 1; + + for (let i = 1; i < lengthPlus1; ++i) + { + fillGradientStops.push(i / lengthPlus1); + } + } + + // stop the bleeding of the last gradient on the line above to the top gradient of the this line + // by hard defining the first gradient colour at point 0, and last gradient colour at point 1 + fill.unshift(style.fill[0]); + fillGradientStops.unshift(0); + + fill.push(style.fill[style.fill.length - 1]); + fillGradientStops.push(1); + if (style.fillGradientType === TEXT_GRADIENT.LINEAR_VERTICAL) { // start the gradient at the top center of the canvas, and end at the bottom middle of the canvas @@ -532,15 +584,22 @@ // we need to repeat the gradient so that each individual line of text has the same vertical gradient effect // ['#FF0000', '#00FF00', '#0000FF'] over 2 lines would create stops at 0.125, 0.25, 0.375, 0.625, 0.75, 0.875 - totalIterations = (style.fill.length + 1) * lines.length; + totalIterations = (fill.length + 1) * lines.length; currentIteration = 0; for (let i = 0; i < lines.length; i++) { currentIteration += 1; - for (let j = 0; j < style.fill.length; j++) + for (let j = 0; j < fill.length; j++) { - stop = (currentIteration / totalIterations); - gradient.addColorStop(stop, style.fill[j]); + if (fillGradientStops[j]) + { + stop = (fillGradientStops[j] / lines.length) + (i / lines.length); + } + else + { + stop = currentIteration / totalIterations; + } + gradient.addColorStop(stop, fill[j]); currentIteration++; } } @@ -552,13 +611,20 @@ // can just evenly space out the gradients in this case, as multiple lines makes no difference // to an even left to right gradient - totalIterations = style.fill.length + 1; + totalIterations = fill.length + 1; currentIteration = 1; - for (let i = 0; i < style.fill.length; i++) + for (let i = 0; i < fill.length; i++) { - stop = currentIteration / totalIterations; - gradient.addColorStop(stop, style.fill[i]); + if (fillGradientStops[i]) + { + stop = fillGradientStops[i]; + } + else + { + stop = currentIteration / totalIterations; + } + gradient.addColorStop(stop, fill[i]); currentIteration++; } } @@ -569,7 +635,7 @@ /** * Destroys this text object. * Note* Unlike a Sprite, a Text object will automatically destroy its baseTexture and texture as - * the majorety of the time the texture will not be shared with any other Sprites. + * the majority of the time the texture will not be shared with any other Sprites. * * @param {object|boolean} [options] - Options parameter. A boolean will act as if all options * have been set to that value @@ -680,7 +746,7 @@ set text(text) // eslint-disable-line require-jsdoc { - text = String(text || ' '); + text = String(text === '' || text === null || text === undefined ? ' ' : text); if (this._text === text) { diff --git a/src/core/text/TextStyle.js b/src/core/text/TextStyle.js index 3b25528..890950b 100644 --- a/src/core/text/TextStyle.js +++ b/src/core/text/TextStyle.js @@ -8,12 +8,14 @@ align: 'left', breakWords: false, dropShadow: false, + dropShadowAlpha: 1, dropShadowAngle: Math.PI / 6, dropShadowBlur: 0, dropShadowColor: '#000000', dropShadowDistance: 5, fill: 'black', fillGradientType: TEXT_GRADIENT.LINEAR_VERTICAL, + fillGradientStops: [], fontFamily: 'Arial', fontSize: 26, fontStyle: 'normal', @@ -27,6 +29,7 @@ stroke: 'black', strokeThickness: 0, textBaseline: 'alphabetic', + trim: false, wordWrap: false, wordWrapWidth: 100, }; @@ -47,6 +50,7 @@ * @param {boolean} [style.breakWords=false] - Indicates if lines can be wrapped within words, it * needs wordWrap to be set to true * @param {boolean} [style.dropShadow=false] - Set a drop shadow for the text + * @param {number} [style.dropShadowAlpha=1] - Set alpha for the drop shadow * @param {number} [style.dropShadowAngle=Math.PI/6] - Set a angle of the drop shadow * @param {number} [style.dropShadowBlur=0] - Set a shadow blur radius * @param {string} [style.dropShadowColor='#000000'] - A fill style to be used on the dropshadow e.g 'red', '#00FF00' @@ -55,8 +59,10 @@ * fillstyle that will be used on the text e.g 'red', '#00FF00'. Can be an array to create a gradient * eg ['#000000','#FFFFFF'] * {@link https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/fillStyle|MDN} - * @param {number} [style.fillGradientType=PIXI.TEXT_GRADIENT.LINEAR_VERTICAL] - If fills styles are - * supplied, this can change the type/direction of the gradient. See {@link PIXI.TEXT_GRADIENT} for possible values + * @param {number} [style.fillGradientType=PIXI.TEXT_GRADIENT.LINEAR_VERTICAL] - If fill is an array of colours + * to create a gradient, this can change the type/direction of the gradient. See {@link PIXI.TEXT_GRADIENT} + * @param {number[]} [style.fillGradientStops] - If fill is an array of colours to create a gradient, this array can set + * the stop points (numbers between 0 and 1) for the color, overriding the default behaviour of evenly spacing them. * @param {string|string[]} [style.fontFamily='Arial'] - The font family * @param {number|string} [style.fontSize=26] - The font size (as a number it converts to px, but as a string, * equivalents are '26px','20pt','160%' or '1.6em') @@ -76,6 +82,7 @@ * e.g 'blue', '#FCFF00' * @param {number} [style.strokeThickness=0] - A number that represents the thickness of the stroke. * Default is 0 (no stroke) + * @param {boolean} [style.trim=false] - Trim transparent borders * @param {string} [style.textBaseline='alphabetic'] - The baseline of the text that is rendered. * @param {boolean} [style.wordWrap=false] - Indicates if word wrap should be used * @param {number} [style.wordWrapWidth=100] - The width at which text will wrap, it needs wordWrap to be set to true @@ -152,6 +159,19 @@ } } + get dropShadowAlpha() + { + return this._dropShadowAlpha; + } + set dropShadowAlpha(dropShadowAlpha) + { + if (this._dropShadowAlpha !== dropShadowAlpha) + { + this._dropShadowAlpha = dropShadowAlpha; + this.styleID++; + } + } + get dropShadowAngle() { return this._dropShadowAngle; @@ -232,6 +252,19 @@ } } + get fillGradientStops() + { + return this._fillGradientStops; + } + set fillGradientStops(fillGradientStops) + { + if (!areArraysEqual(this._fillGradientStops,fillGradientStops)) + { + this._fillGradientStops = fillGradientStops; + this.styleID++; + } + } + get fontFamily() { return this._fontFamily; @@ -402,6 +435,19 @@ } } + get trim() + { + return this._trim; + } + set trim(trim) + { + if (this._trim !== trim) + { + this._trim = trim; + this.styleID++; + } + } + get wordWrap() { return this._wordWrap; @@ -475,3 +521,34 @@ return color; } } + +/** + * Utility function to convert hexadecimal colors to strings, and simply return the color if it's a string. + * This version can also convert array of colors + * + * @param {Array} array1 First array to compare + * @param {Array} array2 Second array to compare + * @return {boolean} Do the arrays contain the same values in the same order + */ +function areArraysEqual(array1, array2) +{ + if (!Array.isArray(array1) || !Array.isArray(array2)) + { + return false; + } + + if (array1.length !== array2.length) + { + return false; + } + + for (let i = 0; i < array1.length; ++i) + { + if (array1[i] !== array2[i]) + { + return false; + } + } + + return true; +} diff --git a/src/core/textures/BaseRenderTexture.js b/src/core/textures/BaseRenderTexture.js index 5d50146..aa727f0 100644 --- a/src/core/textures/BaseRenderTexture.js +++ b/src/core/textures/BaseRenderTexture.js @@ -1,8 +1,6 @@ import BaseTexture from './BaseTexture'; import settings from '../settings'; -const { RESOLUTION, SCALE_MODE } = settings; - /** * A BaseRenderTexture is a special texture that allows any Pixi display object to be rendered to it. * @@ -54,7 +52,7 @@ { super(null, scaleMode); - this.resolution = resolution || RESOLUTION; + this.resolution = resolution || settings.RESOLUTION; this.width = width; this.height = height; @@ -62,7 +60,7 @@ this.realWidth = this.width * this.resolution; this.realHeight = this.height * this.resolution; - this.scaleMode = scaleMode || SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; this.hasLoaded = true; /** diff --git a/src/core/textures/BaseTexture.js b/src/core/textures/BaseTexture.js index 676dca3..aeccf39 100644 --- a/src/core/textures/BaseTexture.js +++ b/src/core/textures/BaseTexture.js @@ -77,7 +77,7 @@ * @default PIXI.settings.SCALE_MODE * @see PIXI.SCALE_MODES */ - this.scaleMode = scaleMode || settings.SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; /** * Set to true once the base texture has successfully loaded. @@ -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/RenderTexture.js b/src/core/textures/RenderTexture.js index 4c5034f..b3c6297 100644 --- a/src/core/textures/RenderTexture.js +++ b/src/core/textures/RenderTexture.js @@ -54,8 +54,8 @@ /* eslint-disable prefer-rest-params, no-console */ const width = arguments[1]; const height = arguments[2]; - const scaleMode = arguments[3] || 0; - const resolution = arguments[4] || 1; + const scaleMode = arguments[3]; + const resolution = arguments[4]; // we have an old render texture.. console.warn(`Please use RenderTexture.create(${width}, ${height}) instead of the ctor directly.`); 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 ca31456..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 @@ -459,7 +497,9 @@ if (frame.x + frame.width > this.baseTexture.width || frame.y + frame.height > this.baseTexture.height) { - throw new Error(`Texture Error: frame does not fit inside the base Texture dimensions ${this}`); + throw new Error('Texture Error: frame does not fit inside the base Texture dimensions: ' + + `X: ${frame.x} + ${frame.width} > ${this.baseTexture.width} ` + + `Y: ${frame.y} + ${frame.height} > ${this.baseTexture.height}`); } // this.valid = frame && frame.width && frame.height && this.baseTexture.source && this.baseTexture.hasLoaded; @@ -520,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. @@ -528,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 2a7497b..99e585a 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)); } /** @@ -360,3 +362,43 @@ * @private */ export const BaseTextureCache = {}; + +/** + * 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/core/utils/trimCanvas.js b/src/core/utils/trimCanvas.js new file mode 100644 index 0000000..5af6de0 --- /dev/null +++ b/src/core/utils/trimCanvas.js @@ -0,0 +1,83 @@ +/** + * Trim transparent borders from a canvas + * + * @memberof PIXI + * @function trimCanvas + * @private + * @param {HTMLCanvasElement} canvas - the canvas to trim + * @returns {object} Trim data + */ +export default function trimCanvas(canvas) +{ + // https://gist.github.com/remy/784508 + + let width = canvas.width; + let height = canvas.height; + + const context = canvas.getContext('2d'); + const imageData = context.getImageData(0, 0, width, height); + const pixels = imageData.data; + const len = pixels.length; + + const bound = { + top: null, + left: null, + right: null, + bottom: null, + }; + let i; + let x; + let y; + + for (i = 0; i < len; i += 4) + { + if (pixels[i + 3] !== 0) + { + x = (i / 4) % width; + y = ~~((i / 4) / width); + + if (bound.top === null) + { + bound.top = y; + } + + if (bound.left === null) + { + bound.left = x; + } + else if (x < bound.left) + { + bound.left = x; + } + + if (bound.right === null) + { + bound.right = x + 1; + } + else if (bound.right < x) + { + bound.right = x + 1; + } + + if (bound.bottom === null) + { + bound.bottom = y; + } + else if (bound.bottom < y) + { + bound.bottom = y; + } + } + } + + width = bound.right - bound.left; + height = bound.bottom - bound.top + 1; + + const data = context.getImageData(bound.left, bound.top, width, height); + + return { + height, + width, + data, + }; +} diff --git a/src/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/extras/TilingSprite.js b/src/extras/TilingSprite.js index f90d248..fe82451 100644 --- a/src/extras/TilingSprite.js +++ b/src/extras/TilingSprite.js @@ -220,20 +220,33 @@ transform.tx * resolution, transform.ty * resolution); - // TODO - this should be rolled into the setTransform above.. - context.scale(this.tileScale.x / baseTextureResolution, this.tileScale.y / baseTextureResolution); - - context.translate(modX + (this.anchor.x * -this._width), - modY + (this.anchor.y * -this._height)); - renderer.setBlendMode(this.blendMode); // fill the pattern! context.fillStyle = this._canvasPattern; - context.fillRect(-modX, - -modY, - this._width / this.tileScale.x * baseTextureResolution, - this._height / this.tileScale.y * baseTextureResolution); + + // TODO - this should be rolled into the setTransform above.. + context.scale(this.tileScale.x / baseTextureResolution, this.tileScale.y / baseTextureResolution); + + const anchorX = this.anchor.x * -this._width; + const anchorY = this.anchor.y * -this._height; + + if (this.uvRespectAnchor) + { + context.translate(modX, modY); + + context.fillRect(-modX + anchorX, -modY + anchorY, + this._width / this.tileScale.x * baseTextureResolution, + this._height / this.tileScale.y * baseTextureResolution); + } + else + { + context.translate(modX + anchorX, modY + anchorY); + + context.fillRect(-modX, -modY, + this._width / this.tileScale.x * baseTextureResolution, + this._height / this.tileScale.y * baseTextureResolution); + } } /** diff --git a/src/extras/cacheAsBitmap.js b/src/extras/cacheAsBitmap.js index 18043a7..5dd5640 100644 --- a/src/extras/cacheAsBitmap.js +++ b/src/extras/cacheAsBitmap.js @@ -41,6 +41,9 @@ * provide a performance benefit for complex static displayObjects. * To remove simply set this property to 'false' * + * IMPORTANT GOTCHA - make sure that all your textures are preloaded BEFORE setting this property to true + * as it will take a snapshot of what is currently there. If the textures have not loaded then they will not appear. + * * @member {boolean} * @memberof PIXI.DisplayObject# */ diff --git a/src/filters/blur/BlurFilter.js b/src/filters/blur/BlurFilter.js index 340fc58..1575db9 100644 --- a/src/filters/blur/BlurFilter.js +++ b/src/filters/blur/BlurFilter.js @@ -24,10 +24,9 @@ this.blurXFilter = new BlurXFilter(strength, quality, resolution, kernelSize); this.blurYFilter = new BlurYFilter(strength, quality, resolution, kernelSize); - this.resolution = 1; this.padding = 0; - this.resolution = resolution || 1; + this.resolution = resolution || core.settings.RESOLUTION; this.quality = quality || 4; this.blur = strength || 8; } diff --git a/src/filters/blur/BlurXFilter.js b/src/filters/blur/BlurXFilter.js index def99c3..0f48889 100644 --- a/src/filters/blur/BlurXFilter.js +++ b/src/filters/blur/BlurXFilter.js @@ -31,7 +31,7 @@ fragSrc ); - this.resolution = resolution || 1; + this.resolution = resolution || core.settings.RESOLUTION; this._quality = 0; diff --git a/src/filters/blur/BlurYFilter.js b/src/filters/blur/BlurYFilter.js index 638e0f0..6d489c7 100644 --- a/src/filters/blur/BlurYFilter.js +++ b/src/filters/blur/BlurYFilter.js @@ -31,7 +31,7 @@ fragSrc ); - this.resolution = resolution || 1; + this.resolution = resolution || core.settings.RESOLUTION; this._quality = 0; diff --git a/src/filters/colormatrix/ColorMatrixFilter.js b/src/filters/colormatrix/ColorMatrixFilter.js index 8509505..679f8ec 100644 --- a/src/filters/colormatrix/ColorMatrixFilter.js +++ b/src/filters/colormatrix/ColorMatrixFilter.js @@ -75,28 +75,28 @@ out[1] = (a[0] * b[1]) + (a[1] * b[6]) + (a[2] * b[11]) + (a[3] * b[16]); out[2] = (a[0] * b[2]) + (a[1] * b[7]) + (a[2] * b[12]) + (a[3] * b[17]); out[3] = (a[0] * b[3]) + (a[1] * b[8]) + (a[2] * b[13]) + (a[3] * b[18]); - out[4] = (a[0] * b[4]) + (a[1] * b[9]) + (a[2] * b[14]) + (a[3] * b[19]); + out[4] = (a[0] * b[4]) + (a[1] * b[9]) + (a[2] * b[14]) + (a[3] * b[19]) + a[4]; // Green Channel out[5] = (a[5] * b[0]) + (a[6] * b[5]) + (a[7] * b[10]) + (a[8] * b[15]); out[6] = (a[5] * b[1]) + (a[6] * b[6]) + (a[7] * b[11]) + (a[8] * b[16]); out[7] = (a[5] * b[2]) + (a[6] * b[7]) + (a[7] * b[12]) + (a[8] * b[17]); out[8] = (a[5] * b[3]) + (a[6] * b[8]) + (a[7] * b[13]) + (a[8] * b[18]); - out[9] = (a[5] * b[4]) + (a[6] * b[9]) + (a[7] * b[14]) + (a[8] * b[19]); + out[9] = (a[5] * b[4]) + (a[6] * b[9]) + (a[7] * b[14]) + (a[8] * b[19]) + a[9]; // Blue Channel out[10] = (a[10] * b[0]) + (a[11] * b[5]) + (a[12] * b[10]) + (a[13] * b[15]); out[11] = (a[10] * b[1]) + (a[11] * b[6]) + (a[12] * b[11]) + (a[13] * b[16]); out[12] = (a[10] * b[2]) + (a[11] * b[7]) + (a[12] * b[12]) + (a[13] * b[17]); out[13] = (a[10] * b[3]) + (a[11] * b[8]) + (a[12] * b[13]) + (a[13] * b[18]); - out[14] = (a[10] * b[4]) + (a[11] * b[9]) + (a[12] * b[14]) + (a[13] * b[19]); + out[14] = (a[10] * b[4]) + (a[11] * b[9]) + (a[12] * b[14]) + (a[13] * b[19]) + a[14]; // Alpha Channel out[15] = (a[15] * b[0]) + (a[16] * b[5]) + (a[17] * b[10]) + (a[18] * b[15]); out[16] = (a[15] * b[1]) + (a[16] * b[6]) + (a[17] * b[11]) + (a[18] * b[16]); out[17] = (a[15] * b[2]) + (a[16] * b[7]) + (a[17] * b[12]) + (a[18] * b[17]); out[18] = (a[15] * b[3]) + (a[16] * b[8]) + (a[17] * b[13]) + (a[18] * b[18]); - out[19] = (a[15] * b[4]) + (a[16] * b[9]) + (a[17] * b[14]) + (a[18] * b[19]); + out[19] = (a[15] * b[4]) + (a[16] * b[9]) + (a[17] * b[14]) + (a[18] * b[19]) + a[19]; return out; } diff --git a/src/filters/colormatrix/colorMatrix.frag b/src/filters/colormatrix/colorMatrix.frag index 3aaf2b3..c73c0e9 100644 --- a/src/filters/colormatrix/colorMatrix.frag +++ b/src/filters/colormatrix/colorMatrix.frag @@ -4,32 +4,38 @@ void main(void) { - vec4 c = texture2D(uSampler, vTextureCoord); + // Un-premultiply alpha before applying the color matrix. See issue #3539. + if (c.a > 0.0) { + c.rgb /= c.a; + } + vec4 result; + result.r = (m[0] * c.r); + result.r += (m[1] * c.g); + result.r += (m[2] * c.b); + result.r += (m[3] * c.a); + result.r += m[4]; - gl_FragColor.r = (m[0] * c.r); - gl_FragColor.r += (m[1] * c.g); - gl_FragColor.r += (m[2] * c.b); - gl_FragColor.r += (m[3] * c.a); - gl_FragColor.r += m[4] * c.a; + result.g = (m[5] * c.r); + result.g += (m[6] * c.g); + result.g += (m[7] * c.b); + result.g += (m[8] * c.a); + result.g += m[9]; - gl_FragColor.g = (m[5] * c.r); - gl_FragColor.g += (m[6] * c.g); - gl_FragColor.g += (m[7] * c.b); - gl_FragColor.g += (m[8] * c.a); - gl_FragColor.g += m[9] * c.a; + result.b = (m[10] * c.r); + result.b += (m[11] * c.g); + result.b += (m[12] * c.b); + result.b += (m[13] * c.a); + result.b += m[14]; - gl_FragColor.b = (m[10] * c.r); - gl_FragColor.b += (m[11] * c.g); - gl_FragColor.b += (m[12] * c.b); - gl_FragColor.b += (m[13] * c.a); - gl_FragColor.b += m[14] * c.a; + result.a = (m[15] * c.r); + result.a += (m[16] * c.g); + result.a += (m[17] * c.b); + result.a += (m[18] * c.a); + result.a += m[19]; - gl_FragColor.a = (m[15] * c.r); - gl_FragColor.a += (m[16] * c.g); - gl_FragColor.a += (m[17] * c.b); - gl_FragColor.a += (m[18] * c.a); - gl_FragColor.a += m[19] * c.a; + // Premultiply alpha again. + result.rgb *= result.a; -// gl_FragColor = vec4(m[0]); + gl_FragColor = result; } diff --git a/src/filters/displacement/DisplacementFilter.js b/src/filters/displacement/DisplacementFilter.js index 112de0b..d5355c3 100644 --- a/src/filters/displacement/DisplacementFilter.js +++ b/src/filters/displacement/DisplacementFilter.js @@ -36,7 +36,7 @@ this.maskMatrix = maskMatrix; this.uniforms.mapSampler = sprite.texture; - this.uniforms.filterMatrix = maskMatrix.toArray(true); + this.uniforms.filterMatrix = maskMatrix; this.uniforms.scale = { x: 1, y: 1 }; if (scale === null || scale === undefined) diff --git a/src/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/InteractionData.js b/src/interaction/InteractionData.js index 02d53a3..818da35 100644 --- a/src/interaction/InteractionData.js +++ b/src/interaction/InteractionData.js @@ -21,9 +21,9 @@ this.global = new core.Point(); /** - * The target Sprite that was interacted with + * The target DisplayObject that was interacted with * - * @member {PIXI.Sprite} + * @member {PIXI.DisplayObject} */ this.target = null; @@ -33,6 +33,13 @@ * @member {Event} */ this.originalEvent = null; + + /** + * Unique identifier for this interaction + * + * @member {number} + */ + this.identifier = null; } /** diff --git a/src/interaction/InteractionEvent.js b/src/interaction/InteractionEvent.js index 75b2966..dc554f9 100644 --- a/src/interaction/InteractionEvent.js +++ b/src/interaction/InteractionEvent.js @@ -58,7 +58,7 @@ } /** - * Prevents event from reaching any objects other than the current object. + * Resets the event. * * @private */ diff --git a/src/interaction/InteractionManager.js b/src/interaction/InteractionManager.js index 7d51134..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/src/prepare/BasePrepare.js b/src/prepare/BasePrepare.js index a2005f5..09eb5e5 100644 --- a/src/prepare/BasePrepare.js +++ b/src/prepare/BasePrepare.js @@ -20,7 +20,7 @@ * * @abstract * @class - * @memberof PIXI + * @memberof PIXI.prepare */ export default class BasePrepare { @@ -107,9 +107,9 @@ /** * Upload all the textures and graphics to the GPU. * - * @param {Function|PIXI.DisplayObject|PIXI.Container} item - Either - * the container or display object to search for items to upload or - * the callback function, if items have been added using `prepare.add`. + * @param {Function|PIXI.DisplayObject|PIXI.Container|PIXI.BaseTexture|PIXI.Texture|PIXI.Graphics|PIXI.Text} item - + * Either the container or display object to search for items to upload, the items to upload themselves, + * or the callback function, if items have been added using `prepare.add`. * @param {Function} [done] - Optional callback when all queued uploads have completed */ upload(item, done) @@ -236,7 +236,8 @@ /** * Manually add an item to the uploading queue. * - * @param {PIXI.DisplayObject|PIXI.Container|*} item - Object to add to the queue + * @param {PIXI.DisplayObject|PIXI.Container|PIXI.BaseTexture|PIXI.Texture|PIXI.Graphics|PIXI.Text|*} item - Object to + * add to the queue * @return {PIXI.CanvasPrepare} Instance of plugin for chaining. */ add(item) diff --git a/src/prepare/canvas/CanvasPrepare.js b/src/prepare/canvas/CanvasPrepare.js index 4b99d22..0224bb5 100644 --- a/src/prepare/canvas/CanvasPrepare.js +++ b/src/prepare/canvas/CanvasPrepare.js @@ -12,7 +12,8 @@ * An instance of this class is automatically created by default, and can be found at renderer.plugins.prepare * * @class - * @memberof PIXI + * @extends PIXI.prepare.BasePrepare + * @memberof PIXI.prepare */ export default class CanvasPrepare extends BasePrepare { diff --git a/package.json b/package.json index 1271fef..71ade15 100644 --- a/package.json +++ b/package.json @@ -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/scripts/release.js b/scripts/release.js index c4c8e36..156620c 100644 --- a/scripts/release.js +++ b/scripts/release.js @@ -12,7 +12,8 @@ 'dist/**/*', 'lib/**/*', 'src/**/*', - 'scripts/**/*', + 'scripts/*', + 'scripts/renders/*', 'test/**/*', '*.json', '*.md', diff --git a/scripts/renders/client.js b/scripts/renders/client.js index 16c9565..6faee34 100755 --- a/scripts/renders/client.js +++ b/scripts/renders/client.js @@ -1,6 +1,6 @@ 'use strict'; -require('../../bin/pixi'); +require('../../dist/pixi'); PIXI.utils.skipHello(); const fs = require('fs'); 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 bc730db..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. @@ -33,8 +33,11 @@ * need to call toDataUrl on the webgl context * @param {number} [options.resolution=1] - The resolution / device pixel ratio of the renderer, retina would be 2 * @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 @@ -43,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. */ @@ -73,7 +99,7 @@ */ stop() { - this.ticker.stop(); + this._ticker.stop(); } /** @@ -81,7 +107,7 @@ */ start() { - this.ticker.start(); + this._ticker.start(); } /** @@ -95,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 index 81e076b..78b17e6 100644 --- a/src/core/Shader.js +++ b/src/core/Shader.js @@ -1,9 +1,7 @@ import { GLShader } from 'pixi-gl-core'; import settings from './settings'; -const { PRECISION } = settings; - -function checkPrecision(src) +function checkPrecision(src, def) { if (src instanceof Array) { @@ -11,14 +9,14 @@ { const copy = src.slice(0); - copy.unshift(`precision ${PRECISION} float;`); + copy.unshift(`precision ${def} float;`); return copy; } } else if (src.substring(0, 9) !== 'precision') { - return `precision ${PRECISION} float;\n${src}`; + return `precision ${def} float;\n${src}`; } return src; @@ -42,6 +40,7 @@ */ constructor(gl, vertexSrc, fragmentSrc) { - super(gl, checkPrecision(vertexSrc), checkPrecision(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 3425b41..f9235e4 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -181,7 +181,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 b3792ef..17fde8d 100644 --- a/src/core/display/Container.js +++ b/src/core/display/Container.js @@ -73,13 +73,14 @@ } child.parent = this; - - // ensure a transform will be recalculated.. - this.transform._parentID = -1; - this._boundsID++; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.push(child); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(this.children.length - 1); child.emit('added', this); @@ -108,9 +109,14 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.splice(index, 0, child); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(index); child.emit('added', this); @@ -174,6 +180,7 @@ removeItems(this.children, currentIndex, 1); // remove from old position this.children.splice(index, 0, child); // add at new position + this.onChildrenChange(index); } @@ -220,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! @@ -244,9 +252,14 @@ { const child = this.getChildAt(index); + // ensure child transform will be recalculated.. child.parent = null; + child.transform._parentID = -1; removeItems(this.children, index, 1); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(index); child.emit('removed', this); @@ -275,8 +288,14 @@ for (let i = 0; i < removed.length; ++i) { removed[i].parent = null; + if (removed[i].transform) + { + removed[i].transform._parentID = -1; + } } + this._boundsID++; + this.onChildrenChange(beginIndex); for (let i = 0; i < removed.length; ++i) diff --git a/src/core/display/DisplayObject.js b/src/core/display/DisplayObject.js index 3affe36..4e6c77d 100644 --- a/src/core/display/DisplayObject.js +++ b/src/core/display/DisplayObject.js @@ -109,7 +109,7 @@ /** * The original, cached mask of the object * - * @member {PIXI.Rectangle} + * @member {PIXI.Graphics|PIXI.Sprite} * @private */ this._mask = null; diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index 95f2af4..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 ); @@ -1065,10 +1061,15 @@ canvasRenderer = new CanvasRenderer(); } - tempMatrix.tx = -bounds.x; - tempMatrix.ty = -bounds.y; + this.transform.updateLocalTransform(); + this.transform.localTransform.copy(tempMatrix); - canvasRenderer.render(this, canvasBuffer, false, tempMatrix); + tempMatrix.invert(); + + tempMatrix.tx -= bounds.x; + tempMatrix.ty -= bounds.y; + + canvasRenderer.render(this, canvasBuffer, true, tempMatrix); const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode); 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 e67680f..867dde2 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/buildCircle.js b/src/core/graphics/webgl/utils/buildCircle.js index 0400d13..abf5d02 100644 --- a/src/core/graphics/webgl/utils/buildCircle.js +++ b/src/core/graphics/webgl/utils/buildCircle.js @@ -33,6 +33,11 @@ height = circleData.height; } + if (width === 0 || height === 0) + { + return; + } + const totalSegs = Math.floor(30 * Math.sqrt(circleData.radius)) || Math.floor(15 * Math.sqrt(circleData.width + circleData.height)); diff --git a/src/core/graphics/webgl/utils/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 90155f9..f03fe15 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -30,6 +30,7 @@ export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; export { default as CanvasGraphicsRenderer } from './graphics/canvas/CanvasGraphicsRenderer'; +export { default as Spritesheet } from './textures/Spritesheet'; export { default as Texture } from './textures/Texture'; export { default as BaseTexture } from './textures/BaseTexture'; export { default as RenderTexture } from './textures/RenderTexture'; diff --git a/src/core/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 ccce680..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); } /** @@ -162,6 +162,9 @@ if (transform) { transform.copy(tempWt); + + // lets not forget to flag the parent transform as dirty... + this._tempDisplayObjectParent.transform._worldID = -1; } else { @@ -169,6 +172,7 @@ } displayObject.parent = this._tempDisplayObjectParent; + displayObject.updateTransform(); displayObject.parent = cacheParent; // displayObject.hitArea = //TODO add a temp hit area @@ -279,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/canvas/utils/CanvasRenderTarget.js b/src/core/renderers/canvas/utils/CanvasRenderTarget.js index 3de0aef..b409891 100644 --- a/src/core/renderers/canvas/utils/CanvasRenderTarget.js +++ b/src/core/renderers/canvas/utils/CanvasRenderTarget.js @@ -1,5 +1,4 @@ import settings from '../../../settings'; -const { RESOLUTION } = settings; /** * Creates a Canvas element of the given size. @@ -30,7 +29,7 @@ */ this.context = this.canvas.getContext('2d'); - this.resolution = resolution || RESOLUTION; + this.resolution = resolution || settings.RESOLUTION; this.resize(width, height); } 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 ce0e1c4..788ed6c 100644 --- a/src/core/renderers/webgl/WebGLRenderer.js +++ b/src/core/renderers/webgl/WebGLRenderer.js @@ -30,8 +30,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 @@ -49,10 +49,19 @@ * enable this if you need to call toDataUrl on the webgl context. * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when * rendering, stopping pixel interpolation. + * @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; + + if (this.legacy) + { + glCore.VertexArrayObject.FORCE_NATIVE = true; + } /** * The type of this renderer as a standardised const @@ -229,7 +238,7 @@ this.emit('context', gl); // setup the width/height properties and gl viewport - this.resize(this.width, this.height); + this.resize(this.screen.width, this.screen.height); } /** @@ -323,16 +332,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) { @@ -376,6 +385,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 @@ -440,9 +469,10 @@ * Changes the current shader to the one given in parameter * * @param {PIXI.Shader} shader - the new shader + * @param {boolean} [autoProject=true] - Whether automatically set the projection matrix * @return {PIXI.WebGLRenderer} Returns itself. */ - bindShader(shader) + bindShader(shader, autoProject) { // TODO cache if (this._activeShader !== shader) @@ -450,8 +480,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 bbab3b3..944f41d 100644 --- a/src/core/renderers/webgl/filters/Filter.js +++ b/src/core/renderers/webgl/filters/Filter.js @@ -1,6 +1,7 @@ import extractUniformsFromSrc from './extractUniformsFromSrc'; import { uid } from '../../../utils'; import { BLEND_MODES } from '../../../const'; +import settings from '../../../settings'; const SOURCE_KEY_MAP = {}; @@ -10,7 +11,7 @@ * @memberof PIXI * @extends PIXI.Shader */ -class Filter +export default class Filter { /** * @param {string} [vertexSrc] - The source of the vertex shader. @@ -35,8 +36,6 @@ this.blendMode = BLEND_MODES.NORMAL; - // pull out the vertex and shader uniforms if they are not specified.. - // currently this does not extract structs only default types this.uniformData = uniforms || extractUniformsFromSrc(this.vertexSrc, this.fragmentSrc, 'projectionMatrix|uSampler'); /** @@ -80,7 +79,7 @@ * * @member {number} */ - this.resolution = 1; + this.resolution = settings.RESOLUTION; /** * If enabled is true the filter is applied, if false it will not. @@ -97,8 +96,11 @@ * @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 { // --- // // this.uniforms.filterMatrix = filterManager.calculateSpriteMatrix(tempMatrix, window.panda ); @@ -170,5 +172,3 @@ ].join('\n'); } } - -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 dc7072a..6c7422d 100644 --- a/src/core/renderers/webgl/managers/FilterManager.js +++ b/src/core/renderers/webgl/managers/FilterManager.js @@ -154,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 @@ -176,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; @@ -184,7 +184,7 @@ flop = t; } - filters[i].apply(this, flip, lastState.renderTarget, true); + filters[i].apply(this, flip, lastState.renderTarget, false, currentState); this.freePotRenderTarget(flip); this.freePotRenderTarget(flop); @@ -291,9 +291,13 @@ let textureCount = 1; let currentState; - if (shader.uniforms.data.filterArea) + // 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; @@ -306,9 +310,9 @@ // use this to clamp displaced texture coords so they belong to filterArea // see displacementFilter fragment shader for an example - if (shader.uniforms.data.filterClamp) + if (shader.uniforms.filterClamp) { - currentState = this.filterData.stack[this.filterData.index]; + currentState = currentState || this.filterData.stack[this.filterData.index]; const filterClamp = shader.uniforms.filterClamp; @@ -485,7 +489,7 @@ */ destroy() { - this.shaderCache = []; + this.shaderCache = {}; this.emptyPool(); } diff --git a/src/core/renderers/webgl/utils/RenderTarget.js b/src/core/renderers/webgl/utils/RenderTarget.js index afe51ab..6d2467f 100644 --- a/src/core/renderers/webgl/utils/RenderTarget.js +++ b/src/core/renderers/webgl/utils/RenderTarget.js @@ -124,7 +124,7 @@ * @default PIXI.settings.SCALE_MODE * @see PIXI.SCALE_MODES */ - this.scaleMode = scaleMode || settings.SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; /** * Whether this object is the root element or not 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 36ea5bb..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; @@ -346,8 +346,8 @@ /** * Gets the local bounds of the sprite object. * - * @param {Rectangle} rect - The output rectangle. - * @return {Rectangle} The bounds. + * @param {PIXI.Rectangle} rect - The output rectangle. + * @return {PIXI.Rectangle} The bounds. */ getLocalBounds(rect) { @@ -440,7 +440,7 @@ * * @static * @param {number|string|PIXI.BaseTexture|HTMLCanvasElement|HTMLVideoElement} source Source to create texture from - * @return {PIXI.Texture} 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 e77b39a..90b08c0 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++) @@ -103,11 +102,18 @@ { const gl = this.renderer.gl; - // step 1: first check max textures the GPU can handle. - this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); + if (this.renderer.legacy) + { + this.MAX_TEXTURES = 1; + } + else + { + // step 1: first check max textures the GPU can handle. + this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); - // step 2: check the maximum number of if statements the shader can have too.. - this.MAX_TEXTURES = checkMaxIfStatmentsInShader(this.MAX_TEXTURES, gl); + // step 2: check the maximum number of if statements the shader can have too.. + this.MAX_TEXTURES = checkMaxIfStatmentsInShader(this.MAX_TEXTURES, gl); + } const shader = this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); @@ -130,8 +136,12 @@ .addIndex(this.indexBuffer) .addAttribute(this.vertexBuffers[i], shader.attributes.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) .addAttribute(this.vertexBuffers[i], shader.attributes.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) - .addAttribute(this.vertexBuffers[i], shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4) - .addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + .addAttribute(this.vertexBuffers[i], shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); + + if (shader.attributes.aTextureId) + { + this.vaos[i].addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + } /* eslint-enable max-len */ } @@ -378,8 +388,12 @@ .addIndex(this.indexBuffer) .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) - .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4) - .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); + + if (this.shader.attributes.aTextureId) + { + this.vaos[this.vertexCount].addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + } /* eslint-enable max-len */ } diff --git a/src/core/text/Text.js b/src/core/text/Text.js index 013625a..cec8c32 100644 --- a/src/core/text/Text.js +++ b/src/core/text/Text.js @@ -6,6 +6,7 @@ import { TEXT_GRADIENT } from '../const'; import settings from '../settings'; import TextStyle from './TextStyle'; +import trimCanvas from '../utils/trimCanvas'; const defaultDestroyOptions = { texture: true, @@ -57,7 +58,7 @@ /** * The canvas 2d context that everything is drawn with - * @member {HTMLCanvasElement} + * @member {CanvasRenderingContext2D} */ this.context = this.canvas.getContext('2d'); @@ -189,10 +190,12 @@ if (style.dropShadow) { + this.context.shadowBlur = style.dropShadowBlur; + this.context.globalAlpha = style.dropShadowAlpha; + if (style.dropShadowBlur > 0) { this.context.shadowColor = style.dropShadowColor; - this.context.shadowBlur = style.dropShadowBlur; } else { @@ -237,6 +240,10 @@ } } + // reset the shadow blur and alpha that was set by the drop shadow, for the regular text + this.context.shadowBlur = 0; + this.context.globalAlpha = 1; + // set canvas text styles this.context.fillStyle = this._generateFillStyle(style, lines); @@ -326,6 +333,15 @@ */ updateTexture() { + if (this._style.trim) + { + const trimmed = trimCanvas(this.canvas); + + this.canvas.width = trimmed.width; + this.canvas.height = trimmed.height; + this.context.putImageData(trimmed.data, 0, 0); + } + const texture = this._texture; const style = this._style; @@ -475,6 +491,19 @@ } /** + * Gets the local bounds of the text object. + * + * @param {Rectangle} rect - The output rectangle. + * @return {Rectangle} The bounds. + */ + getLocalBounds(rect) + { + this.updateText(true); + + return super.getLocalBounds.call(this, rect); + } + + /** * calculates the bounds of the Text as a rectangle. The bounds calculation takes the worldTransform into account. */ _calculateBounds() @@ -525,6 +554,29 @@ const width = this.canvas.width / this.resolution; const height = this.canvas.height / this.resolution; + // make a copy of the style settings, so we can manipulate them later + const fill = style.fill.slice(); + const fillGradientStops = style.fillGradientStops.slice(); + + // wanting to evenly distribute the fills. So an array of 4 colours should give fills of 0.25, 0.5 and 0.75 + if (!fillGradientStops.length) + { + const lengthPlus1 = fill.length + 1; + + for (let i = 1; i < lengthPlus1; ++i) + { + fillGradientStops.push(i / lengthPlus1); + } + } + + // stop the bleeding of the last gradient on the line above to the top gradient of the this line + // by hard defining the first gradient colour at point 0, and last gradient colour at point 1 + fill.unshift(style.fill[0]); + fillGradientStops.unshift(0); + + fill.push(style.fill[style.fill.length - 1]); + fillGradientStops.push(1); + if (style.fillGradientType === TEXT_GRADIENT.LINEAR_VERTICAL) { // start the gradient at the top center of the canvas, and end at the bottom middle of the canvas @@ -532,15 +584,22 @@ // we need to repeat the gradient so that each individual line of text has the same vertical gradient effect // ['#FF0000', '#00FF00', '#0000FF'] over 2 lines would create stops at 0.125, 0.25, 0.375, 0.625, 0.75, 0.875 - totalIterations = (style.fill.length + 1) * lines.length; + totalIterations = (fill.length + 1) * lines.length; currentIteration = 0; for (let i = 0; i < lines.length; i++) { currentIteration += 1; - for (let j = 0; j < style.fill.length; j++) + for (let j = 0; j < fill.length; j++) { - stop = (currentIteration / totalIterations); - gradient.addColorStop(stop, style.fill[j]); + if (fillGradientStops[j]) + { + stop = (fillGradientStops[j] / lines.length) + (i / lines.length); + } + else + { + stop = currentIteration / totalIterations; + } + gradient.addColorStop(stop, fill[j]); currentIteration++; } } @@ -552,13 +611,20 @@ // can just evenly space out the gradients in this case, as multiple lines makes no difference // to an even left to right gradient - totalIterations = style.fill.length + 1; + totalIterations = fill.length + 1; currentIteration = 1; - for (let i = 0; i < style.fill.length; i++) + for (let i = 0; i < fill.length; i++) { - stop = currentIteration / totalIterations; - gradient.addColorStop(stop, style.fill[i]); + if (fillGradientStops[i]) + { + stop = fillGradientStops[i]; + } + else + { + stop = currentIteration / totalIterations; + } + gradient.addColorStop(stop, fill[i]); currentIteration++; } } @@ -569,7 +635,7 @@ /** * Destroys this text object. * Note* Unlike a Sprite, a Text object will automatically destroy its baseTexture and texture as - * the majorety of the time the texture will not be shared with any other Sprites. + * the majority of the time the texture will not be shared with any other Sprites. * * @param {object|boolean} [options] - Options parameter. A boolean will act as if all options * have been set to that value @@ -680,7 +746,7 @@ set text(text) // eslint-disable-line require-jsdoc { - text = String(text || ' '); + text = String(text === '' || text === null || text === undefined ? ' ' : text); if (this._text === text) { diff --git a/src/core/text/TextStyle.js b/src/core/text/TextStyle.js index 3b25528..890950b 100644 --- a/src/core/text/TextStyle.js +++ b/src/core/text/TextStyle.js @@ -8,12 +8,14 @@ align: 'left', breakWords: false, dropShadow: false, + dropShadowAlpha: 1, dropShadowAngle: Math.PI / 6, dropShadowBlur: 0, dropShadowColor: '#000000', dropShadowDistance: 5, fill: 'black', fillGradientType: TEXT_GRADIENT.LINEAR_VERTICAL, + fillGradientStops: [], fontFamily: 'Arial', fontSize: 26, fontStyle: 'normal', @@ -27,6 +29,7 @@ stroke: 'black', strokeThickness: 0, textBaseline: 'alphabetic', + trim: false, wordWrap: false, wordWrapWidth: 100, }; @@ -47,6 +50,7 @@ * @param {boolean} [style.breakWords=false] - Indicates if lines can be wrapped within words, it * needs wordWrap to be set to true * @param {boolean} [style.dropShadow=false] - Set a drop shadow for the text + * @param {number} [style.dropShadowAlpha=1] - Set alpha for the drop shadow * @param {number} [style.dropShadowAngle=Math.PI/6] - Set a angle of the drop shadow * @param {number} [style.dropShadowBlur=0] - Set a shadow blur radius * @param {string} [style.dropShadowColor='#000000'] - A fill style to be used on the dropshadow e.g 'red', '#00FF00' @@ -55,8 +59,10 @@ * fillstyle that will be used on the text e.g 'red', '#00FF00'. Can be an array to create a gradient * eg ['#000000','#FFFFFF'] * {@link https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/fillStyle|MDN} - * @param {number} [style.fillGradientType=PIXI.TEXT_GRADIENT.LINEAR_VERTICAL] - If fills styles are - * supplied, this can change the type/direction of the gradient. See {@link PIXI.TEXT_GRADIENT} for possible values + * @param {number} [style.fillGradientType=PIXI.TEXT_GRADIENT.LINEAR_VERTICAL] - If fill is an array of colours + * to create a gradient, this can change the type/direction of the gradient. See {@link PIXI.TEXT_GRADIENT} + * @param {number[]} [style.fillGradientStops] - If fill is an array of colours to create a gradient, this array can set + * the stop points (numbers between 0 and 1) for the color, overriding the default behaviour of evenly spacing them. * @param {string|string[]} [style.fontFamily='Arial'] - The font family * @param {number|string} [style.fontSize=26] - The font size (as a number it converts to px, but as a string, * equivalents are '26px','20pt','160%' or '1.6em') @@ -76,6 +82,7 @@ * e.g 'blue', '#FCFF00' * @param {number} [style.strokeThickness=0] - A number that represents the thickness of the stroke. * Default is 0 (no stroke) + * @param {boolean} [style.trim=false] - Trim transparent borders * @param {string} [style.textBaseline='alphabetic'] - The baseline of the text that is rendered. * @param {boolean} [style.wordWrap=false] - Indicates if word wrap should be used * @param {number} [style.wordWrapWidth=100] - The width at which text will wrap, it needs wordWrap to be set to true @@ -152,6 +159,19 @@ } } + get dropShadowAlpha() + { + return this._dropShadowAlpha; + } + set dropShadowAlpha(dropShadowAlpha) + { + if (this._dropShadowAlpha !== dropShadowAlpha) + { + this._dropShadowAlpha = dropShadowAlpha; + this.styleID++; + } + } + get dropShadowAngle() { return this._dropShadowAngle; @@ -232,6 +252,19 @@ } } + get fillGradientStops() + { + return this._fillGradientStops; + } + set fillGradientStops(fillGradientStops) + { + if (!areArraysEqual(this._fillGradientStops,fillGradientStops)) + { + this._fillGradientStops = fillGradientStops; + this.styleID++; + } + } + get fontFamily() { return this._fontFamily; @@ -402,6 +435,19 @@ } } + get trim() + { + return this._trim; + } + set trim(trim) + { + if (this._trim !== trim) + { + this._trim = trim; + this.styleID++; + } + } + get wordWrap() { return this._wordWrap; @@ -475,3 +521,34 @@ return color; } } + +/** + * Utility function to convert hexadecimal colors to strings, and simply return the color if it's a string. + * This version can also convert array of colors + * + * @param {Array} array1 First array to compare + * @param {Array} array2 Second array to compare + * @return {boolean} Do the arrays contain the same values in the same order + */ +function areArraysEqual(array1, array2) +{ + if (!Array.isArray(array1) || !Array.isArray(array2)) + { + return false; + } + + if (array1.length !== array2.length) + { + return false; + } + + for (let i = 0; i < array1.length; ++i) + { + if (array1[i] !== array2[i]) + { + return false; + } + } + + return true; +} diff --git a/src/core/textures/BaseRenderTexture.js b/src/core/textures/BaseRenderTexture.js index 5d50146..aa727f0 100644 --- a/src/core/textures/BaseRenderTexture.js +++ b/src/core/textures/BaseRenderTexture.js @@ -1,8 +1,6 @@ import BaseTexture from './BaseTexture'; import settings from '../settings'; -const { RESOLUTION, SCALE_MODE } = settings; - /** * A BaseRenderTexture is a special texture that allows any Pixi display object to be rendered to it. * @@ -54,7 +52,7 @@ { super(null, scaleMode); - this.resolution = resolution || RESOLUTION; + this.resolution = resolution || settings.RESOLUTION; this.width = width; this.height = height; @@ -62,7 +60,7 @@ this.realWidth = this.width * this.resolution; this.realHeight = this.height * this.resolution; - this.scaleMode = scaleMode || SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; this.hasLoaded = true; /** diff --git a/src/core/textures/BaseTexture.js b/src/core/textures/BaseTexture.js index 676dca3..aeccf39 100644 --- a/src/core/textures/BaseTexture.js +++ b/src/core/textures/BaseTexture.js @@ -77,7 +77,7 @@ * @default PIXI.settings.SCALE_MODE * @see PIXI.SCALE_MODES */ - this.scaleMode = scaleMode || settings.SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; /** * Set to true once the base texture has successfully loaded. @@ -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/RenderTexture.js b/src/core/textures/RenderTexture.js index 4c5034f..b3c6297 100644 --- a/src/core/textures/RenderTexture.js +++ b/src/core/textures/RenderTexture.js @@ -54,8 +54,8 @@ /* eslint-disable prefer-rest-params, no-console */ const width = arguments[1]; const height = arguments[2]; - const scaleMode = arguments[3] || 0; - const resolution = arguments[4] || 1; + const scaleMode = arguments[3]; + const resolution = arguments[4]; // we have an old render texture.. console.warn(`Please use RenderTexture.create(${width}, ${height}) instead of the ctor directly.`); 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 ca31456..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 @@ -459,7 +497,9 @@ if (frame.x + frame.width > this.baseTexture.width || frame.y + frame.height > this.baseTexture.height) { - throw new Error(`Texture Error: frame does not fit inside the base Texture dimensions ${this}`); + throw new Error('Texture Error: frame does not fit inside the base Texture dimensions: ' + + `X: ${frame.x} + ${frame.width} > ${this.baseTexture.width} ` + + `Y: ${frame.y} + ${frame.height} > ${this.baseTexture.height}`); } // this.valid = frame && frame.width && frame.height && this.baseTexture.source && this.baseTexture.hasLoaded; @@ -520,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. @@ -528,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 2a7497b..99e585a 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)); } /** @@ -360,3 +362,43 @@ * @private */ export const BaseTextureCache = {}; + +/** + * 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/core/utils/trimCanvas.js b/src/core/utils/trimCanvas.js new file mode 100644 index 0000000..5af6de0 --- /dev/null +++ b/src/core/utils/trimCanvas.js @@ -0,0 +1,83 @@ +/** + * Trim transparent borders from a canvas + * + * @memberof PIXI + * @function trimCanvas + * @private + * @param {HTMLCanvasElement} canvas - the canvas to trim + * @returns {object} Trim data + */ +export default function trimCanvas(canvas) +{ + // https://gist.github.com/remy/784508 + + let width = canvas.width; + let height = canvas.height; + + const context = canvas.getContext('2d'); + const imageData = context.getImageData(0, 0, width, height); + const pixels = imageData.data; + const len = pixels.length; + + const bound = { + top: null, + left: null, + right: null, + bottom: null, + }; + let i; + let x; + let y; + + for (i = 0; i < len; i += 4) + { + if (pixels[i + 3] !== 0) + { + x = (i / 4) % width; + y = ~~((i / 4) / width); + + if (bound.top === null) + { + bound.top = y; + } + + if (bound.left === null) + { + bound.left = x; + } + else if (x < bound.left) + { + bound.left = x; + } + + if (bound.right === null) + { + bound.right = x + 1; + } + else if (bound.right < x) + { + bound.right = x + 1; + } + + if (bound.bottom === null) + { + bound.bottom = y; + } + else if (bound.bottom < y) + { + bound.bottom = y; + } + } + } + + width = bound.right - bound.left; + height = bound.bottom - bound.top + 1; + + const data = context.getImageData(bound.left, bound.top, width, height); + + return { + height, + width, + data, + }; +} diff --git a/src/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/extras/TilingSprite.js b/src/extras/TilingSprite.js index f90d248..fe82451 100644 --- a/src/extras/TilingSprite.js +++ b/src/extras/TilingSprite.js @@ -220,20 +220,33 @@ transform.tx * resolution, transform.ty * resolution); - // TODO - this should be rolled into the setTransform above.. - context.scale(this.tileScale.x / baseTextureResolution, this.tileScale.y / baseTextureResolution); - - context.translate(modX + (this.anchor.x * -this._width), - modY + (this.anchor.y * -this._height)); - renderer.setBlendMode(this.blendMode); // fill the pattern! context.fillStyle = this._canvasPattern; - context.fillRect(-modX, - -modY, - this._width / this.tileScale.x * baseTextureResolution, - this._height / this.tileScale.y * baseTextureResolution); + + // TODO - this should be rolled into the setTransform above.. + context.scale(this.tileScale.x / baseTextureResolution, this.tileScale.y / baseTextureResolution); + + const anchorX = this.anchor.x * -this._width; + const anchorY = this.anchor.y * -this._height; + + if (this.uvRespectAnchor) + { + context.translate(modX, modY); + + context.fillRect(-modX + anchorX, -modY + anchorY, + this._width / this.tileScale.x * baseTextureResolution, + this._height / this.tileScale.y * baseTextureResolution); + } + else + { + context.translate(modX + anchorX, modY + anchorY); + + context.fillRect(-modX, -modY, + this._width / this.tileScale.x * baseTextureResolution, + this._height / this.tileScale.y * baseTextureResolution); + } } /** diff --git a/src/extras/cacheAsBitmap.js b/src/extras/cacheAsBitmap.js index 18043a7..5dd5640 100644 --- a/src/extras/cacheAsBitmap.js +++ b/src/extras/cacheAsBitmap.js @@ -41,6 +41,9 @@ * provide a performance benefit for complex static displayObjects. * To remove simply set this property to 'false' * + * IMPORTANT GOTCHA - make sure that all your textures are preloaded BEFORE setting this property to true + * as it will take a snapshot of what is currently there. If the textures have not loaded then they will not appear. + * * @member {boolean} * @memberof PIXI.DisplayObject# */ diff --git a/src/filters/blur/BlurFilter.js b/src/filters/blur/BlurFilter.js index 340fc58..1575db9 100644 --- a/src/filters/blur/BlurFilter.js +++ b/src/filters/blur/BlurFilter.js @@ -24,10 +24,9 @@ this.blurXFilter = new BlurXFilter(strength, quality, resolution, kernelSize); this.blurYFilter = new BlurYFilter(strength, quality, resolution, kernelSize); - this.resolution = 1; this.padding = 0; - this.resolution = resolution || 1; + this.resolution = resolution || core.settings.RESOLUTION; this.quality = quality || 4; this.blur = strength || 8; } diff --git a/src/filters/blur/BlurXFilter.js b/src/filters/blur/BlurXFilter.js index def99c3..0f48889 100644 --- a/src/filters/blur/BlurXFilter.js +++ b/src/filters/blur/BlurXFilter.js @@ -31,7 +31,7 @@ fragSrc ); - this.resolution = resolution || 1; + this.resolution = resolution || core.settings.RESOLUTION; this._quality = 0; diff --git a/src/filters/blur/BlurYFilter.js b/src/filters/blur/BlurYFilter.js index 638e0f0..6d489c7 100644 --- a/src/filters/blur/BlurYFilter.js +++ b/src/filters/blur/BlurYFilter.js @@ -31,7 +31,7 @@ fragSrc ); - this.resolution = resolution || 1; + this.resolution = resolution || core.settings.RESOLUTION; this._quality = 0; diff --git a/src/filters/colormatrix/ColorMatrixFilter.js b/src/filters/colormatrix/ColorMatrixFilter.js index 8509505..679f8ec 100644 --- a/src/filters/colormatrix/ColorMatrixFilter.js +++ b/src/filters/colormatrix/ColorMatrixFilter.js @@ -75,28 +75,28 @@ out[1] = (a[0] * b[1]) + (a[1] * b[6]) + (a[2] * b[11]) + (a[3] * b[16]); out[2] = (a[0] * b[2]) + (a[1] * b[7]) + (a[2] * b[12]) + (a[3] * b[17]); out[3] = (a[0] * b[3]) + (a[1] * b[8]) + (a[2] * b[13]) + (a[3] * b[18]); - out[4] = (a[0] * b[4]) + (a[1] * b[9]) + (a[2] * b[14]) + (a[3] * b[19]); + out[4] = (a[0] * b[4]) + (a[1] * b[9]) + (a[2] * b[14]) + (a[3] * b[19]) + a[4]; // Green Channel out[5] = (a[5] * b[0]) + (a[6] * b[5]) + (a[7] * b[10]) + (a[8] * b[15]); out[6] = (a[5] * b[1]) + (a[6] * b[6]) + (a[7] * b[11]) + (a[8] * b[16]); out[7] = (a[5] * b[2]) + (a[6] * b[7]) + (a[7] * b[12]) + (a[8] * b[17]); out[8] = (a[5] * b[3]) + (a[6] * b[8]) + (a[7] * b[13]) + (a[8] * b[18]); - out[9] = (a[5] * b[4]) + (a[6] * b[9]) + (a[7] * b[14]) + (a[8] * b[19]); + out[9] = (a[5] * b[4]) + (a[6] * b[9]) + (a[7] * b[14]) + (a[8] * b[19]) + a[9]; // Blue Channel out[10] = (a[10] * b[0]) + (a[11] * b[5]) + (a[12] * b[10]) + (a[13] * b[15]); out[11] = (a[10] * b[1]) + (a[11] * b[6]) + (a[12] * b[11]) + (a[13] * b[16]); out[12] = (a[10] * b[2]) + (a[11] * b[7]) + (a[12] * b[12]) + (a[13] * b[17]); out[13] = (a[10] * b[3]) + (a[11] * b[8]) + (a[12] * b[13]) + (a[13] * b[18]); - out[14] = (a[10] * b[4]) + (a[11] * b[9]) + (a[12] * b[14]) + (a[13] * b[19]); + out[14] = (a[10] * b[4]) + (a[11] * b[9]) + (a[12] * b[14]) + (a[13] * b[19]) + a[14]; // Alpha Channel out[15] = (a[15] * b[0]) + (a[16] * b[5]) + (a[17] * b[10]) + (a[18] * b[15]); out[16] = (a[15] * b[1]) + (a[16] * b[6]) + (a[17] * b[11]) + (a[18] * b[16]); out[17] = (a[15] * b[2]) + (a[16] * b[7]) + (a[17] * b[12]) + (a[18] * b[17]); out[18] = (a[15] * b[3]) + (a[16] * b[8]) + (a[17] * b[13]) + (a[18] * b[18]); - out[19] = (a[15] * b[4]) + (a[16] * b[9]) + (a[17] * b[14]) + (a[18] * b[19]); + out[19] = (a[15] * b[4]) + (a[16] * b[9]) + (a[17] * b[14]) + (a[18] * b[19]) + a[19]; return out; } diff --git a/src/filters/colormatrix/colorMatrix.frag b/src/filters/colormatrix/colorMatrix.frag index 3aaf2b3..c73c0e9 100644 --- a/src/filters/colormatrix/colorMatrix.frag +++ b/src/filters/colormatrix/colorMatrix.frag @@ -4,32 +4,38 @@ void main(void) { - vec4 c = texture2D(uSampler, vTextureCoord); + // Un-premultiply alpha before applying the color matrix. See issue #3539. + if (c.a > 0.0) { + c.rgb /= c.a; + } + vec4 result; + result.r = (m[0] * c.r); + result.r += (m[1] * c.g); + result.r += (m[2] * c.b); + result.r += (m[3] * c.a); + result.r += m[4]; - gl_FragColor.r = (m[0] * c.r); - gl_FragColor.r += (m[1] * c.g); - gl_FragColor.r += (m[2] * c.b); - gl_FragColor.r += (m[3] * c.a); - gl_FragColor.r += m[4] * c.a; + result.g = (m[5] * c.r); + result.g += (m[6] * c.g); + result.g += (m[7] * c.b); + result.g += (m[8] * c.a); + result.g += m[9]; - gl_FragColor.g = (m[5] * c.r); - gl_FragColor.g += (m[6] * c.g); - gl_FragColor.g += (m[7] * c.b); - gl_FragColor.g += (m[8] * c.a); - gl_FragColor.g += m[9] * c.a; + result.b = (m[10] * c.r); + result.b += (m[11] * c.g); + result.b += (m[12] * c.b); + result.b += (m[13] * c.a); + result.b += m[14]; - gl_FragColor.b = (m[10] * c.r); - gl_FragColor.b += (m[11] * c.g); - gl_FragColor.b += (m[12] * c.b); - gl_FragColor.b += (m[13] * c.a); - gl_FragColor.b += m[14] * c.a; + result.a = (m[15] * c.r); + result.a += (m[16] * c.g); + result.a += (m[17] * c.b); + result.a += (m[18] * c.a); + result.a += m[19]; - gl_FragColor.a = (m[15] * c.r); - gl_FragColor.a += (m[16] * c.g); - gl_FragColor.a += (m[17] * c.b); - gl_FragColor.a += (m[18] * c.a); - gl_FragColor.a += m[19] * c.a; + // Premultiply alpha again. + result.rgb *= result.a; -// gl_FragColor = vec4(m[0]); + gl_FragColor = result; } diff --git a/src/filters/displacement/DisplacementFilter.js b/src/filters/displacement/DisplacementFilter.js index 112de0b..d5355c3 100644 --- a/src/filters/displacement/DisplacementFilter.js +++ b/src/filters/displacement/DisplacementFilter.js @@ -36,7 +36,7 @@ this.maskMatrix = maskMatrix; this.uniforms.mapSampler = sprite.texture; - this.uniforms.filterMatrix = maskMatrix.toArray(true); + this.uniforms.filterMatrix = maskMatrix; this.uniforms.scale = { x: 1, y: 1 }; if (scale === null || scale === undefined) diff --git a/src/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/InteractionData.js b/src/interaction/InteractionData.js index 02d53a3..818da35 100644 --- a/src/interaction/InteractionData.js +++ b/src/interaction/InteractionData.js @@ -21,9 +21,9 @@ this.global = new core.Point(); /** - * The target Sprite that was interacted with + * The target DisplayObject that was interacted with * - * @member {PIXI.Sprite} + * @member {PIXI.DisplayObject} */ this.target = null; @@ -33,6 +33,13 @@ * @member {Event} */ this.originalEvent = null; + + /** + * Unique identifier for this interaction + * + * @member {number} + */ + this.identifier = null; } /** diff --git a/src/interaction/InteractionEvent.js b/src/interaction/InteractionEvent.js index 75b2966..dc554f9 100644 --- a/src/interaction/InteractionEvent.js +++ b/src/interaction/InteractionEvent.js @@ -58,7 +58,7 @@ } /** - * Prevents event from reaching any objects other than the current object. + * Resets the event. * * @private */ diff --git a/src/interaction/InteractionManager.js b/src/interaction/InteractionManager.js index 7d51134..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/src/prepare/BasePrepare.js b/src/prepare/BasePrepare.js index a2005f5..09eb5e5 100644 --- a/src/prepare/BasePrepare.js +++ b/src/prepare/BasePrepare.js @@ -20,7 +20,7 @@ * * @abstract * @class - * @memberof PIXI + * @memberof PIXI.prepare */ export default class BasePrepare { @@ -107,9 +107,9 @@ /** * Upload all the textures and graphics to the GPU. * - * @param {Function|PIXI.DisplayObject|PIXI.Container} item - Either - * the container or display object to search for items to upload or - * the callback function, if items have been added using `prepare.add`. + * @param {Function|PIXI.DisplayObject|PIXI.Container|PIXI.BaseTexture|PIXI.Texture|PIXI.Graphics|PIXI.Text} item - + * Either the container or display object to search for items to upload, the items to upload themselves, + * or the callback function, if items have been added using `prepare.add`. * @param {Function} [done] - Optional callback when all queued uploads have completed */ upload(item, done) @@ -236,7 +236,8 @@ /** * Manually add an item to the uploading queue. * - * @param {PIXI.DisplayObject|PIXI.Container|*} item - Object to add to the queue + * @param {PIXI.DisplayObject|PIXI.Container|PIXI.BaseTexture|PIXI.Texture|PIXI.Graphics|PIXI.Text|*} item - Object to + * add to the queue * @return {PIXI.CanvasPrepare} Instance of plugin for chaining. */ add(item) diff --git a/src/prepare/canvas/CanvasPrepare.js b/src/prepare/canvas/CanvasPrepare.js index 4b99d22..0224bb5 100644 --- a/src/prepare/canvas/CanvasPrepare.js +++ b/src/prepare/canvas/CanvasPrepare.js @@ -12,7 +12,8 @@ * An instance of this class is automatically created by default, and can be found at renderer.plugins.prepare * * @class - * @memberof PIXI + * @extends PIXI.prepare.BasePrepare + * @memberof PIXI.prepare */ export default class CanvasPrepare extends BasePrepare { diff --git a/src/prepare/webgl/WebGLPrepare.js b/src/prepare/webgl/WebGLPrepare.js index 1ef7467..eb7023f 100644 --- a/src/prepare/webgl/WebGLPrepare.js +++ b/src/prepare/webgl/WebGLPrepare.js @@ -7,7 +7,8 @@ * An instance of this class is automatically created by default, and can be found at renderer.plugins.prepare * * @class - * @memberof PIXI + * @extends PIXI.prepare.BasePrepare + * @memberof PIXI.prepare */ export default class WebGLPrepare extends BasePrepare { diff --git a/package.json b/package.json index 1271fef..71ade15 100644 --- a/package.json +++ b/package.json @@ -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/scripts/release.js b/scripts/release.js index c4c8e36..156620c 100644 --- a/scripts/release.js +++ b/scripts/release.js @@ -12,7 +12,8 @@ 'dist/**/*', 'lib/**/*', 'src/**/*', - 'scripts/**/*', + 'scripts/*', + 'scripts/renders/*', 'test/**/*', '*.json', '*.md', diff --git a/scripts/renders/client.js b/scripts/renders/client.js index 16c9565..6faee34 100755 --- a/scripts/renders/client.js +++ b/scripts/renders/client.js @@ -1,6 +1,6 @@ 'use strict'; -require('../../bin/pixi'); +require('../../dist/pixi'); PIXI.utils.skipHello(); const fs = require('fs'); 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 bc730db..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. @@ -33,8 +33,11 @@ * need to call toDataUrl on the webgl context * @param {number} [options.resolution=1] - The resolution / device pixel ratio of the renderer, retina would be 2 * @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 @@ -43,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. */ @@ -73,7 +99,7 @@ */ stop() { - this.ticker.stop(); + this._ticker.stop(); } /** @@ -81,7 +107,7 @@ */ start() { - this.ticker.start(); + this._ticker.start(); } /** @@ -95,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 index 81e076b..78b17e6 100644 --- a/src/core/Shader.js +++ b/src/core/Shader.js @@ -1,9 +1,7 @@ import { GLShader } from 'pixi-gl-core'; import settings from './settings'; -const { PRECISION } = settings; - -function checkPrecision(src) +function checkPrecision(src, def) { if (src instanceof Array) { @@ -11,14 +9,14 @@ { const copy = src.slice(0); - copy.unshift(`precision ${PRECISION} float;`); + copy.unshift(`precision ${def} float;`); return copy; } } else if (src.substring(0, 9) !== 'precision') { - return `precision ${PRECISION} float;\n${src}`; + return `precision ${def} float;\n${src}`; } return src; @@ -42,6 +40,7 @@ */ constructor(gl, vertexSrc, fragmentSrc) { - super(gl, checkPrecision(vertexSrc), checkPrecision(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 3425b41..f9235e4 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -181,7 +181,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 b3792ef..17fde8d 100644 --- a/src/core/display/Container.js +++ b/src/core/display/Container.js @@ -73,13 +73,14 @@ } child.parent = this; - - // ensure a transform will be recalculated.. - this.transform._parentID = -1; - this._boundsID++; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.push(child); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(this.children.length - 1); child.emit('added', this); @@ -108,9 +109,14 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.splice(index, 0, child); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(index); child.emit('added', this); @@ -174,6 +180,7 @@ removeItems(this.children, currentIndex, 1); // remove from old position this.children.splice(index, 0, child); // add at new position + this.onChildrenChange(index); } @@ -220,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! @@ -244,9 +252,14 @@ { const child = this.getChildAt(index); + // ensure child transform will be recalculated.. child.parent = null; + child.transform._parentID = -1; removeItems(this.children, index, 1); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(index); child.emit('removed', this); @@ -275,8 +288,14 @@ for (let i = 0; i < removed.length; ++i) { removed[i].parent = null; + if (removed[i].transform) + { + removed[i].transform._parentID = -1; + } } + this._boundsID++; + this.onChildrenChange(beginIndex); for (let i = 0; i < removed.length; ++i) diff --git a/src/core/display/DisplayObject.js b/src/core/display/DisplayObject.js index 3affe36..4e6c77d 100644 --- a/src/core/display/DisplayObject.js +++ b/src/core/display/DisplayObject.js @@ -109,7 +109,7 @@ /** * The original, cached mask of the object * - * @member {PIXI.Rectangle} + * @member {PIXI.Graphics|PIXI.Sprite} * @private */ this._mask = null; diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index 95f2af4..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 ); @@ -1065,10 +1061,15 @@ canvasRenderer = new CanvasRenderer(); } - tempMatrix.tx = -bounds.x; - tempMatrix.ty = -bounds.y; + this.transform.updateLocalTransform(); + this.transform.localTransform.copy(tempMatrix); - canvasRenderer.render(this, canvasBuffer, false, tempMatrix); + tempMatrix.invert(); + + tempMatrix.tx -= bounds.x; + tempMatrix.ty -= bounds.y; + + canvasRenderer.render(this, canvasBuffer, true, tempMatrix); const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode); 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 e67680f..867dde2 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/buildCircle.js b/src/core/graphics/webgl/utils/buildCircle.js index 0400d13..abf5d02 100644 --- a/src/core/graphics/webgl/utils/buildCircle.js +++ b/src/core/graphics/webgl/utils/buildCircle.js @@ -33,6 +33,11 @@ height = circleData.height; } + if (width === 0 || height === 0) + { + return; + } + const totalSegs = Math.floor(30 * Math.sqrt(circleData.radius)) || Math.floor(15 * Math.sqrt(circleData.width + circleData.height)); diff --git a/src/core/graphics/webgl/utils/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 90155f9..f03fe15 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -30,6 +30,7 @@ export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; export { default as CanvasGraphicsRenderer } from './graphics/canvas/CanvasGraphicsRenderer'; +export { default as Spritesheet } from './textures/Spritesheet'; export { default as Texture } from './textures/Texture'; export { default as BaseTexture } from './textures/BaseTexture'; export { default as RenderTexture } from './textures/RenderTexture'; diff --git a/src/core/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 ccce680..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); } /** @@ -162,6 +162,9 @@ if (transform) { transform.copy(tempWt); + + // lets not forget to flag the parent transform as dirty... + this._tempDisplayObjectParent.transform._worldID = -1; } else { @@ -169,6 +172,7 @@ } displayObject.parent = this._tempDisplayObjectParent; + displayObject.updateTransform(); displayObject.parent = cacheParent; // displayObject.hitArea = //TODO add a temp hit area @@ -279,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/canvas/utils/CanvasRenderTarget.js b/src/core/renderers/canvas/utils/CanvasRenderTarget.js index 3de0aef..b409891 100644 --- a/src/core/renderers/canvas/utils/CanvasRenderTarget.js +++ b/src/core/renderers/canvas/utils/CanvasRenderTarget.js @@ -1,5 +1,4 @@ import settings from '../../../settings'; -const { RESOLUTION } = settings; /** * Creates a Canvas element of the given size. @@ -30,7 +29,7 @@ */ this.context = this.canvas.getContext('2d'); - this.resolution = resolution || RESOLUTION; + this.resolution = resolution || settings.RESOLUTION; this.resize(width, height); } 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 ce0e1c4..788ed6c 100644 --- a/src/core/renderers/webgl/WebGLRenderer.js +++ b/src/core/renderers/webgl/WebGLRenderer.js @@ -30,8 +30,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 @@ -49,10 +49,19 @@ * enable this if you need to call toDataUrl on the webgl context. * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when * rendering, stopping pixel interpolation. + * @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; + + if (this.legacy) + { + glCore.VertexArrayObject.FORCE_NATIVE = true; + } /** * The type of this renderer as a standardised const @@ -229,7 +238,7 @@ this.emit('context', gl); // setup the width/height properties and gl viewport - this.resize(this.width, this.height); + this.resize(this.screen.width, this.screen.height); } /** @@ -323,16 +332,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) { @@ -376,6 +385,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 @@ -440,9 +469,10 @@ * Changes the current shader to the one given in parameter * * @param {PIXI.Shader} shader - the new shader + * @param {boolean} [autoProject=true] - Whether automatically set the projection matrix * @return {PIXI.WebGLRenderer} Returns itself. */ - bindShader(shader) + bindShader(shader, autoProject) { // TODO cache if (this._activeShader !== shader) @@ -450,8 +480,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 bbab3b3..944f41d 100644 --- a/src/core/renderers/webgl/filters/Filter.js +++ b/src/core/renderers/webgl/filters/Filter.js @@ -1,6 +1,7 @@ import extractUniformsFromSrc from './extractUniformsFromSrc'; import { uid } from '../../../utils'; import { BLEND_MODES } from '../../../const'; +import settings from '../../../settings'; const SOURCE_KEY_MAP = {}; @@ -10,7 +11,7 @@ * @memberof PIXI * @extends PIXI.Shader */ -class Filter +export default class Filter { /** * @param {string} [vertexSrc] - The source of the vertex shader. @@ -35,8 +36,6 @@ this.blendMode = BLEND_MODES.NORMAL; - // pull out the vertex and shader uniforms if they are not specified.. - // currently this does not extract structs only default types this.uniformData = uniforms || extractUniformsFromSrc(this.vertexSrc, this.fragmentSrc, 'projectionMatrix|uSampler'); /** @@ -80,7 +79,7 @@ * * @member {number} */ - this.resolution = 1; + this.resolution = settings.RESOLUTION; /** * If enabled is true the filter is applied, if false it will not. @@ -97,8 +96,11 @@ * @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 { // --- // // this.uniforms.filterMatrix = filterManager.calculateSpriteMatrix(tempMatrix, window.panda ); @@ -170,5 +172,3 @@ ].join('\n'); } } - -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 dc7072a..6c7422d 100644 --- a/src/core/renderers/webgl/managers/FilterManager.js +++ b/src/core/renderers/webgl/managers/FilterManager.js @@ -154,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 @@ -176,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; @@ -184,7 +184,7 @@ flop = t; } - filters[i].apply(this, flip, lastState.renderTarget, true); + filters[i].apply(this, flip, lastState.renderTarget, false, currentState); this.freePotRenderTarget(flip); this.freePotRenderTarget(flop); @@ -291,9 +291,13 @@ let textureCount = 1; let currentState; - if (shader.uniforms.data.filterArea) + // 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; @@ -306,9 +310,9 @@ // use this to clamp displaced texture coords so they belong to filterArea // see displacementFilter fragment shader for an example - if (shader.uniforms.data.filterClamp) + if (shader.uniforms.filterClamp) { - currentState = this.filterData.stack[this.filterData.index]; + currentState = currentState || this.filterData.stack[this.filterData.index]; const filterClamp = shader.uniforms.filterClamp; @@ -485,7 +489,7 @@ */ destroy() { - this.shaderCache = []; + this.shaderCache = {}; this.emptyPool(); } diff --git a/src/core/renderers/webgl/utils/RenderTarget.js b/src/core/renderers/webgl/utils/RenderTarget.js index afe51ab..6d2467f 100644 --- a/src/core/renderers/webgl/utils/RenderTarget.js +++ b/src/core/renderers/webgl/utils/RenderTarget.js @@ -124,7 +124,7 @@ * @default PIXI.settings.SCALE_MODE * @see PIXI.SCALE_MODES */ - this.scaleMode = scaleMode || settings.SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; /** * Whether this object is the root element or not 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 36ea5bb..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; @@ -346,8 +346,8 @@ /** * Gets the local bounds of the sprite object. * - * @param {Rectangle} rect - The output rectangle. - * @return {Rectangle} The bounds. + * @param {PIXI.Rectangle} rect - The output rectangle. + * @return {PIXI.Rectangle} The bounds. */ getLocalBounds(rect) { @@ -440,7 +440,7 @@ * * @static * @param {number|string|PIXI.BaseTexture|HTMLCanvasElement|HTMLVideoElement} source Source to create texture from - * @return {PIXI.Texture} 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 e77b39a..90b08c0 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++) @@ -103,11 +102,18 @@ { const gl = this.renderer.gl; - // step 1: first check max textures the GPU can handle. - this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); + if (this.renderer.legacy) + { + this.MAX_TEXTURES = 1; + } + else + { + // step 1: first check max textures the GPU can handle. + this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); - // step 2: check the maximum number of if statements the shader can have too.. - this.MAX_TEXTURES = checkMaxIfStatmentsInShader(this.MAX_TEXTURES, gl); + // step 2: check the maximum number of if statements the shader can have too.. + this.MAX_TEXTURES = checkMaxIfStatmentsInShader(this.MAX_TEXTURES, gl); + } const shader = this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); @@ -130,8 +136,12 @@ .addIndex(this.indexBuffer) .addAttribute(this.vertexBuffers[i], shader.attributes.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) .addAttribute(this.vertexBuffers[i], shader.attributes.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) - .addAttribute(this.vertexBuffers[i], shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4) - .addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + .addAttribute(this.vertexBuffers[i], shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); + + if (shader.attributes.aTextureId) + { + this.vaos[i].addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + } /* eslint-enable max-len */ } @@ -378,8 +388,12 @@ .addIndex(this.indexBuffer) .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) - .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4) - .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); + + if (this.shader.attributes.aTextureId) + { + this.vaos[this.vertexCount].addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + } /* eslint-enable max-len */ } diff --git a/src/core/text/Text.js b/src/core/text/Text.js index 013625a..cec8c32 100644 --- a/src/core/text/Text.js +++ b/src/core/text/Text.js @@ -6,6 +6,7 @@ import { TEXT_GRADIENT } from '../const'; import settings from '../settings'; import TextStyle from './TextStyle'; +import trimCanvas from '../utils/trimCanvas'; const defaultDestroyOptions = { texture: true, @@ -57,7 +58,7 @@ /** * The canvas 2d context that everything is drawn with - * @member {HTMLCanvasElement} + * @member {CanvasRenderingContext2D} */ this.context = this.canvas.getContext('2d'); @@ -189,10 +190,12 @@ if (style.dropShadow) { + this.context.shadowBlur = style.dropShadowBlur; + this.context.globalAlpha = style.dropShadowAlpha; + if (style.dropShadowBlur > 0) { this.context.shadowColor = style.dropShadowColor; - this.context.shadowBlur = style.dropShadowBlur; } else { @@ -237,6 +240,10 @@ } } + // reset the shadow blur and alpha that was set by the drop shadow, for the regular text + this.context.shadowBlur = 0; + this.context.globalAlpha = 1; + // set canvas text styles this.context.fillStyle = this._generateFillStyle(style, lines); @@ -326,6 +333,15 @@ */ updateTexture() { + if (this._style.trim) + { + const trimmed = trimCanvas(this.canvas); + + this.canvas.width = trimmed.width; + this.canvas.height = trimmed.height; + this.context.putImageData(trimmed.data, 0, 0); + } + const texture = this._texture; const style = this._style; @@ -475,6 +491,19 @@ } /** + * Gets the local bounds of the text object. + * + * @param {Rectangle} rect - The output rectangle. + * @return {Rectangle} The bounds. + */ + getLocalBounds(rect) + { + this.updateText(true); + + return super.getLocalBounds.call(this, rect); + } + + /** * calculates the bounds of the Text as a rectangle. The bounds calculation takes the worldTransform into account. */ _calculateBounds() @@ -525,6 +554,29 @@ const width = this.canvas.width / this.resolution; const height = this.canvas.height / this.resolution; + // make a copy of the style settings, so we can manipulate them later + const fill = style.fill.slice(); + const fillGradientStops = style.fillGradientStops.slice(); + + // wanting to evenly distribute the fills. So an array of 4 colours should give fills of 0.25, 0.5 and 0.75 + if (!fillGradientStops.length) + { + const lengthPlus1 = fill.length + 1; + + for (let i = 1; i < lengthPlus1; ++i) + { + fillGradientStops.push(i / lengthPlus1); + } + } + + // stop the bleeding of the last gradient on the line above to the top gradient of the this line + // by hard defining the first gradient colour at point 0, and last gradient colour at point 1 + fill.unshift(style.fill[0]); + fillGradientStops.unshift(0); + + fill.push(style.fill[style.fill.length - 1]); + fillGradientStops.push(1); + if (style.fillGradientType === TEXT_GRADIENT.LINEAR_VERTICAL) { // start the gradient at the top center of the canvas, and end at the bottom middle of the canvas @@ -532,15 +584,22 @@ // we need to repeat the gradient so that each individual line of text has the same vertical gradient effect // ['#FF0000', '#00FF00', '#0000FF'] over 2 lines would create stops at 0.125, 0.25, 0.375, 0.625, 0.75, 0.875 - totalIterations = (style.fill.length + 1) * lines.length; + totalIterations = (fill.length + 1) * lines.length; currentIteration = 0; for (let i = 0; i < lines.length; i++) { currentIteration += 1; - for (let j = 0; j < style.fill.length; j++) + for (let j = 0; j < fill.length; j++) { - stop = (currentIteration / totalIterations); - gradient.addColorStop(stop, style.fill[j]); + if (fillGradientStops[j]) + { + stop = (fillGradientStops[j] / lines.length) + (i / lines.length); + } + else + { + stop = currentIteration / totalIterations; + } + gradient.addColorStop(stop, fill[j]); currentIteration++; } } @@ -552,13 +611,20 @@ // can just evenly space out the gradients in this case, as multiple lines makes no difference // to an even left to right gradient - totalIterations = style.fill.length + 1; + totalIterations = fill.length + 1; currentIteration = 1; - for (let i = 0; i < style.fill.length; i++) + for (let i = 0; i < fill.length; i++) { - stop = currentIteration / totalIterations; - gradient.addColorStop(stop, style.fill[i]); + if (fillGradientStops[i]) + { + stop = fillGradientStops[i]; + } + else + { + stop = currentIteration / totalIterations; + } + gradient.addColorStop(stop, fill[i]); currentIteration++; } } @@ -569,7 +635,7 @@ /** * Destroys this text object. * Note* Unlike a Sprite, a Text object will automatically destroy its baseTexture and texture as - * the majorety of the time the texture will not be shared with any other Sprites. + * the majority of the time the texture will not be shared with any other Sprites. * * @param {object|boolean} [options] - Options parameter. A boolean will act as if all options * have been set to that value @@ -680,7 +746,7 @@ set text(text) // eslint-disable-line require-jsdoc { - text = String(text || ' '); + text = String(text === '' || text === null || text === undefined ? ' ' : text); if (this._text === text) { diff --git a/src/core/text/TextStyle.js b/src/core/text/TextStyle.js index 3b25528..890950b 100644 --- a/src/core/text/TextStyle.js +++ b/src/core/text/TextStyle.js @@ -8,12 +8,14 @@ align: 'left', breakWords: false, dropShadow: false, + dropShadowAlpha: 1, dropShadowAngle: Math.PI / 6, dropShadowBlur: 0, dropShadowColor: '#000000', dropShadowDistance: 5, fill: 'black', fillGradientType: TEXT_GRADIENT.LINEAR_VERTICAL, + fillGradientStops: [], fontFamily: 'Arial', fontSize: 26, fontStyle: 'normal', @@ -27,6 +29,7 @@ stroke: 'black', strokeThickness: 0, textBaseline: 'alphabetic', + trim: false, wordWrap: false, wordWrapWidth: 100, }; @@ -47,6 +50,7 @@ * @param {boolean} [style.breakWords=false] - Indicates if lines can be wrapped within words, it * needs wordWrap to be set to true * @param {boolean} [style.dropShadow=false] - Set a drop shadow for the text + * @param {number} [style.dropShadowAlpha=1] - Set alpha for the drop shadow * @param {number} [style.dropShadowAngle=Math.PI/6] - Set a angle of the drop shadow * @param {number} [style.dropShadowBlur=0] - Set a shadow blur radius * @param {string} [style.dropShadowColor='#000000'] - A fill style to be used on the dropshadow e.g 'red', '#00FF00' @@ -55,8 +59,10 @@ * fillstyle that will be used on the text e.g 'red', '#00FF00'. Can be an array to create a gradient * eg ['#000000','#FFFFFF'] * {@link https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/fillStyle|MDN} - * @param {number} [style.fillGradientType=PIXI.TEXT_GRADIENT.LINEAR_VERTICAL] - If fills styles are - * supplied, this can change the type/direction of the gradient. See {@link PIXI.TEXT_GRADIENT} for possible values + * @param {number} [style.fillGradientType=PIXI.TEXT_GRADIENT.LINEAR_VERTICAL] - If fill is an array of colours + * to create a gradient, this can change the type/direction of the gradient. See {@link PIXI.TEXT_GRADIENT} + * @param {number[]} [style.fillGradientStops] - If fill is an array of colours to create a gradient, this array can set + * the stop points (numbers between 0 and 1) for the color, overriding the default behaviour of evenly spacing them. * @param {string|string[]} [style.fontFamily='Arial'] - The font family * @param {number|string} [style.fontSize=26] - The font size (as a number it converts to px, but as a string, * equivalents are '26px','20pt','160%' or '1.6em') @@ -76,6 +82,7 @@ * e.g 'blue', '#FCFF00' * @param {number} [style.strokeThickness=0] - A number that represents the thickness of the stroke. * Default is 0 (no stroke) + * @param {boolean} [style.trim=false] - Trim transparent borders * @param {string} [style.textBaseline='alphabetic'] - The baseline of the text that is rendered. * @param {boolean} [style.wordWrap=false] - Indicates if word wrap should be used * @param {number} [style.wordWrapWidth=100] - The width at which text will wrap, it needs wordWrap to be set to true @@ -152,6 +159,19 @@ } } + get dropShadowAlpha() + { + return this._dropShadowAlpha; + } + set dropShadowAlpha(dropShadowAlpha) + { + if (this._dropShadowAlpha !== dropShadowAlpha) + { + this._dropShadowAlpha = dropShadowAlpha; + this.styleID++; + } + } + get dropShadowAngle() { return this._dropShadowAngle; @@ -232,6 +252,19 @@ } } + get fillGradientStops() + { + return this._fillGradientStops; + } + set fillGradientStops(fillGradientStops) + { + if (!areArraysEqual(this._fillGradientStops,fillGradientStops)) + { + this._fillGradientStops = fillGradientStops; + this.styleID++; + } + } + get fontFamily() { return this._fontFamily; @@ -402,6 +435,19 @@ } } + get trim() + { + return this._trim; + } + set trim(trim) + { + if (this._trim !== trim) + { + this._trim = trim; + this.styleID++; + } + } + get wordWrap() { return this._wordWrap; @@ -475,3 +521,34 @@ return color; } } + +/** + * Utility function to convert hexadecimal colors to strings, and simply return the color if it's a string. + * This version can also convert array of colors + * + * @param {Array} array1 First array to compare + * @param {Array} array2 Second array to compare + * @return {boolean} Do the arrays contain the same values in the same order + */ +function areArraysEqual(array1, array2) +{ + if (!Array.isArray(array1) || !Array.isArray(array2)) + { + return false; + } + + if (array1.length !== array2.length) + { + return false; + } + + for (let i = 0; i < array1.length; ++i) + { + if (array1[i] !== array2[i]) + { + return false; + } + } + + return true; +} diff --git a/src/core/textures/BaseRenderTexture.js b/src/core/textures/BaseRenderTexture.js index 5d50146..aa727f0 100644 --- a/src/core/textures/BaseRenderTexture.js +++ b/src/core/textures/BaseRenderTexture.js @@ -1,8 +1,6 @@ import BaseTexture from './BaseTexture'; import settings from '../settings'; -const { RESOLUTION, SCALE_MODE } = settings; - /** * A BaseRenderTexture is a special texture that allows any Pixi display object to be rendered to it. * @@ -54,7 +52,7 @@ { super(null, scaleMode); - this.resolution = resolution || RESOLUTION; + this.resolution = resolution || settings.RESOLUTION; this.width = width; this.height = height; @@ -62,7 +60,7 @@ this.realWidth = this.width * this.resolution; this.realHeight = this.height * this.resolution; - this.scaleMode = scaleMode || SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; this.hasLoaded = true; /** diff --git a/src/core/textures/BaseTexture.js b/src/core/textures/BaseTexture.js index 676dca3..aeccf39 100644 --- a/src/core/textures/BaseTexture.js +++ b/src/core/textures/BaseTexture.js @@ -77,7 +77,7 @@ * @default PIXI.settings.SCALE_MODE * @see PIXI.SCALE_MODES */ - this.scaleMode = scaleMode || settings.SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; /** * Set to true once the base texture has successfully loaded. @@ -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/RenderTexture.js b/src/core/textures/RenderTexture.js index 4c5034f..b3c6297 100644 --- a/src/core/textures/RenderTexture.js +++ b/src/core/textures/RenderTexture.js @@ -54,8 +54,8 @@ /* eslint-disable prefer-rest-params, no-console */ const width = arguments[1]; const height = arguments[2]; - const scaleMode = arguments[3] || 0; - const resolution = arguments[4] || 1; + const scaleMode = arguments[3]; + const resolution = arguments[4]; // we have an old render texture.. console.warn(`Please use RenderTexture.create(${width}, ${height}) instead of the ctor directly.`); 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 ca31456..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 @@ -459,7 +497,9 @@ if (frame.x + frame.width > this.baseTexture.width || frame.y + frame.height > this.baseTexture.height) { - throw new Error(`Texture Error: frame does not fit inside the base Texture dimensions ${this}`); + throw new Error('Texture Error: frame does not fit inside the base Texture dimensions: ' + + `X: ${frame.x} + ${frame.width} > ${this.baseTexture.width} ` + + `Y: ${frame.y} + ${frame.height} > ${this.baseTexture.height}`); } // this.valid = frame && frame.width && frame.height && this.baseTexture.source && this.baseTexture.hasLoaded; @@ -520,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. @@ -528,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 2a7497b..99e585a 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)); } /** @@ -360,3 +362,43 @@ * @private */ export const BaseTextureCache = {}; + +/** + * 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/core/utils/trimCanvas.js b/src/core/utils/trimCanvas.js new file mode 100644 index 0000000..5af6de0 --- /dev/null +++ b/src/core/utils/trimCanvas.js @@ -0,0 +1,83 @@ +/** + * Trim transparent borders from a canvas + * + * @memberof PIXI + * @function trimCanvas + * @private + * @param {HTMLCanvasElement} canvas - the canvas to trim + * @returns {object} Trim data + */ +export default function trimCanvas(canvas) +{ + // https://gist.github.com/remy/784508 + + let width = canvas.width; + let height = canvas.height; + + const context = canvas.getContext('2d'); + const imageData = context.getImageData(0, 0, width, height); + const pixels = imageData.data; + const len = pixels.length; + + const bound = { + top: null, + left: null, + right: null, + bottom: null, + }; + let i; + let x; + let y; + + for (i = 0; i < len; i += 4) + { + if (pixels[i + 3] !== 0) + { + x = (i / 4) % width; + y = ~~((i / 4) / width); + + if (bound.top === null) + { + bound.top = y; + } + + if (bound.left === null) + { + bound.left = x; + } + else if (x < bound.left) + { + bound.left = x; + } + + if (bound.right === null) + { + bound.right = x + 1; + } + else if (bound.right < x) + { + bound.right = x + 1; + } + + if (bound.bottom === null) + { + bound.bottom = y; + } + else if (bound.bottom < y) + { + bound.bottom = y; + } + } + } + + width = bound.right - bound.left; + height = bound.bottom - bound.top + 1; + + const data = context.getImageData(bound.left, bound.top, width, height); + + return { + height, + width, + data, + }; +} diff --git a/src/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/extras/TilingSprite.js b/src/extras/TilingSprite.js index f90d248..fe82451 100644 --- a/src/extras/TilingSprite.js +++ b/src/extras/TilingSprite.js @@ -220,20 +220,33 @@ transform.tx * resolution, transform.ty * resolution); - // TODO - this should be rolled into the setTransform above.. - context.scale(this.tileScale.x / baseTextureResolution, this.tileScale.y / baseTextureResolution); - - context.translate(modX + (this.anchor.x * -this._width), - modY + (this.anchor.y * -this._height)); - renderer.setBlendMode(this.blendMode); // fill the pattern! context.fillStyle = this._canvasPattern; - context.fillRect(-modX, - -modY, - this._width / this.tileScale.x * baseTextureResolution, - this._height / this.tileScale.y * baseTextureResolution); + + // TODO - this should be rolled into the setTransform above.. + context.scale(this.tileScale.x / baseTextureResolution, this.tileScale.y / baseTextureResolution); + + const anchorX = this.anchor.x * -this._width; + const anchorY = this.anchor.y * -this._height; + + if (this.uvRespectAnchor) + { + context.translate(modX, modY); + + context.fillRect(-modX + anchorX, -modY + anchorY, + this._width / this.tileScale.x * baseTextureResolution, + this._height / this.tileScale.y * baseTextureResolution); + } + else + { + context.translate(modX + anchorX, modY + anchorY); + + context.fillRect(-modX, -modY, + this._width / this.tileScale.x * baseTextureResolution, + this._height / this.tileScale.y * baseTextureResolution); + } } /** diff --git a/src/extras/cacheAsBitmap.js b/src/extras/cacheAsBitmap.js index 18043a7..5dd5640 100644 --- a/src/extras/cacheAsBitmap.js +++ b/src/extras/cacheAsBitmap.js @@ -41,6 +41,9 @@ * provide a performance benefit for complex static displayObjects. * To remove simply set this property to 'false' * + * IMPORTANT GOTCHA - make sure that all your textures are preloaded BEFORE setting this property to true + * as it will take a snapshot of what is currently there. If the textures have not loaded then they will not appear. + * * @member {boolean} * @memberof PIXI.DisplayObject# */ diff --git a/src/filters/blur/BlurFilter.js b/src/filters/blur/BlurFilter.js index 340fc58..1575db9 100644 --- a/src/filters/blur/BlurFilter.js +++ b/src/filters/blur/BlurFilter.js @@ -24,10 +24,9 @@ this.blurXFilter = new BlurXFilter(strength, quality, resolution, kernelSize); this.blurYFilter = new BlurYFilter(strength, quality, resolution, kernelSize); - this.resolution = 1; this.padding = 0; - this.resolution = resolution || 1; + this.resolution = resolution || core.settings.RESOLUTION; this.quality = quality || 4; this.blur = strength || 8; } diff --git a/src/filters/blur/BlurXFilter.js b/src/filters/blur/BlurXFilter.js index def99c3..0f48889 100644 --- a/src/filters/blur/BlurXFilter.js +++ b/src/filters/blur/BlurXFilter.js @@ -31,7 +31,7 @@ fragSrc ); - this.resolution = resolution || 1; + this.resolution = resolution || core.settings.RESOLUTION; this._quality = 0; diff --git a/src/filters/blur/BlurYFilter.js b/src/filters/blur/BlurYFilter.js index 638e0f0..6d489c7 100644 --- a/src/filters/blur/BlurYFilter.js +++ b/src/filters/blur/BlurYFilter.js @@ -31,7 +31,7 @@ fragSrc ); - this.resolution = resolution || 1; + this.resolution = resolution || core.settings.RESOLUTION; this._quality = 0; diff --git a/src/filters/colormatrix/ColorMatrixFilter.js b/src/filters/colormatrix/ColorMatrixFilter.js index 8509505..679f8ec 100644 --- a/src/filters/colormatrix/ColorMatrixFilter.js +++ b/src/filters/colormatrix/ColorMatrixFilter.js @@ -75,28 +75,28 @@ out[1] = (a[0] * b[1]) + (a[1] * b[6]) + (a[2] * b[11]) + (a[3] * b[16]); out[2] = (a[0] * b[2]) + (a[1] * b[7]) + (a[2] * b[12]) + (a[3] * b[17]); out[3] = (a[0] * b[3]) + (a[1] * b[8]) + (a[2] * b[13]) + (a[3] * b[18]); - out[4] = (a[0] * b[4]) + (a[1] * b[9]) + (a[2] * b[14]) + (a[3] * b[19]); + out[4] = (a[0] * b[4]) + (a[1] * b[9]) + (a[2] * b[14]) + (a[3] * b[19]) + a[4]; // Green Channel out[5] = (a[5] * b[0]) + (a[6] * b[5]) + (a[7] * b[10]) + (a[8] * b[15]); out[6] = (a[5] * b[1]) + (a[6] * b[6]) + (a[7] * b[11]) + (a[8] * b[16]); out[7] = (a[5] * b[2]) + (a[6] * b[7]) + (a[7] * b[12]) + (a[8] * b[17]); out[8] = (a[5] * b[3]) + (a[6] * b[8]) + (a[7] * b[13]) + (a[8] * b[18]); - out[9] = (a[5] * b[4]) + (a[6] * b[9]) + (a[7] * b[14]) + (a[8] * b[19]); + out[9] = (a[5] * b[4]) + (a[6] * b[9]) + (a[7] * b[14]) + (a[8] * b[19]) + a[9]; // Blue Channel out[10] = (a[10] * b[0]) + (a[11] * b[5]) + (a[12] * b[10]) + (a[13] * b[15]); out[11] = (a[10] * b[1]) + (a[11] * b[6]) + (a[12] * b[11]) + (a[13] * b[16]); out[12] = (a[10] * b[2]) + (a[11] * b[7]) + (a[12] * b[12]) + (a[13] * b[17]); out[13] = (a[10] * b[3]) + (a[11] * b[8]) + (a[12] * b[13]) + (a[13] * b[18]); - out[14] = (a[10] * b[4]) + (a[11] * b[9]) + (a[12] * b[14]) + (a[13] * b[19]); + out[14] = (a[10] * b[4]) + (a[11] * b[9]) + (a[12] * b[14]) + (a[13] * b[19]) + a[14]; // Alpha Channel out[15] = (a[15] * b[0]) + (a[16] * b[5]) + (a[17] * b[10]) + (a[18] * b[15]); out[16] = (a[15] * b[1]) + (a[16] * b[6]) + (a[17] * b[11]) + (a[18] * b[16]); out[17] = (a[15] * b[2]) + (a[16] * b[7]) + (a[17] * b[12]) + (a[18] * b[17]); out[18] = (a[15] * b[3]) + (a[16] * b[8]) + (a[17] * b[13]) + (a[18] * b[18]); - out[19] = (a[15] * b[4]) + (a[16] * b[9]) + (a[17] * b[14]) + (a[18] * b[19]); + out[19] = (a[15] * b[4]) + (a[16] * b[9]) + (a[17] * b[14]) + (a[18] * b[19]) + a[19]; return out; } diff --git a/src/filters/colormatrix/colorMatrix.frag b/src/filters/colormatrix/colorMatrix.frag index 3aaf2b3..c73c0e9 100644 --- a/src/filters/colormatrix/colorMatrix.frag +++ b/src/filters/colormatrix/colorMatrix.frag @@ -4,32 +4,38 @@ void main(void) { - vec4 c = texture2D(uSampler, vTextureCoord); + // Un-premultiply alpha before applying the color matrix. See issue #3539. + if (c.a > 0.0) { + c.rgb /= c.a; + } + vec4 result; + result.r = (m[0] * c.r); + result.r += (m[1] * c.g); + result.r += (m[2] * c.b); + result.r += (m[3] * c.a); + result.r += m[4]; - gl_FragColor.r = (m[0] * c.r); - gl_FragColor.r += (m[1] * c.g); - gl_FragColor.r += (m[2] * c.b); - gl_FragColor.r += (m[3] * c.a); - gl_FragColor.r += m[4] * c.a; + result.g = (m[5] * c.r); + result.g += (m[6] * c.g); + result.g += (m[7] * c.b); + result.g += (m[8] * c.a); + result.g += m[9]; - gl_FragColor.g = (m[5] * c.r); - gl_FragColor.g += (m[6] * c.g); - gl_FragColor.g += (m[7] * c.b); - gl_FragColor.g += (m[8] * c.a); - gl_FragColor.g += m[9] * c.a; + result.b = (m[10] * c.r); + result.b += (m[11] * c.g); + result.b += (m[12] * c.b); + result.b += (m[13] * c.a); + result.b += m[14]; - gl_FragColor.b = (m[10] * c.r); - gl_FragColor.b += (m[11] * c.g); - gl_FragColor.b += (m[12] * c.b); - gl_FragColor.b += (m[13] * c.a); - gl_FragColor.b += m[14] * c.a; + result.a = (m[15] * c.r); + result.a += (m[16] * c.g); + result.a += (m[17] * c.b); + result.a += (m[18] * c.a); + result.a += m[19]; - gl_FragColor.a = (m[15] * c.r); - gl_FragColor.a += (m[16] * c.g); - gl_FragColor.a += (m[17] * c.b); - gl_FragColor.a += (m[18] * c.a); - gl_FragColor.a += m[19] * c.a; + // Premultiply alpha again. + result.rgb *= result.a; -// gl_FragColor = vec4(m[0]); + gl_FragColor = result; } diff --git a/src/filters/displacement/DisplacementFilter.js b/src/filters/displacement/DisplacementFilter.js index 112de0b..d5355c3 100644 --- a/src/filters/displacement/DisplacementFilter.js +++ b/src/filters/displacement/DisplacementFilter.js @@ -36,7 +36,7 @@ this.maskMatrix = maskMatrix; this.uniforms.mapSampler = sprite.texture; - this.uniforms.filterMatrix = maskMatrix.toArray(true); + this.uniforms.filterMatrix = maskMatrix; this.uniforms.scale = { x: 1, y: 1 }; if (scale === null || scale === undefined) diff --git a/src/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/InteractionData.js b/src/interaction/InteractionData.js index 02d53a3..818da35 100644 --- a/src/interaction/InteractionData.js +++ b/src/interaction/InteractionData.js @@ -21,9 +21,9 @@ this.global = new core.Point(); /** - * The target Sprite that was interacted with + * The target DisplayObject that was interacted with * - * @member {PIXI.Sprite} + * @member {PIXI.DisplayObject} */ this.target = null; @@ -33,6 +33,13 @@ * @member {Event} */ this.originalEvent = null; + + /** + * Unique identifier for this interaction + * + * @member {number} + */ + this.identifier = null; } /** diff --git a/src/interaction/InteractionEvent.js b/src/interaction/InteractionEvent.js index 75b2966..dc554f9 100644 --- a/src/interaction/InteractionEvent.js +++ b/src/interaction/InteractionEvent.js @@ -58,7 +58,7 @@ } /** - * Prevents event from reaching any objects other than the current object. + * Resets the event. * * @private */ diff --git a/src/interaction/InteractionManager.js b/src/interaction/InteractionManager.js index 7d51134..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/src/prepare/BasePrepare.js b/src/prepare/BasePrepare.js index a2005f5..09eb5e5 100644 --- a/src/prepare/BasePrepare.js +++ b/src/prepare/BasePrepare.js @@ -20,7 +20,7 @@ * * @abstract * @class - * @memberof PIXI + * @memberof PIXI.prepare */ export default class BasePrepare { @@ -107,9 +107,9 @@ /** * Upload all the textures and graphics to the GPU. * - * @param {Function|PIXI.DisplayObject|PIXI.Container} item - Either - * the container or display object to search for items to upload or - * the callback function, if items have been added using `prepare.add`. + * @param {Function|PIXI.DisplayObject|PIXI.Container|PIXI.BaseTexture|PIXI.Texture|PIXI.Graphics|PIXI.Text} item - + * Either the container or display object to search for items to upload, the items to upload themselves, + * or the callback function, if items have been added using `prepare.add`. * @param {Function} [done] - Optional callback when all queued uploads have completed */ upload(item, done) @@ -236,7 +236,8 @@ /** * Manually add an item to the uploading queue. * - * @param {PIXI.DisplayObject|PIXI.Container|*} item - Object to add to the queue + * @param {PIXI.DisplayObject|PIXI.Container|PIXI.BaseTexture|PIXI.Texture|PIXI.Graphics|PIXI.Text|*} item - Object to + * add to the queue * @return {PIXI.CanvasPrepare} Instance of plugin for chaining. */ add(item) diff --git a/src/prepare/canvas/CanvasPrepare.js b/src/prepare/canvas/CanvasPrepare.js index 4b99d22..0224bb5 100644 --- a/src/prepare/canvas/CanvasPrepare.js +++ b/src/prepare/canvas/CanvasPrepare.js @@ -12,7 +12,8 @@ * An instance of this class is automatically created by default, and can be found at renderer.plugins.prepare * * @class - * @memberof PIXI + * @extends PIXI.prepare.BasePrepare + * @memberof PIXI.prepare */ export default class CanvasPrepare extends BasePrepare { diff --git a/src/prepare/webgl/WebGLPrepare.js b/src/prepare/webgl/WebGLPrepare.js index 1ef7467..eb7023f 100644 --- a/src/prepare/webgl/WebGLPrepare.js +++ b/src/prepare/webgl/WebGLPrepare.js @@ -7,7 +7,8 @@ * An instance of this class is automatically created by default, and can be found at renderer.plugins.prepare * * @class - * @memberof PIXI + * @extends PIXI.prepare.BasePrepare + * @memberof PIXI.prepare */ export default class WebGLPrepare extends BasePrepare { diff --git a/test/core/Circle.js b/test/core/Circle.js index 183f98f..f621992 100644 --- a/test/core/Circle.js +++ b/test/core/Circle.js @@ -53,6 +53,10 @@ expect(circ1.contains(10, 16)).to.be.false; expect(circ1.contains(11, 15)).to.be.false; expect(circ1.contains(0, 0)).to.be.false; + + const circ2 = new PIXI.Circle(10, 10, 0); + + expect(circ2.contains(10, 10)).to.be.false; }); it('should return framing rectangle', function () diff --git a/package.json b/package.json index 1271fef..71ade15 100644 --- a/package.json +++ b/package.json @@ -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/scripts/release.js b/scripts/release.js index c4c8e36..156620c 100644 --- a/scripts/release.js +++ b/scripts/release.js @@ -12,7 +12,8 @@ 'dist/**/*', 'lib/**/*', 'src/**/*', - 'scripts/**/*', + 'scripts/*', + 'scripts/renders/*', 'test/**/*', '*.json', '*.md', diff --git a/scripts/renders/client.js b/scripts/renders/client.js index 16c9565..6faee34 100755 --- a/scripts/renders/client.js +++ b/scripts/renders/client.js @@ -1,6 +1,6 @@ 'use strict'; -require('../../bin/pixi'); +require('../../dist/pixi'); PIXI.utils.skipHello(); const fs = require('fs'); 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 bc730db..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. @@ -33,8 +33,11 @@ * need to call toDataUrl on the webgl context * @param {number} [options.resolution=1] - The resolution / device pixel ratio of the renderer, retina would be 2 * @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 @@ -43,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. */ @@ -73,7 +99,7 @@ */ stop() { - this.ticker.stop(); + this._ticker.stop(); } /** @@ -81,7 +107,7 @@ */ start() { - this.ticker.start(); + this._ticker.start(); } /** @@ -95,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 index 81e076b..78b17e6 100644 --- a/src/core/Shader.js +++ b/src/core/Shader.js @@ -1,9 +1,7 @@ import { GLShader } from 'pixi-gl-core'; import settings from './settings'; -const { PRECISION } = settings; - -function checkPrecision(src) +function checkPrecision(src, def) { if (src instanceof Array) { @@ -11,14 +9,14 @@ { const copy = src.slice(0); - copy.unshift(`precision ${PRECISION} float;`); + copy.unshift(`precision ${def} float;`); return copy; } } else if (src.substring(0, 9) !== 'precision') { - return `precision ${PRECISION} float;\n${src}`; + return `precision ${def} float;\n${src}`; } return src; @@ -42,6 +40,7 @@ */ constructor(gl, vertexSrc, fragmentSrc) { - super(gl, checkPrecision(vertexSrc), checkPrecision(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 3425b41..f9235e4 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -181,7 +181,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 b3792ef..17fde8d 100644 --- a/src/core/display/Container.js +++ b/src/core/display/Container.js @@ -73,13 +73,14 @@ } child.parent = this; - - // ensure a transform will be recalculated.. - this.transform._parentID = -1; - this._boundsID++; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.push(child); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(this.children.length - 1); child.emit('added', this); @@ -108,9 +109,14 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.splice(index, 0, child); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(index); child.emit('added', this); @@ -174,6 +180,7 @@ removeItems(this.children, currentIndex, 1); // remove from old position this.children.splice(index, 0, child); // add at new position + this.onChildrenChange(index); } @@ -220,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! @@ -244,9 +252,14 @@ { const child = this.getChildAt(index); + // ensure child transform will be recalculated.. child.parent = null; + child.transform._parentID = -1; removeItems(this.children, index, 1); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(index); child.emit('removed', this); @@ -275,8 +288,14 @@ for (let i = 0; i < removed.length; ++i) { removed[i].parent = null; + if (removed[i].transform) + { + removed[i].transform._parentID = -1; + } } + this._boundsID++; + this.onChildrenChange(beginIndex); for (let i = 0; i < removed.length; ++i) diff --git a/src/core/display/DisplayObject.js b/src/core/display/DisplayObject.js index 3affe36..4e6c77d 100644 --- a/src/core/display/DisplayObject.js +++ b/src/core/display/DisplayObject.js @@ -109,7 +109,7 @@ /** * The original, cached mask of the object * - * @member {PIXI.Rectangle} + * @member {PIXI.Graphics|PIXI.Sprite} * @private */ this._mask = null; diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index 95f2af4..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 ); @@ -1065,10 +1061,15 @@ canvasRenderer = new CanvasRenderer(); } - tempMatrix.tx = -bounds.x; - tempMatrix.ty = -bounds.y; + this.transform.updateLocalTransform(); + this.transform.localTransform.copy(tempMatrix); - canvasRenderer.render(this, canvasBuffer, false, tempMatrix); + tempMatrix.invert(); + + tempMatrix.tx -= bounds.x; + tempMatrix.ty -= bounds.y; + + canvasRenderer.render(this, canvasBuffer, true, tempMatrix); const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode); 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 e67680f..867dde2 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/buildCircle.js b/src/core/graphics/webgl/utils/buildCircle.js index 0400d13..abf5d02 100644 --- a/src/core/graphics/webgl/utils/buildCircle.js +++ b/src/core/graphics/webgl/utils/buildCircle.js @@ -33,6 +33,11 @@ height = circleData.height; } + if (width === 0 || height === 0) + { + return; + } + const totalSegs = Math.floor(30 * Math.sqrt(circleData.radius)) || Math.floor(15 * Math.sqrt(circleData.width + circleData.height)); diff --git a/src/core/graphics/webgl/utils/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 90155f9..f03fe15 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -30,6 +30,7 @@ export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; export { default as CanvasGraphicsRenderer } from './graphics/canvas/CanvasGraphicsRenderer'; +export { default as Spritesheet } from './textures/Spritesheet'; export { default as Texture } from './textures/Texture'; export { default as BaseTexture } from './textures/BaseTexture'; export { default as RenderTexture } from './textures/RenderTexture'; diff --git a/src/core/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 ccce680..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); } /** @@ -162,6 +162,9 @@ if (transform) { transform.copy(tempWt); + + // lets not forget to flag the parent transform as dirty... + this._tempDisplayObjectParent.transform._worldID = -1; } else { @@ -169,6 +172,7 @@ } displayObject.parent = this._tempDisplayObjectParent; + displayObject.updateTransform(); displayObject.parent = cacheParent; // displayObject.hitArea = //TODO add a temp hit area @@ -279,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/canvas/utils/CanvasRenderTarget.js b/src/core/renderers/canvas/utils/CanvasRenderTarget.js index 3de0aef..b409891 100644 --- a/src/core/renderers/canvas/utils/CanvasRenderTarget.js +++ b/src/core/renderers/canvas/utils/CanvasRenderTarget.js @@ -1,5 +1,4 @@ import settings from '../../../settings'; -const { RESOLUTION } = settings; /** * Creates a Canvas element of the given size. @@ -30,7 +29,7 @@ */ this.context = this.canvas.getContext('2d'); - this.resolution = resolution || RESOLUTION; + this.resolution = resolution || settings.RESOLUTION; this.resize(width, height); } 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 ce0e1c4..788ed6c 100644 --- a/src/core/renderers/webgl/WebGLRenderer.js +++ b/src/core/renderers/webgl/WebGLRenderer.js @@ -30,8 +30,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 @@ -49,10 +49,19 @@ * enable this if you need to call toDataUrl on the webgl context. * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when * rendering, stopping pixel interpolation. + * @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; + + if (this.legacy) + { + glCore.VertexArrayObject.FORCE_NATIVE = true; + } /** * The type of this renderer as a standardised const @@ -229,7 +238,7 @@ this.emit('context', gl); // setup the width/height properties and gl viewport - this.resize(this.width, this.height); + this.resize(this.screen.width, this.screen.height); } /** @@ -323,16 +332,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) { @@ -376,6 +385,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 @@ -440,9 +469,10 @@ * Changes the current shader to the one given in parameter * * @param {PIXI.Shader} shader - the new shader + * @param {boolean} [autoProject=true] - Whether automatically set the projection matrix * @return {PIXI.WebGLRenderer} Returns itself. */ - bindShader(shader) + bindShader(shader, autoProject) { // TODO cache if (this._activeShader !== shader) @@ -450,8 +480,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 bbab3b3..944f41d 100644 --- a/src/core/renderers/webgl/filters/Filter.js +++ b/src/core/renderers/webgl/filters/Filter.js @@ -1,6 +1,7 @@ import extractUniformsFromSrc from './extractUniformsFromSrc'; import { uid } from '../../../utils'; import { BLEND_MODES } from '../../../const'; +import settings from '../../../settings'; const SOURCE_KEY_MAP = {}; @@ -10,7 +11,7 @@ * @memberof PIXI * @extends PIXI.Shader */ -class Filter +export default class Filter { /** * @param {string} [vertexSrc] - The source of the vertex shader. @@ -35,8 +36,6 @@ this.blendMode = BLEND_MODES.NORMAL; - // pull out the vertex and shader uniforms if they are not specified.. - // currently this does not extract structs only default types this.uniformData = uniforms || extractUniformsFromSrc(this.vertexSrc, this.fragmentSrc, 'projectionMatrix|uSampler'); /** @@ -80,7 +79,7 @@ * * @member {number} */ - this.resolution = 1; + this.resolution = settings.RESOLUTION; /** * If enabled is true the filter is applied, if false it will not. @@ -97,8 +96,11 @@ * @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 { // --- // // this.uniforms.filterMatrix = filterManager.calculateSpriteMatrix(tempMatrix, window.panda ); @@ -170,5 +172,3 @@ ].join('\n'); } } - -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 dc7072a..6c7422d 100644 --- a/src/core/renderers/webgl/managers/FilterManager.js +++ b/src/core/renderers/webgl/managers/FilterManager.js @@ -154,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 @@ -176,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; @@ -184,7 +184,7 @@ flop = t; } - filters[i].apply(this, flip, lastState.renderTarget, true); + filters[i].apply(this, flip, lastState.renderTarget, false, currentState); this.freePotRenderTarget(flip); this.freePotRenderTarget(flop); @@ -291,9 +291,13 @@ let textureCount = 1; let currentState; - if (shader.uniforms.data.filterArea) + // 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; @@ -306,9 +310,9 @@ // use this to clamp displaced texture coords so they belong to filterArea // see displacementFilter fragment shader for an example - if (shader.uniforms.data.filterClamp) + if (shader.uniforms.filterClamp) { - currentState = this.filterData.stack[this.filterData.index]; + currentState = currentState || this.filterData.stack[this.filterData.index]; const filterClamp = shader.uniforms.filterClamp; @@ -485,7 +489,7 @@ */ destroy() { - this.shaderCache = []; + this.shaderCache = {}; this.emptyPool(); } diff --git a/src/core/renderers/webgl/utils/RenderTarget.js b/src/core/renderers/webgl/utils/RenderTarget.js index afe51ab..6d2467f 100644 --- a/src/core/renderers/webgl/utils/RenderTarget.js +++ b/src/core/renderers/webgl/utils/RenderTarget.js @@ -124,7 +124,7 @@ * @default PIXI.settings.SCALE_MODE * @see PIXI.SCALE_MODES */ - this.scaleMode = scaleMode || settings.SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; /** * Whether this object is the root element or not 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 36ea5bb..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; @@ -346,8 +346,8 @@ /** * Gets the local bounds of the sprite object. * - * @param {Rectangle} rect - The output rectangle. - * @return {Rectangle} The bounds. + * @param {PIXI.Rectangle} rect - The output rectangle. + * @return {PIXI.Rectangle} The bounds. */ getLocalBounds(rect) { @@ -440,7 +440,7 @@ * * @static * @param {number|string|PIXI.BaseTexture|HTMLCanvasElement|HTMLVideoElement} source Source to create texture from - * @return {PIXI.Texture} 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 e77b39a..90b08c0 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++) @@ -103,11 +102,18 @@ { const gl = this.renderer.gl; - // step 1: first check max textures the GPU can handle. - this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); + if (this.renderer.legacy) + { + this.MAX_TEXTURES = 1; + } + else + { + // step 1: first check max textures the GPU can handle. + this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); - // step 2: check the maximum number of if statements the shader can have too.. - this.MAX_TEXTURES = checkMaxIfStatmentsInShader(this.MAX_TEXTURES, gl); + // step 2: check the maximum number of if statements the shader can have too.. + this.MAX_TEXTURES = checkMaxIfStatmentsInShader(this.MAX_TEXTURES, gl); + } const shader = this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); @@ -130,8 +136,12 @@ .addIndex(this.indexBuffer) .addAttribute(this.vertexBuffers[i], shader.attributes.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) .addAttribute(this.vertexBuffers[i], shader.attributes.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) - .addAttribute(this.vertexBuffers[i], shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4) - .addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + .addAttribute(this.vertexBuffers[i], shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); + + if (shader.attributes.aTextureId) + { + this.vaos[i].addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + } /* eslint-enable max-len */ } @@ -378,8 +388,12 @@ .addIndex(this.indexBuffer) .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) - .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4) - .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); + + if (this.shader.attributes.aTextureId) + { + this.vaos[this.vertexCount].addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + } /* eslint-enable max-len */ } diff --git a/src/core/text/Text.js b/src/core/text/Text.js index 013625a..cec8c32 100644 --- a/src/core/text/Text.js +++ b/src/core/text/Text.js @@ -6,6 +6,7 @@ import { TEXT_GRADIENT } from '../const'; import settings from '../settings'; import TextStyle from './TextStyle'; +import trimCanvas from '../utils/trimCanvas'; const defaultDestroyOptions = { texture: true, @@ -57,7 +58,7 @@ /** * The canvas 2d context that everything is drawn with - * @member {HTMLCanvasElement} + * @member {CanvasRenderingContext2D} */ this.context = this.canvas.getContext('2d'); @@ -189,10 +190,12 @@ if (style.dropShadow) { + this.context.shadowBlur = style.dropShadowBlur; + this.context.globalAlpha = style.dropShadowAlpha; + if (style.dropShadowBlur > 0) { this.context.shadowColor = style.dropShadowColor; - this.context.shadowBlur = style.dropShadowBlur; } else { @@ -237,6 +240,10 @@ } } + // reset the shadow blur and alpha that was set by the drop shadow, for the regular text + this.context.shadowBlur = 0; + this.context.globalAlpha = 1; + // set canvas text styles this.context.fillStyle = this._generateFillStyle(style, lines); @@ -326,6 +333,15 @@ */ updateTexture() { + if (this._style.trim) + { + const trimmed = trimCanvas(this.canvas); + + this.canvas.width = trimmed.width; + this.canvas.height = trimmed.height; + this.context.putImageData(trimmed.data, 0, 0); + } + const texture = this._texture; const style = this._style; @@ -475,6 +491,19 @@ } /** + * Gets the local bounds of the text object. + * + * @param {Rectangle} rect - The output rectangle. + * @return {Rectangle} The bounds. + */ + getLocalBounds(rect) + { + this.updateText(true); + + return super.getLocalBounds.call(this, rect); + } + + /** * calculates the bounds of the Text as a rectangle. The bounds calculation takes the worldTransform into account. */ _calculateBounds() @@ -525,6 +554,29 @@ const width = this.canvas.width / this.resolution; const height = this.canvas.height / this.resolution; + // make a copy of the style settings, so we can manipulate them later + const fill = style.fill.slice(); + const fillGradientStops = style.fillGradientStops.slice(); + + // wanting to evenly distribute the fills. So an array of 4 colours should give fills of 0.25, 0.5 and 0.75 + if (!fillGradientStops.length) + { + const lengthPlus1 = fill.length + 1; + + for (let i = 1; i < lengthPlus1; ++i) + { + fillGradientStops.push(i / lengthPlus1); + } + } + + // stop the bleeding of the last gradient on the line above to the top gradient of the this line + // by hard defining the first gradient colour at point 0, and last gradient colour at point 1 + fill.unshift(style.fill[0]); + fillGradientStops.unshift(0); + + fill.push(style.fill[style.fill.length - 1]); + fillGradientStops.push(1); + if (style.fillGradientType === TEXT_GRADIENT.LINEAR_VERTICAL) { // start the gradient at the top center of the canvas, and end at the bottom middle of the canvas @@ -532,15 +584,22 @@ // we need to repeat the gradient so that each individual line of text has the same vertical gradient effect // ['#FF0000', '#00FF00', '#0000FF'] over 2 lines would create stops at 0.125, 0.25, 0.375, 0.625, 0.75, 0.875 - totalIterations = (style.fill.length + 1) * lines.length; + totalIterations = (fill.length + 1) * lines.length; currentIteration = 0; for (let i = 0; i < lines.length; i++) { currentIteration += 1; - for (let j = 0; j < style.fill.length; j++) + for (let j = 0; j < fill.length; j++) { - stop = (currentIteration / totalIterations); - gradient.addColorStop(stop, style.fill[j]); + if (fillGradientStops[j]) + { + stop = (fillGradientStops[j] / lines.length) + (i / lines.length); + } + else + { + stop = currentIteration / totalIterations; + } + gradient.addColorStop(stop, fill[j]); currentIteration++; } } @@ -552,13 +611,20 @@ // can just evenly space out the gradients in this case, as multiple lines makes no difference // to an even left to right gradient - totalIterations = style.fill.length + 1; + totalIterations = fill.length + 1; currentIteration = 1; - for (let i = 0; i < style.fill.length; i++) + for (let i = 0; i < fill.length; i++) { - stop = currentIteration / totalIterations; - gradient.addColorStop(stop, style.fill[i]); + if (fillGradientStops[i]) + { + stop = fillGradientStops[i]; + } + else + { + stop = currentIteration / totalIterations; + } + gradient.addColorStop(stop, fill[i]); currentIteration++; } } @@ -569,7 +635,7 @@ /** * Destroys this text object. * Note* Unlike a Sprite, a Text object will automatically destroy its baseTexture and texture as - * the majorety of the time the texture will not be shared with any other Sprites. + * the majority of the time the texture will not be shared with any other Sprites. * * @param {object|boolean} [options] - Options parameter. A boolean will act as if all options * have been set to that value @@ -680,7 +746,7 @@ set text(text) // eslint-disable-line require-jsdoc { - text = String(text || ' '); + text = String(text === '' || text === null || text === undefined ? ' ' : text); if (this._text === text) { diff --git a/src/core/text/TextStyle.js b/src/core/text/TextStyle.js index 3b25528..890950b 100644 --- a/src/core/text/TextStyle.js +++ b/src/core/text/TextStyle.js @@ -8,12 +8,14 @@ align: 'left', breakWords: false, dropShadow: false, + dropShadowAlpha: 1, dropShadowAngle: Math.PI / 6, dropShadowBlur: 0, dropShadowColor: '#000000', dropShadowDistance: 5, fill: 'black', fillGradientType: TEXT_GRADIENT.LINEAR_VERTICAL, + fillGradientStops: [], fontFamily: 'Arial', fontSize: 26, fontStyle: 'normal', @@ -27,6 +29,7 @@ stroke: 'black', strokeThickness: 0, textBaseline: 'alphabetic', + trim: false, wordWrap: false, wordWrapWidth: 100, }; @@ -47,6 +50,7 @@ * @param {boolean} [style.breakWords=false] - Indicates if lines can be wrapped within words, it * needs wordWrap to be set to true * @param {boolean} [style.dropShadow=false] - Set a drop shadow for the text + * @param {number} [style.dropShadowAlpha=1] - Set alpha for the drop shadow * @param {number} [style.dropShadowAngle=Math.PI/6] - Set a angle of the drop shadow * @param {number} [style.dropShadowBlur=0] - Set a shadow blur radius * @param {string} [style.dropShadowColor='#000000'] - A fill style to be used on the dropshadow e.g 'red', '#00FF00' @@ -55,8 +59,10 @@ * fillstyle that will be used on the text e.g 'red', '#00FF00'. Can be an array to create a gradient * eg ['#000000','#FFFFFF'] * {@link https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/fillStyle|MDN} - * @param {number} [style.fillGradientType=PIXI.TEXT_GRADIENT.LINEAR_VERTICAL] - If fills styles are - * supplied, this can change the type/direction of the gradient. See {@link PIXI.TEXT_GRADIENT} for possible values + * @param {number} [style.fillGradientType=PIXI.TEXT_GRADIENT.LINEAR_VERTICAL] - If fill is an array of colours + * to create a gradient, this can change the type/direction of the gradient. See {@link PIXI.TEXT_GRADIENT} + * @param {number[]} [style.fillGradientStops] - If fill is an array of colours to create a gradient, this array can set + * the stop points (numbers between 0 and 1) for the color, overriding the default behaviour of evenly spacing them. * @param {string|string[]} [style.fontFamily='Arial'] - The font family * @param {number|string} [style.fontSize=26] - The font size (as a number it converts to px, but as a string, * equivalents are '26px','20pt','160%' or '1.6em') @@ -76,6 +82,7 @@ * e.g 'blue', '#FCFF00' * @param {number} [style.strokeThickness=0] - A number that represents the thickness of the stroke. * Default is 0 (no stroke) + * @param {boolean} [style.trim=false] - Trim transparent borders * @param {string} [style.textBaseline='alphabetic'] - The baseline of the text that is rendered. * @param {boolean} [style.wordWrap=false] - Indicates if word wrap should be used * @param {number} [style.wordWrapWidth=100] - The width at which text will wrap, it needs wordWrap to be set to true @@ -152,6 +159,19 @@ } } + get dropShadowAlpha() + { + return this._dropShadowAlpha; + } + set dropShadowAlpha(dropShadowAlpha) + { + if (this._dropShadowAlpha !== dropShadowAlpha) + { + this._dropShadowAlpha = dropShadowAlpha; + this.styleID++; + } + } + get dropShadowAngle() { return this._dropShadowAngle; @@ -232,6 +252,19 @@ } } + get fillGradientStops() + { + return this._fillGradientStops; + } + set fillGradientStops(fillGradientStops) + { + if (!areArraysEqual(this._fillGradientStops,fillGradientStops)) + { + this._fillGradientStops = fillGradientStops; + this.styleID++; + } + } + get fontFamily() { return this._fontFamily; @@ -402,6 +435,19 @@ } } + get trim() + { + return this._trim; + } + set trim(trim) + { + if (this._trim !== trim) + { + this._trim = trim; + this.styleID++; + } + } + get wordWrap() { return this._wordWrap; @@ -475,3 +521,34 @@ return color; } } + +/** + * Utility function to convert hexadecimal colors to strings, and simply return the color if it's a string. + * This version can also convert array of colors + * + * @param {Array} array1 First array to compare + * @param {Array} array2 Second array to compare + * @return {boolean} Do the arrays contain the same values in the same order + */ +function areArraysEqual(array1, array2) +{ + if (!Array.isArray(array1) || !Array.isArray(array2)) + { + return false; + } + + if (array1.length !== array2.length) + { + return false; + } + + for (let i = 0; i < array1.length; ++i) + { + if (array1[i] !== array2[i]) + { + return false; + } + } + + return true; +} diff --git a/src/core/textures/BaseRenderTexture.js b/src/core/textures/BaseRenderTexture.js index 5d50146..aa727f0 100644 --- a/src/core/textures/BaseRenderTexture.js +++ b/src/core/textures/BaseRenderTexture.js @@ -1,8 +1,6 @@ import BaseTexture from './BaseTexture'; import settings from '../settings'; -const { RESOLUTION, SCALE_MODE } = settings; - /** * A BaseRenderTexture is a special texture that allows any Pixi display object to be rendered to it. * @@ -54,7 +52,7 @@ { super(null, scaleMode); - this.resolution = resolution || RESOLUTION; + this.resolution = resolution || settings.RESOLUTION; this.width = width; this.height = height; @@ -62,7 +60,7 @@ this.realWidth = this.width * this.resolution; this.realHeight = this.height * this.resolution; - this.scaleMode = scaleMode || SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; this.hasLoaded = true; /** diff --git a/src/core/textures/BaseTexture.js b/src/core/textures/BaseTexture.js index 676dca3..aeccf39 100644 --- a/src/core/textures/BaseTexture.js +++ b/src/core/textures/BaseTexture.js @@ -77,7 +77,7 @@ * @default PIXI.settings.SCALE_MODE * @see PIXI.SCALE_MODES */ - this.scaleMode = scaleMode || settings.SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; /** * Set to true once the base texture has successfully loaded. @@ -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/RenderTexture.js b/src/core/textures/RenderTexture.js index 4c5034f..b3c6297 100644 --- a/src/core/textures/RenderTexture.js +++ b/src/core/textures/RenderTexture.js @@ -54,8 +54,8 @@ /* eslint-disable prefer-rest-params, no-console */ const width = arguments[1]; const height = arguments[2]; - const scaleMode = arguments[3] || 0; - const resolution = arguments[4] || 1; + const scaleMode = arguments[3]; + const resolution = arguments[4]; // we have an old render texture.. console.warn(`Please use RenderTexture.create(${width}, ${height}) instead of the ctor directly.`); 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 ca31456..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 @@ -459,7 +497,9 @@ if (frame.x + frame.width > this.baseTexture.width || frame.y + frame.height > this.baseTexture.height) { - throw new Error(`Texture Error: frame does not fit inside the base Texture dimensions ${this}`); + throw new Error('Texture Error: frame does not fit inside the base Texture dimensions: ' + + `X: ${frame.x} + ${frame.width} > ${this.baseTexture.width} ` + + `Y: ${frame.y} + ${frame.height} > ${this.baseTexture.height}`); } // this.valid = frame && frame.width && frame.height && this.baseTexture.source && this.baseTexture.hasLoaded; @@ -520,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. @@ -528,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 2a7497b..99e585a 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)); } /** @@ -360,3 +362,43 @@ * @private */ export const BaseTextureCache = {}; + +/** + * 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/core/utils/trimCanvas.js b/src/core/utils/trimCanvas.js new file mode 100644 index 0000000..5af6de0 --- /dev/null +++ b/src/core/utils/trimCanvas.js @@ -0,0 +1,83 @@ +/** + * Trim transparent borders from a canvas + * + * @memberof PIXI + * @function trimCanvas + * @private + * @param {HTMLCanvasElement} canvas - the canvas to trim + * @returns {object} Trim data + */ +export default function trimCanvas(canvas) +{ + // https://gist.github.com/remy/784508 + + let width = canvas.width; + let height = canvas.height; + + const context = canvas.getContext('2d'); + const imageData = context.getImageData(0, 0, width, height); + const pixels = imageData.data; + const len = pixels.length; + + const bound = { + top: null, + left: null, + right: null, + bottom: null, + }; + let i; + let x; + let y; + + for (i = 0; i < len; i += 4) + { + if (pixels[i + 3] !== 0) + { + x = (i / 4) % width; + y = ~~((i / 4) / width); + + if (bound.top === null) + { + bound.top = y; + } + + if (bound.left === null) + { + bound.left = x; + } + else if (x < bound.left) + { + bound.left = x; + } + + if (bound.right === null) + { + bound.right = x + 1; + } + else if (bound.right < x) + { + bound.right = x + 1; + } + + if (bound.bottom === null) + { + bound.bottom = y; + } + else if (bound.bottom < y) + { + bound.bottom = y; + } + } + } + + width = bound.right - bound.left; + height = bound.bottom - bound.top + 1; + + const data = context.getImageData(bound.left, bound.top, width, height); + + return { + height, + width, + data, + }; +} diff --git a/src/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/extras/TilingSprite.js b/src/extras/TilingSprite.js index f90d248..fe82451 100644 --- a/src/extras/TilingSprite.js +++ b/src/extras/TilingSprite.js @@ -220,20 +220,33 @@ transform.tx * resolution, transform.ty * resolution); - // TODO - this should be rolled into the setTransform above.. - context.scale(this.tileScale.x / baseTextureResolution, this.tileScale.y / baseTextureResolution); - - context.translate(modX + (this.anchor.x * -this._width), - modY + (this.anchor.y * -this._height)); - renderer.setBlendMode(this.blendMode); // fill the pattern! context.fillStyle = this._canvasPattern; - context.fillRect(-modX, - -modY, - this._width / this.tileScale.x * baseTextureResolution, - this._height / this.tileScale.y * baseTextureResolution); + + // TODO - this should be rolled into the setTransform above.. + context.scale(this.tileScale.x / baseTextureResolution, this.tileScale.y / baseTextureResolution); + + const anchorX = this.anchor.x * -this._width; + const anchorY = this.anchor.y * -this._height; + + if (this.uvRespectAnchor) + { + context.translate(modX, modY); + + context.fillRect(-modX + anchorX, -modY + anchorY, + this._width / this.tileScale.x * baseTextureResolution, + this._height / this.tileScale.y * baseTextureResolution); + } + else + { + context.translate(modX + anchorX, modY + anchorY); + + context.fillRect(-modX, -modY, + this._width / this.tileScale.x * baseTextureResolution, + this._height / this.tileScale.y * baseTextureResolution); + } } /** diff --git a/src/extras/cacheAsBitmap.js b/src/extras/cacheAsBitmap.js index 18043a7..5dd5640 100644 --- a/src/extras/cacheAsBitmap.js +++ b/src/extras/cacheAsBitmap.js @@ -41,6 +41,9 @@ * provide a performance benefit for complex static displayObjects. * To remove simply set this property to 'false' * + * IMPORTANT GOTCHA - make sure that all your textures are preloaded BEFORE setting this property to true + * as it will take a snapshot of what is currently there. If the textures have not loaded then they will not appear. + * * @member {boolean} * @memberof PIXI.DisplayObject# */ diff --git a/src/filters/blur/BlurFilter.js b/src/filters/blur/BlurFilter.js index 340fc58..1575db9 100644 --- a/src/filters/blur/BlurFilter.js +++ b/src/filters/blur/BlurFilter.js @@ -24,10 +24,9 @@ this.blurXFilter = new BlurXFilter(strength, quality, resolution, kernelSize); this.blurYFilter = new BlurYFilter(strength, quality, resolution, kernelSize); - this.resolution = 1; this.padding = 0; - this.resolution = resolution || 1; + this.resolution = resolution || core.settings.RESOLUTION; this.quality = quality || 4; this.blur = strength || 8; } diff --git a/src/filters/blur/BlurXFilter.js b/src/filters/blur/BlurXFilter.js index def99c3..0f48889 100644 --- a/src/filters/blur/BlurXFilter.js +++ b/src/filters/blur/BlurXFilter.js @@ -31,7 +31,7 @@ fragSrc ); - this.resolution = resolution || 1; + this.resolution = resolution || core.settings.RESOLUTION; this._quality = 0; diff --git a/src/filters/blur/BlurYFilter.js b/src/filters/blur/BlurYFilter.js index 638e0f0..6d489c7 100644 --- a/src/filters/blur/BlurYFilter.js +++ b/src/filters/blur/BlurYFilter.js @@ -31,7 +31,7 @@ fragSrc ); - this.resolution = resolution || 1; + this.resolution = resolution || core.settings.RESOLUTION; this._quality = 0; diff --git a/src/filters/colormatrix/ColorMatrixFilter.js b/src/filters/colormatrix/ColorMatrixFilter.js index 8509505..679f8ec 100644 --- a/src/filters/colormatrix/ColorMatrixFilter.js +++ b/src/filters/colormatrix/ColorMatrixFilter.js @@ -75,28 +75,28 @@ out[1] = (a[0] * b[1]) + (a[1] * b[6]) + (a[2] * b[11]) + (a[3] * b[16]); out[2] = (a[0] * b[2]) + (a[1] * b[7]) + (a[2] * b[12]) + (a[3] * b[17]); out[3] = (a[0] * b[3]) + (a[1] * b[8]) + (a[2] * b[13]) + (a[3] * b[18]); - out[4] = (a[0] * b[4]) + (a[1] * b[9]) + (a[2] * b[14]) + (a[3] * b[19]); + out[4] = (a[0] * b[4]) + (a[1] * b[9]) + (a[2] * b[14]) + (a[3] * b[19]) + a[4]; // Green Channel out[5] = (a[5] * b[0]) + (a[6] * b[5]) + (a[7] * b[10]) + (a[8] * b[15]); out[6] = (a[5] * b[1]) + (a[6] * b[6]) + (a[7] * b[11]) + (a[8] * b[16]); out[7] = (a[5] * b[2]) + (a[6] * b[7]) + (a[7] * b[12]) + (a[8] * b[17]); out[8] = (a[5] * b[3]) + (a[6] * b[8]) + (a[7] * b[13]) + (a[8] * b[18]); - out[9] = (a[5] * b[4]) + (a[6] * b[9]) + (a[7] * b[14]) + (a[8] * b[19]); + out[9] = (a[5] * b[4]) + (a[6] * b[9]) + (a[7] * b[14]) + (a[8] * b[19]) + a[9]; // Blue Channel out[10] = (a[10] * b[0]) + (a[11] * b[5]) + (a[12] * b[10]) + (a[13] * b[15]); out[11] = (a[10] * b[1]) + (a[11] * b[6]) + (a[12] * b[11]) + (a[13] * b[16]); out[12] = (a[10] * b[2]) + (a[11] * b[7]) + (a[12] * b[12]) + (a[13] * b[17]); out[13] = (a[10] * b[3]) + (a[11] * b[8]) + (a[12] * b[13]) + (a[13] * b[18]); - out[14] = (a[10] * b[4]) + (a[11] * b[9]) + (a[12] * b[14]) + (a[13] * b[19]); + out[14] = (a[10] * b[4]) + (a[11] * b[9]) + (a[12] * b[14]) + (a[13] * b[19]) + a[14]; // Alpha Channel out[15] = (a[15] * b[0]) + (a[16] * b[5]) + (a[17] * b[10]) + (a[18] * b[15]); out[16] = (a[15] * b[1]) + (a[16] * b[6]) + (a[17] * b[11]) + (a[18] * b[16]); out[17] = (a[15] * b[2]) + (a[16] * b[7]) + (a[17] * b[12]) + (a[18] * b[17]); out[18] = (a[15] * b[3]) + (a[16] * b[8]) + (a[17] * b[13]) + (a[18] * b[18]); - out[19] = (a[15] * b[4]) + (a[16] * b[9]) + (a[17] * b[14]) + (a[18] * b[19]); + out[19] = (a[15] * b[4]) + (a[16] * b[9]) + (a[17] * b[14]) + (a[18] * b[19]) + a[19]; return out; } diff --git a/src/filters/colormatrix/colorMatrix.frag b/src/filters/colormatrix/colorMatrix.frag index 3aaf2b3..c73c0e9 100644 --- a/src/filters/colormatrix/colorMatrix.frag +++ b/src/filters/colormatrix/colorMatrix.frag @@ -4,32 +4,38 @@ void main(void) { - vec4 c = texture2D(uSampler, vTextureCoord); + // Un-premultiply alpha before applying the color matrix. See issue #3539. + if (c.a > 0.0) { + c.rgb /= c.a; + } + vec4 result; + result.r = (m[0] * c.r); + result.r += (m[1] * c.g); + result.r += (m[2] * c.b); + result.r += (m[3] * c.a); + result.r += m[4]; - gl_FragColor.r = (m[0] * c.r); - gl_FragColor.r += (m[1] * c.g); - gl_FragColor.r += (m[2] * c.b); - gl_FragColor.r += (m[3] * c.a); - gl_FragColor.r += m[4] * c.a; + result.g = (m[5] * c.r); + result.g += (m[6] * c.g); + result.g += (m[7] * c.b); + result.g += (m[8] * c.a); + result.g += m[9]; - gl_FragColor.g = (m[5] * c.r); - gl_FragColor.g += (m[6] * c.g); - gl_FragColor.g += (m[7] * c.b); - gl_FragColor.g += (m[8] * c.a); - gl_FragColor.g += m[9] * c.a; + result.b = (m[10] * c.r); + result.b += (m[11] * c.g); + result.b += (m[12] * c.b); + result.b += (m[13] * c.a); + result.b += m[14]; - gl_FragColor.b = (m[10] * c.r); - gl_FragColor.b += (m[11] * c.g); - gl_FragColor.b += (m[12] * c.b); - gl_FragColor.b += (m[13] * c.a); - gl_FragColor.b += m[14] * c.a; + result.a = (m[15] * c.r); + result.a += (m[16] * c.g); + result.a += (m[17] * c.b); + result.a += (m[18] * c.a); + result.a += m[19]; - gl_FragColor.a = (m[15] * c.r); - gl_FragColor.a += (m[16] * c.g); - gl_FragColor.a += (m[17] * c.b); - gl_FragColor.a += (m[18] * c.a); - gl_FragColor.a += m[19] * c.a; + // Premultiply alpha again. + result.rgb *= result.a; -// gl_FragColor = vec4(m[0]); + gl_FragColor = result; } diff --git a/src/filters/displacement/DisplacementFilter.js b/src/filters/displacement/DisplacementFilter.js index 112de0b..d5355c3 100644 --- a/src/filters/displacement/DisplacementFilter.js +++ b/src/filters/displacement/DisplacementFilter.js @@ -36,7 +36,7 @@ this.maskMatrix = maskMatrix; this.uniforms.mapSampler = sprite.texture; - this.uniforms.filterMatrix = maskMatrix.toArray(true); + this.uniforms.filterMatrix = maskMatrix; this.uniforms.scale = { x: 1, y: 1 }; if (scale === null || scale === undefined) diff --git a/src/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/InteractionData.js b/src/interaction/InteractionData.js index 02d53a3..818da35 100644 --- a/src/interaction/InteractionData.js +++ b/src/interaction/InteractionData.js @@ -21,9 +21,9 @@ this.global = new core.Point(); /** - * The target Sprite that was interacted with + * The target DisplayObject that was interacted with * - * @member {PIXI.Sprite} + * @member {PIXI.DisplayObject} */ this.target = null; @@ -33,6 +33,13 @@ * @member {Event} */ this.originalEvent = null; + + /** + * Unique identifier for this interaction + * + * @member {number} + */ + this.identifier = null; } /** diff --git a/src/interaction/InteractionEvent.js b/src/interaction/InteractionEvent.js index 75b2966..dc554f9 100644 --- a/src/interaction/InteractionEvent.js +++ b/src/interaction/InteractionEvent.js @@ -58,7 +58,7 @@ } /** - * Prevents event from reaching any objects other than the current object. + * Resets the event. * * @private */ diff --git a/src/interaction/InteractionManager.js b/src/interaction/InteractionManager.js index 7d51134..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/src/prepare/BasePrepare.js b/src/prepare/BasePrepare.js index a2005f5..09eb5e5 100644 --- a/src/prepare/BasePrepare.js +++ b/src/prepare/BasePrepare.js @@ -20,7 +20,7 @@ * * @abstract * @class - * @memberof PIXI + * @memberof PIXI.prepare */ export default class BasePrepare { @@ -107,9 +107,9 @@ /** * Upload all the textures and graphics to the GPU. * - * @param {Function|PIXI.DisplayObject|PIXI.Container} item - Either - * the container or display object to search for items to upload or - * the callback function, if items have been added using `prepare.add`. + * @param {Function|PIXI.DisplayObject|PIXI.Container|PIXI.BaseTexture|PIXI.Texture|PIXI.Graphics|PIXI.Text} item - + * Either the container or display object to search for items to upload, the items to upload themselves, + * or the callback function, if items have been added using `prepare.add`. * @param {Function} [done] - Optional callback when all queued uploads have completed */ upload(item, done) @@ -236,7 +236,8 @@ /** * Manually add an item to the uploading queue. * - * @param {PIXI.DisplayObject|PIXI.Container|*} item - Object to add to the queue + * @param {PIXI.DisplayObject|PIXI.Container|PIXI.BaseTexture|PIXI.Texture|PIXI.Graphics|PIXI.Text|*} item - Object to + * add to the queue * @return {PIXI.CanvasPrepare} Instance of plugin for chaining. */ add(item) diff --git a/src/prepare/canvas/CanvasPrepare.js b/src/prepare/canvas/CanvasPrepare.js index 4b99d22..0224bb5 100644 --- a/src/prepare/canvas/CanvasPrepare.js +++ b/src/prepare/canvas/CanvasPrepare.js @@ -12,7 +12,8 @@ * An instance of this class is automatically created by default, and can be found at renderer.plugins.prepare * * @class - * @memberof PIXI + * @extends PIXI.prepare.BasePrepare + * @memberof PIXI.prepare */ export default class CanvasPrepare extends BasePrepare { diff --git a/src/prepare/webgl/WebGLPrepare.js b/src/prepare/webgl/WebGLPrepare.js index 1ef7467..eb7023f 100644 --- a/src/prepare/webgl/WebGLPrepare.js +++ b/src/prepare/webgl/WebGLPrepare.js @@ -7,7 +7,8 @@ * An instance of this class is automatically created by default, and can be found at renderer.plugins.prepare * * @class - * @memberof PIXI + * @extends PIXI.prepare.BasePrepare + * @memberof PIXI.prepare */ export default class WebGLPrepare extends BasePrepare { diff --git a/test/core/Circle.js b/test/core/Circle.js index 183f98f..f621992 100644 --- a/test/core/Circle.js +++ b/test/core/Circle.js @@ -53,6 +53,10 @@ expect(circ1.contains(10, 16)).to.be.false; expect(circ1.contains(11, 15)).to.be.false; expect(circ1.contains(0, 0)).to.be.false; + + const circ2 = new PIXI.Circle(10, 10, 0); + + expect(circ2.contains(10, 10)).to.be.false; }); it('should return framing rectangle', function () diff --git a/test/core/Container.js b/test/core/Container.js index 8a10b40..cefde54 100644 --- a/test/core/Container.js +++ b/test/core/Container.js @@ -1,5 +1,39 @@ 'use strict'; +function testAddChild(fn) +{ + return function () + { + fn(function (container, obj) + { + container.addChild(obj); + }); + fn(function (container, obj) + { + container.addChildAt(obj); + }); + }; +} + +function testRemoveChild(fn) +{ + return function () + { + 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 +104,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 +279,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 1271fef..71ade15 100644 --- a/package.json +++ b/package.json @@ -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/scripts/release.js b/scripts/release.js index c4c8e36..156620c 100644 --- a/scripts/release.js +++ b/scripts/release.js @@ -12,7 +12,8 @@ 'dist/**/*', 'lib/**/*', 'src/**/*', - 'scripts/**/*', + 'scripts/*', + 'scripts/renders/*', 'test/**/*', '*.json', '*.md', diff --git a/scripts/renders/client.js b/scripts/renders/client.js index 16c9565..6faee34 100755 --- a/scripts/renders/client.js +++ b/scripts/renders/client.js @@ -1,6 +1,6 @@ 'use strict'; -require('../../bin/pixi'); +require('../../dist/pixi'); PIXI.utils.skipHello(); const fs = require('fs'); 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 bc730db..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. @@ -33,8 +33,11 @@ * need to call toDataUrl on the webgl context * @param {number} [options.resolution=1] - The resolution / device pixel ratio of the renderer, retina would be 2 * @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 @@ -43,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. */ @@ -73,7 +99,7 @@ */ stop() { - this.ticker.stop(); + this._ticker.stop(); } /** @@ -81,7 +107,7 @@ */ start() { - this.ticker.start(); + this._ticker.start(); } /** @@ -95,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 index 81e076b..78b17e6 100644 --- a/src/core/Shader.js +++ b/src/core/Shader.js @@ -1,9 +1,7 @@ import { GLShader } from 'pixi-gl-core'; import settings from './settings'; -const { PRECISION } = settings; - -function checkPrecision(src) +function checkPrecision(src, def) { if (src instanceof Array) { @@ -11,14 +9,14 @@ { const copy = src.slice(0); - copy.unshift(`precision ${PRECISION} float;`); + copy.unshift(`precision ${def} float;`); return copy; } } else if (src.substring(0, 9) !== 'precision') { - return `precision ${PRECISION} float;\n${src}`; + return `precision ${def} float;\n${src}`; } return src; @@ -42,6 +40,7 @@ */ constructor(gl, vertexSrc, fragmentSrc) { - super(gl, checkPrecision(vertexSrc), checkPrecision(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 3425b41..f9235e4 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -181,7 +181,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 b3792ef..17fde8d 100644 --- a/src/core/display/Container.js +++ b/src/core/display/Container.js @@ -73,13 +73,14 @@ } child.parent = this; - - // ensure a transform will be recalculated.. - this.transform._parentID = -1; - this._boundsID++; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.push(child); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(this.children.length - 1); child.emit('added', this); @@ -108,9 +109,14 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.splice(index, 0, child); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(index); child.emit('added', this); @@ -174,6 +180,7 @@ removeItems(this.children, currentIndex, 1); // remove from old position this.children.splice(index, 0, child); // add at new position + this.onChildrenChange(index); } @@ -220,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! @@ -244,9 +252,14 @@ { const child = this.getChildAt(index); + // ensure child transform will be recalculated.. child.parent = null; + child.transform._parentID = -1; removeItems(this.children, index, 1); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(index); child.emit('removed', this); @@ -275,8 +288,14 @@ for (let i = 0; i < removed.length; ++i) { removed[i].parent = null; + if (removed[i].transform) + { + removed[i].transform._parentID = -1; + } } + this._boundsID++; + this.onChildrenChange(beginIndex); for (let i = 0; i < removed.length; ++i) diff --git a/src/core/display/DisplayObject.js b/src/core/display/DisplayObject.js index 3affe36..4e6c77d 100644 --- a/src/core/display/DisplayObject.js +++ b/src/core/display/DisplayObject.js @@ -109,7 +109,7 @@ /** * The original, cached mask of the object * - * @member {PIXI.Rectangle} + * @member {PIXI.Graphics|PIXI.Sprite} * @private */ this._mask = null; diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index 95f2af4..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 ); @@ -1065,10 +1061,15 @@ canvasRenderer = new CanvasRenderer(); } - tempMatrix.tx = -bounds.x; - tempMatrix.ty = -bounds.y; + this.transform.updateLocalTransform(); + this.transform.localTransform.copy(tempMatrix); - canvasRenderer.render(this, canvasBuffer, false, tempMatrix); + tempMatrix.invert(); + + tempMatrix.tx -= bounds.x; + tempMatrix.ty -= bounds.y; + + canvasRenderer.render(this, canvasBuffer, true, tempMatrix); const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode); 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 e67680f..867dde2 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/buildCircle.js b/src/core/graphics/webgl/utils/buildCircle.js index 0400d13..abf5d02 100644 --- a/src/core/graphics/webgl/utils/buildCircle.js +++ b/src/core/graphics/webgl/utils/buildCircle.js @@ -33,6 +33,11 @@ height = circleData.height; } + if (width === 0 || height === 0) + { + return; + } + const totalSegs = Math.floor(30 * Math.sqrt(circleData.radius)) || Math.floor(15 * Math.sqrt(circleData.width + circleData.height)); diff --git a/src/core/graphics/webgl/utils/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 90155f9..f03fe15 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -30,6 +30,7 @@ export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; export { default as CanvasGraphicsRenderer } from './graphics/canvas/CanvasGraphicsRenderer'; +export { default as Spritesheet } from './textures/Spritesheet'; export { default as Texture } from './textures/Texture'; export { default as BaseTexture } from './textures/BaseTexture'; export { default as RenderTexture } from './textures/RenderTexture'; diff --git a/src/core/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 ccce680..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); } /** @@ -162,6 +162,9 @@ if (transform) { transform.copy(tempWt); + + // lets not forget to flag the parent transform as dirty... + this._tempDisplayObjectParent.transform._worldID = -1; } else { @@ -169,6 +172,7 @@ } displayObject.parent = this._tempDisplayObjectParent; + displayObject.updateTransform(); displayObject.parent = cacheParent; // displayObject.hitArea = //TODO add a temp hit area @@ -279,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/canvas/utils/CanvasRenderTarget.js b/src/core/renderers/canvas/utils/CanvasRenderTarget.js index 3de0aef..b409891 100644 --- a/src/core/renderers/canvas/utils/CanvasRenderTarget.js +++ b/src/core/renderers/canvas/utils/CanvasRenderTarget.js @@ -1,5 +1,4 @@ import settings from '../../../settings'; -const { RESOLUTION } = settings; /** * Creates a Canvas element of the given size. @@ -30,7 +29,7 @@ */ this.context = this.canvas.getContext('2d'); - this.resolution = resolution || RESOLUTION; + this.resolution = resolution || settings.RESOLUTION; this.resize(width, height); } 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 ce0e1c4..788ed6c 100644 --- a/src/core/renderers/webgl/WebGLRenderer.js +++ b/src/core/renderers/webgl/WebGLRenderer.js @@ -30,8 +30,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 @@ -49,10 +49,19 @@ * enable this if you need to call toDataUrl on the webgl context. * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when * rendering, stopping pixel interpolation. + * @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; + + if (this.legacy) + { + glCore.VertexArrayObject.FORCE_NATIVE = true; + } /** * The type of this renderer as a standardised const @@ -229,7 +238,7 @@ this.emit('context', gl); // setup the width/height properties and gl viewport - this.resize(this.width, this.height); + this.resize(this.screen.width, this.screen.height); } /** @@ -323,16 +332,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) { @@ -376,6 +385,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 @@ -440,9 +469,10 @@ * Changes the current shader to the one given in parameter * * @param {PIXI.Shader} shader - the new shader + * @param {boolean} [autoProject=true] - Whether automatically set the projection matrix * @return {PIXI.WebGLRenderer} Returns itself. */ - bindShader(shader) + bindShader(shader, autoProject) { // TODO cache if (this._activeShader !== shader) @@ -450,8 +480,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 bbab3b3..944f41d 100644 --- a/src/core/renderers/webgl/filters/Filter.js +++ b/src/core/renderers/webgl/filters/Filter.js @@ -1,6 +1,7 @@ import extractUniformsFromSrc from './extractUniformsFromSrc'; import { uid } from '../../../utils'; import { BLEND_MODES } from '../../../const'; +import settings from '../../../settings'; const SOURCE_KEY_MAP = {}; @@ -10,7 +11,7 @@ * @memberof PIXI * @extends PIXI.Shader */ -class Filter +export default class Filter { /** * @param {string} [vertexSrc] - The source of the vertex shader. @@ -35,8 +36,6 @@ this.blendMode = BLEND_MODES.NORMAL; - // pull out the vertex and shader uniforms if they are not specified.. - // currently this does not extract structs only default types this.uniformData = uniforms || extractUniformsFromSrc(this.vertexSrc, this.fragmentSrc, 'projectionMatrix|uSampler'); /** @@ -80,7 +79,7 @@ * * @member {number} */ - this.resolution = 1; + this.resolution = settings.RESOLUTION; /** * If enabled is true the filter is applied, if false it will not. @@ -97,8 +96,11 @@ * @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 { // --- // // this.uniforms.filterMatrix = filterManager.calculateSpriteMatrix(tempMatrix, window.panda ); @@ -170,5 +172,3 @@ ].join('\n'); } } - -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 dc7072a..6c7422d 100644 --- a/src/core/renderers/webgl/managers/FilterManager.js +++ b/src/core/renderers/webgl/managers/FilterManager.js @@ -154,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 @@ -176,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; @@ -184,7 +184,7 @@ flop = t; } - filters[i].apply(this, flip, lastState.renderTarget, true); + filters[i].apply(this, flip, lastState.renderTarget, false, currentState); this.freePotRenderTarget(flip); this.freePotRenderTarget(flop); @@ -291,9 +291,13 @@ let textureCount = 1; let currentState; - if (shader.uniforms.data.filterArea) + // 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; @@ -306,9 +310,9 @@ // use this to clamp displaced texture coords so they belong to filterArea // see displacementFilter fragment shader for an example - if (shader.uniforms.data.filterClamp) + if (shader.uniforms.filterClamp) { - currentState = this.filterData.stack[this.filterData.index]; + currentState = currentState || this.filterData.stack[this.filterData.index]; const filterClamp = shader.uniforms.filterClamp; @@ -485,7 +489,7 @@ */ destroy() { - this.shaderCache = []; + this.shaderCache = {}; this.emptyPool(); } diff --git a/src/core/renderers/webgl/utils/RenderTarget.js b/src/core/renderers/webgl/utils/RenderTarget.js index afe51ab..6d2467f 100644 --- a/src/core/renderers/webgl/utils/RenderTarget.js +++ b/src/core/renderers/webgl/utils/RenderTarget.js @@ -124,7 +124,7 @@ * @default PIXI.settings.SCALE_MODE * @see PIXI.SCALE_MODES */ - this.scaleMode = scaleMode || settings.SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; /** * Whether this object is the root element or not 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 36ea5bb..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; @@ -346,8 +346,8 @@ /** * Gets the local bounds of the sprite object. * - * @param {Rectangle} rect - The output rectangle. - * @return {Rectangle} The bounds. + * @param {PIXI.Rectangle} rect - The output rectangle. + * @return {PIXI.Rectangle} The bounds. */ getLocalBounds(rect) { @@ -440,7 +440,7 @@ * * @static * @param {number|string|PIXI.BaseTexture|HTMLCanvasElement|HTMLVideoElement} source Source to create texture from - * @return {PIXI.Texture} 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 e77b39a..90b08c0 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++) @@ -103,11 +102,18 @@ { const gl = this.renderer.gl; - // step 1: first check max textures the GPU can handle. - this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); + if (this.renderer.legacy) + { + this.MAX_TEXTURES = 1; + } + else + { + // step 1: first check max textures the GPU can handle. + this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); - // step 2: check the maximum number of if statements the shader can have too.. - this.MAX_TEXTURES = checkMaxIfStatmentsInShader(this.MAX_TEXTURES, gl); + // step 2: check the maximum number of if statements the shader can have too.. + this.MAX_TEXTURES = checkMaxIfStatmentsInShader(this.MAX_TEXTURES, gl); + } const shader = this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); @@ -130,8 +136,12 @@ .addIndex(this.indexBuffer) .addAttribute(this.vertexBuffers[i], shader.attributes.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) .addAttribute(this.vertexBuffers[i], shader.attributes.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) - .addAttribute(this.vertexBuffers[i], shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4) - .addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + .addAttribute(this.vertexBuffers[i], shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); + + if (shader.attributes.aTextureId) + { + this.vaos[i].addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + } /* eslint-enable max-len */ } @@ -378,8 +388,12 @@ .addIndex(this.indexBuffer) .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) - .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4) - .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); + + if (this.shader.attributes.aTextureId) + { + this.vaos[this.vertexCount].addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + } /* eslint-enable max-len */ } diff --git a/src/core/text/Text.js b/src/core/text/Text.js index 013625a..cec8c32 100644 --- a/src/core/text/Text.js +++ b/src/core/text/Text.js @@ -6,6 +6,7 @@ import { TEXT_GRADIENT } from '../const'; import settings from '../settings'; import TextStyle from './TextStyle'; +import trimCanvas from '../utils/trimCanvas'; const defaultDestroyOptions = { texture: true, @@ -57,7 +58,7 @@ /** * The canvas 2d context that everything is drawn with - * @member {HTMLCanvasElement} + * @member {CanvasRenderingContext2D} */ this.context = this.canvas.getContext('2d'); @@ -189,10 +190,12 @@ if (style.dropShadow) { + this.context.shadowBlur = style.dropShadowBlur; + this.context.globalAlpha = style.dropShadowAlpha; + if (style.dropShadowBlur > 0) { this.context.shadowColor = style.dropShadowColor; - this.context.shadowBlur = style.dropShadowBlur; } else { @@ -237,6 +240,10 @@ } } + // reset the shadow blur and alpha that was set by the drop shadow, for the regular text + this.context.shadowBlur = 0; + this.context.globalAlpha = 1; + // set canvas text styles this.context.fillStyle = this._generateFillStyle(style, lines); @@ -326,6 +333,15 @@ */ updateTexture() { + if (this._style.trim) + { + const trimmed = trimCanvas(this.canvas); + + this.canvas.width = trimmed.width; + this.canvas.height = trimmed.height; + this.context.putImageData(trimmed.data, 0, 0); + } + const texture = this._texture; const style = this._style; @@ -475,6 +491,19 @@ } /** + * Gets the local bounds of the text object. + * + * @param {Rectangle} rect - The output rectangle. + * @return {Rectangle} The bounds. + */ + getLocalBounds(rect) + { + this.updateText(true); + + return super.getLocalBounds.call(this, rect); + } + + /** * calculates the bounds of the Text as a rectangle. The bounds calculation takes the worldTransform into account. */ _calculateBounds() @@ -525,6 +554,29 @@ const width = this.canvas.width / this.resolution; const height = this.canvas.height / this.resolution; + // make a copy of the style settings, so we can manipulate them later + const fill = style.fill.slice(); + const fillGradientStops = style.fillGradientStops.slice(); + + // wanting to evenly distribute the fills. So an array of 4 colours should give fills of 0.25, 0.5 and 0.75 + if (!fillGradientStops.length) + { + const lengthPlus1 = fill.length + 1; + + for (let i = 1; i < lengthPlus1; ++i) + { + fillGradientStops.push(i / lengthPlus1); + } + } + + // stop the bleeding of the last gradient on the line above to the top gradient of the this line + // by hard defining the first gradient colour at point 0, and last gradient colour at point 1 + fill.unshift(style.fill[0]); + fillGradientStops.unshift(0); + + fill.push(style.fill[style.fill.length - 1]); + fillGradientStops.push(1); + if (style.fillGradientType === TEXT_GRADIENT.LINEAR_VERTICAL) { // start the gradient at the top center of the canvas, and end at the bottom middle of the canvas @@ -532,15 +584,22 @@ // we need to repeat the gradient so that each individual line of text has the same vertical gradient effect // ['#FF0000', '#00FF00', '#0000FF'] over 2 lines would create stops at 0.125, 0.25, 0.375, 0.625, 0.75, 0.875 - totalIterations = (style.fill.length + 1) * lines.length; + totalIterations = (fill.length + 1) * lines.length; currentIteration = 0; for (let i = 0; i < lines.length; i++) { currentIteration += 1; - for (let j = 0; j < style.fill.length; j++) + for (let j = 0; j < fill.length; j++) { - stop = (currentIteration / totalIterations); - gradient.addColorStop(stop, style.fill[j]); + if (fillGradientStops[j]) + { + stop = (fillGradientStops[j] / lines.length) + (i / lines.length); + } + else + { + stop = currentIteration / totalIterations; + } + gradient.addColorStop(stop, fill[j]); currentIteration++; } } @@ -552,13 +611,20 @@ // can just evenly space out the gradients in this case, as multiple lines makes no difference // to an even left to right gradient - totalIterations = style.fill.length + 1; + totalIterations = fill.length + 1; currentIteration = 1; - for (let i = 0; i < style.fill.length; i++) + for (let i = 0; i < fill.length; i++) { - stop = currentIteration / totalIterations; - gradient.addColorStop(stop, style.fill[i]); + if (fillGradientStops[i]) + { + stop = fillGradientStops[i]; + } + else + { + stop = currentIteration / totalIterations; + } + gradient.addColorStop(stop, fill[i]); currentIteration++; } } @@ -569,7 +635,7 @@ /** * Destroys this text object. * Note* Unlike a Sprite, a Text object will automatically destroy its baseTexture and texture as - * the majorety of the time the texture will not be shared with any other Sprites. + * the majority of the time the texture will not be shared with any other Sprites. * * @param {object|boolean} [options] - Options parameter. A boolean will act as if all options * have been set to that value @@ -680,7 +746,7 @@ set text(text) // eslint-disable-line require-jsdoc { - text = String(text || ' '); + text = String(text === '' || text === null || text === undefined ? ' ' : text); if (this._text === text) { diff --git a/src/core/text/TextStyle.js b/src/core/text/TextStyle.js index 3b25528..890950b 100644 --- a/src/core/text/TextStyle.js +++ b/src/core/text/TextStyle.js @@ -8,12 +8,14 @@ align: 'left', breakWords: false, dropShadow: false, + dropShadowAlpha: 1, dropShadowAngle: Math.PI / 6, dropShadowBlur: 0, dropShadowColor: '#000000', dropShadowDistance: 5, fill: 'black', fillGradientType: TEXT_GRADIENT.LINEAR_VERTICAL, + fillGradientStops: [], fontFamily: 'Arial', fontSize: 26, fontStyle: 'normal', @@ -27,6 +29,7 @@ stroke: 'black', strokeThickness: 0, textBaseline: 'alphabetic', + trim: false, wordWrap: false, wordWrapWidth: 100, }; @@ -47,6 +50,7 @@ * @param {boolean} [style.breakWords=false] - Indicates if lines can be wrapped within words, it * needs wordWrap to be set to true * @param {boolean} [style.dropShadow=false] - Set a drop shadow for the text + * @param {number} [style.dropShadowAlpha=1] - Set alpha for the drop shadow * @param {number} [style.dropShadowAngle=Math.PI/6] - Set a angle of the drop shadow * @param {number} [style.dropShadowBlur=0] - Set a shadow blur radius * @param {string} [style.dropShadowColor='#000000'] - A fill style to be used on the dropshadow e.g 'red', '#00FF00' @@ -55,8 +59,10 @@ * fillstyle that will be used on the text e.g 'red', '#00FF00'. Can be an array to create a gradient * eg ['#000000','#FFFFFF'] * {@link https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/fillStyle|MDN} - * @param {number} [style.fillGradientType=PIXI.TEXT_GRADIENT.LINEAR_VERTICAL] - If fills styles are - * supplied, this can change the type/direction of the gradient. See {@link PIXI.TEXT_GRADIENT} for possible values + * @param {number} [style.fillGradientType=PIXI.TEXT_GRADIENT.LINEAR_VERTICAL] - If fill is an array of colours + * to create a gradient, this can change the type/direction of the gradient. See {@link PIXI.TEXT_GRADIENT} + * @param {number[]} [style.fillGradientStops] - If fill is an array of colours to create a gradient, this array can set + * the stop points (numbers between 0 and 1) for the color, overriding the default behaviour of evenly spacing them. * @param {string|string[]} [style.fontFamily='Arial'] - The font family * @param {number|string} [style.fontSize=26] - The font size (as a number it converts to px, but as a string, * equivalents are '26px','20pt','160%' or '1.6em') @@ -76,6 +82,7 @@ * e.g 'blue', '#FCFF00' * @param {number} [style.strokeThickness=0] - A number that represents the thickness of the stroke. * Default is 0 (no stroke) + * @param {boolean} [style.trim=false] - Trim transparent borders * @param {string} [style.textBaseline='alphabetic'] - The baseline of the text that is rendered. * @param {boolean} [style.wordWrap=false] - Indicates if word wrap should be used * @param {number} [style.wordWrapWidth=100] - The width at which text will wrap, it needs wordWrap to be set to true @@ -152,6 +159,19 @@ } } + get dropShadowAlpha() + { + return this._dropShadowAlpha; + } + set dropShadowAlpha(dropShadowAlpha) + { + if (this._dropShadowAlpha !== dropShadowAlpha) + { + this._dropShadowAlpha = dropShadowAlpha; + this.styleID++; + } + } + get dropShadowAngle() { return this._dropShadowAngle; @@ -232,6 +252,19 @@ } } + get fillGradientStops() + { + return this._fillGradientStops; + } + set fillGradientStops(fillGradientStops) + { + if (!areArraysEqual(this._fillGradientStops,fillGradientStops)) + { + this._fillGradientStops = fillGradientStops; + this.styleID++; + } + } + get fontFamily() { return this._fontFamily; @@ -402,6 +435,19 @@ } } + get trim() + { + return this._trim; + } + set trim(trim) + { + if (this._trim !== trim) + { + this._trim = trim; + this.styleID++; + } + } + get wordWrap() { return this._wordWrap; @@ -475,3 +521,34 @@ return color; } } + +/** + * Utility function to convert hexadecimal colors to strings, and simply return the color if it's a string. + * This version can also convert array of colors + * + * @param {Array} array1 First array to compare + * @param {Array} array2 Second array to compare + * @return {boolean} Do the arrays contain the same values in the same order + */ +function areArraysEqual(array1, array2) +{ + if (!Array.isArray(array1) || !Array.isArray(array2)) + { + return false; + } + + if (array1.length !== array2.length) + { + return false; + } + + for (let i = 0; i < array1.length; ++i) + { + if (array1[i] !== array2[i]) + { + return false; + } + } + + return true; +} diff --git a/src/core/textures/BaseRenderTexture.js b/src/core/textures/BaseRenderTexture.js index 5d50146..aa727f0 100644 --- a/src/core/textures/BaseRenderTexture.js +++ b/src/core/textures/BaseRenderTexture.js @@ -1,8 +1,6 @@ import BaseTexture from './BaseTexture'; import settings from '../settings'; -const { RESOLUTION, SCALE_MODE } = settings; - /** * A BaseRenderTexture is a special texture that allows any Pixi display object to be rendered to it. * @@ -54,7 +52,7 @@ { super(null, scaleMode); - this.resolution = resolution || RESOLUTION; + this.resolution = resolution || settings.RESOLUTION; this.width = width; this.height = height; @@ -62,7 +60,7 @@ this.realWidth = this.width * this.resolution; this.realHeight = this.height * this.resolution; - this.scaleMode = scaleMode || SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; this.hasLoaded = true; /** diff --git a/src/core/textures/BaseTexture.js b/src/core/textures/BaseTexture.js index 676dca3..aeccf39 100644 --- a/src/core/textures/BaseTexture.js +++ b/src/core/textures/BaseTexture.js @@ -77,7 +77,7 @@ * @default PIXI.settings.SCALE_MODE * @see PIXI.SCALE_MODES */ - this.scaleMode = scaleMode || settings.SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; /** * Set to true once the base texture has successfully loaded. @@ -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/RenderTexture.js b/src/core/textures/RenderTexture.js index 4c5034f..b3c6297 100644 --- a/src/core/textures/RenderTexture.js +++ b/src/core/textures/RenderTexture.js @@ -54,8 +54,8 @@ /* eslint-disable prefer-rest-params, no-console */ const width = arguments[1]; const height = arguments[2]; - const scaleMode = arguments[3] || 0; - const resolution = arguments[4] || 1; + const scaleMode = arguments[3]; + const resolution = arguments[4]; // we have an old render texture.. console.warn(`Please use RenderTexture.create(${width}, ${height}) instead of the ctor directly.`); 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 ca31456..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 @@ -459,7 +497,9 @@ if (frame.x + frame.width > this.baseTexture.width || frame.y + frame.height > this.baseTexture.height) { - throw new Error(`Texture Error: frame does not fit inside the base Texture dimensions ${this}`); + throw new Error('Texture Error: frame does not fit inside the base Texture dimensions: ' + + `X: ${frame.x} + ${frame.width} > ${this.baseTexture.width} ` + + `Y: ${frame.y} + ${frame.height} > ${this.baseTexture.height}`); } // this.valid = frame && frame.width && frame.height && this.baseTexture.source && this.baseTexture.hasLoaded; @@ -520,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. @@ -528,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 2a7497b..99e585a 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)); } /** @@ -360,3 +362,43 @@ * @private */ export const BaseTextureCache = {}; + +/** + * 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/core/utils/trimCanvas.js b/src/core/utils/trimCanvas.js new file mode 100644 index 0000000..5af6de0 --- /dev/null +++ b/src/core/utils/trimCanvas.js @@ -0,0 +1,83 @@ +/** + * Trim transparent borders from a canvas + * + * @memberof PIXI + * @function trimCanvas + * @private + * @param {HTMLCanvasElement} canvas - the canvas to trim + * @returns {object} Trim data + */ +export default function trimCanvas(canvas) +{ + // https://gist.github.com/remy/784508 + + let width = canvas.width; + let height = canvas.height; + + const context = canvas.getContext('2d'); + const imageData = context.getImageData(0, 0, width, height); + const pixels = imageData.data; + const len = pixels.length; + + const bound = { + top: null, + left: null, + right: null, + bottom: null, + }; + let i; + let x; + let y; + + for (i = 0; i < len; i += 4) + { + if (pixels[i + 3] !== 0) + { + x = (i / 4) % width; + y = ~~((i / 4) / width); + + if (bound.top === null) + { + bound.top = y; + } + + if (bound.left === null) + { + bound.left = x; + } + else if (x < bound.left) + { + bound.left = x; + } + + if (bound.right === null) + { + bound.right = x + 1; + } + else if (bound.right < x) + { + bound.right = x + 1; + } + + if (bound.bottom === null) + { + bound.bottom = y; + } + else if (bound.bottom < y) + { + bound.bottom = y; + } + } + } + + width = bound.right - bound.left; + height = bound.bottom - bound.top + 1; + + const data = context.getImageData(bound.left, bound.top, width, height); + + return { + height, + width, + data, + }; +} diff --git a/src/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/extras/TilingSprite.js b/src/extras/TilingSprite.js index f90d248..fe82451 100644 --- a/src/extras/TilingSprite.js +++ b/src/extras/TilingSprite.js @@ -220,20 +220,33 @@ transform.tx * resolution, transform.ty * resolution); - // TODO - this should be rolled into the setTransform above.. - context.scale(this.tileScale.x / baseTextureResolution, this.tileScale.y / baseTextureResolution); - - context.translate(modX + (this.anchor.x * -this._width), - modY + (this.anchor.y * -this._height)); - renderer.setBlendMode(this.blendMode); // fill the pattern! context.fillStyle = this._canvasPattern; - context.fillRect(-modX, - -modY, - this._width / this.tileScale.x * baseTextureResolution, - this._height / this.tileScale.y * baseTextureResolution); + + // TODO - this should be rolled into the setTransform above.. + context.scale(this.tileScale.x / baseTextureResolution, this.tileScale.y / baseTextureResolution); + + const anchorX = this.anchor.x * -this._width; + const anchorY = this.anchor.y * -this._height; + + if (this.uvRespectAnchor) + { + context.translate(modX, modY); + + context.fillRect(-modX + anchorX, -modY + anchorY, + this._width / this.tileScale.x * baseTextureResolution, + this._height / this.tileScale.y * baseTextureResolution); + } + else + { + context.translate(modX + anchorX, modY + anchorY); + + context.fillRect(-modX, -modY, + this._width / this.tileScale.x * baseTextureResolution, + this._height / this.tileScale.y * baseTextureResolution); + } } /** diff --git a/src/extras/cacheAsBitmap.js b/src/extras/cacheAsBitmap.js index 18043a7..5dd5640 100644 --- a/src/extras/cacheAsBitmap.js +++ b/src/extras/cacheAsBitmap.js @@ -41,6 +41,9 @@ * provide a performance benefit for complex static displayObjects. * To remove simply set this property to 'false' * + * IMPORTANT GOTCHA - make sure that all your textures are preloaded BEFORE setting this property to true + * as it will take a snapshot of what is currently there. If the textures have not loaded then they will not appear. + * * @member {boolean} * @memberof PIXI.DisplayObject# */ diff --git a/src/filters/blur/BlurFilter.js b/src/filters/blur/BlurFilter.js index 340fc58..1575db9 100644 --- a/src/filters/blur/BlurFilter.js +++ b/src/filters/blur/BlurFilter.js @@ -24,10 +24,9 @@ this.blurXFilter = new BlurXFilter(strength, quality, resolution, kernelSize); this.blurYFilter = new BlurYFilter(strength, quality, resolution, kernelSize); - this.resolution = 1; this.padding = 0; - this.resolution = resolution || 1; + this.resolution = resolution || core.settings.RESOLUTION; this.quality = quality || 4; this.blur = strength || 8; } diff --git a/src/filters/blur/BlurXFilter.js b/src/filters/blur/BlurXFilter.js index def99c3..0f48889 100644 --- a/src/filters/blur/BlurXFilter.js +++ b/src/filters/blur/BlurXFilter.js @@ -31,7 +31,7 @@ fragSrc ); - this.resolution = resolution || 1; + this.resolution = resolution || core.settings.RESOLUTION; this._quality = 0; diff --git a/src/filters/blur/BlurYFilter.js b/src/filters/blur/BlurYFilter.js index 638e0f0..6d489c7 100644 --- a/src/filters/blur/BlurYFilter.js +++ b/src/filters/blur/BlurYFilter.js @@ -31,7 +31,7 @@ fragSrc ); - this.resolution = resolution || 1; + this.resolution = resolution || core.settings.RESOLUTION; this._quality = 0; diff --git a/src/filters/colormatrix/ColorMatrixFilter.js b/src/filters/colormatrix/ColorMatrixFilter.js index 8509505..679f8ec 100644 --- a/src/filters/colormatrix/ColorMatrixFilter.js +++ b/src/filters/colormatrix/ColorMatrixFilter.js @@ -75,28 +75,28 @@ out[1] = (a[0] * b[1]) + (a[1] * b[6]) + (a[2] * b[11]) + (a[3] * b[16]); out[2] = (a[0] * b[2]) + (a[1] * b[7]) + (a[2] * b[12]) + (a[3] * b[17]); out[3] = (a[0] * b[3]) + (a[1] * b[8]) + (a[2] * b[13]) + (a[3] * b[18]); - out[4] = (a[0] * b[4]) + (a[1] * b[9]) + (a[2] * b[14]) + (a[3] * b[19]); + out[4] = (a[0] * b[4]) + (a[1] * b[9]) + (a[2] * b[14]) + (a[3] * b[19]) + a[4]; // Green Channel out[5] = (a[5] * b[0]) + (a[6] * b[5]) + (a[7] * b[10]) + (a[8] * b[15]); out[6] = (a[5] * b[1]) + (a[6] * b[6]) + (a[7] * b[11]) + (a[8] * b[16]); out[7] = (a[5] * b[2]) + (a[6] * b[7]) + (a[7] * b[12]) + (a[8] * b[17]); out[8] = (a[5] * b[3]) + (a[6] * b[8]) + (a[7] * b[13]) + (a[8] * b[18]); - out[9] = (a[5] * b[4]) + (a[6] * b[9]) + (a[7] * b[14]) + (a[8] * b[19]); + out[9] = (a[5] * b[4]) + (a[6] * b[9]) + (a[7] * b[14]) + (a[8] * b[19]) + a[9]; // Blue Channel out[10] = (a[10] * b[0]) + (a[11] * b[5]) + (a[12] * b[10]) + (a[13] * b[15]); out[11] = (a[10] * b[1]) + (a[11] * b[6]) + (a[12] * b[11]) + (a[13] * b[16]); out[12] = (a[10] * b[2]) + (a[11] * b[7]) + (a[12] * b[12]) + (a[13] * b[17]); out[13] = (a[10] * b[3]) + (a[11] * b[8]) + (a[12] * b[13]) + (a[13] * b[18]); - out[14] = (a[10] * b[4]) + (a[11] * b[9]) + (a[12] * b[14]) + (a[13] * b[19]); + out[14] = (a[10] * b[4]) + (a[11] * b[9]) + (a[12] * b[14]) + (a[13] * b[19]) + a[14]; // Alpha Channel out[15] = (a[15] * b[0]) + (a[16] * b[5]) + (a[17] * b[10]) + (a[18] * b[15]); out[16] = (a[15] * b[1]) + (a[16] * b[6]) + (a[17] * b[11]) + (a[18] * b[16]); out[17] = (a[15] * b[2]) + (a[16] * b[7]) + (a[17] * b[12]) + (a[18] * b[17]); out[18] = (a[15] * b[3]) + (a[16] * b[8]) + (a[17] * b[13]) + (a[18] * b[18]); - out[19] = (a[15] * b[4]) + (a[16] * b[9]) + (a[17] * b[14]) + (a[18] * b[19]); + out[19] = (a[15] * b[4]) + (a[16] * b[9]) + (a[17] * b[14]) + (a[18] * b[19]) + a[19]; return out; } diff --git a/src/filters/colormatrix/colorMatrix.frag b/src/filters/colormatrix/colorMatrix.frag index 3aaf2b3..c73c0e9 100644 --- a/src/filters/colormatrix/colorMatrix.frag +++ b/src/filters/colormatrix/colorMatrix.frag @@ -4,32 +4,38 @@ void main(void) { - vec4 c = texture2D(uSampler, vTextureCoord); + // Un-premultiply alpha before applying the color matrix. See issue #3539. + if (c.a > 0.0) { + c.rgb /= c.a; + } + vec4 result; + result.r = (m[0] * c.r); + result.r += (m[1] * c.g); + result.r += (m[2] * c.b); + result.r += (m[3] * c.a); + result.r += m[4]; - gl_FragColor.r = (m[0] * c.r); - gl_FragColor.r += (m[1] * c.g); - gl_FragColor.r += (m[2] * c.b); - gl_FragColor.r += (m[3] * c.a); - gl_FragColor.r += m[4] * c.a; + result.g = (m[5] * c.r); + result.g += (m[6] * c.g); + result.g += (m[7] * c.b); + result.g += (m[8] * c.a); + result.g += m[9]; - gl_FragColor.g = (m[5] * c.r); - gl_FragColor.g += (m[6] * c.g); - gl_FragColor.g += (m[7] * c.b); - gl_FragColor.g += (m[8] * c.a); - gl_FragColor.g += m[9] * c.a; + result.b = (m[10] * c.r); + result.b += (m[11] * c.g); + result.b += (m[12] * c.b); + result.b += (m[13] * c.a); + result.b += m[14]; - gl_FragColor.b = (m[10] * c.r); - gl_FragColor.b += (m[11] * c.g); - gl_FragColor.b += (m[12] * c.b); - gl_FragColor.b += (m[13] * c.a); - gl_FragColor.b += m[14] * c.a; + result.a = (m[15] * c.r); + result.a += (m[16] * c.g); + result.a += (m[17] * c.b); + result.a += (m[18] * c.a); + result.a += m[19]; - gl_FragColor.a = (m[15] * c.r); - gl_FragColor.a += (m[16] * c.g); - gl_FragColor.a += (m[17] * c.b); - gl_FragColor.a += (m[18] * c.a); - gl_FragColor.a += m[19] * c.a; + // Premultiply alpha again. + result.rgb *= result.a; -// gl_FragColor = vec4(m[0]); + gl_FragColor = result; } diff --git a/src/filters/displacement/DisplacementFilter.js b/src/filters/displacement/DisplacementFilter.js index 112de0b..d5355c3 100644 --- a/src/filters/displacement/DisplacementFilter.js +++ b/src/filters/displacement/DisplacementFilter.js @@ -36,7 +36,7 @@ this.maskMatrix = maskMatrix; this.uniforms.mapSampler = sprite.texture; - this.uniforms.filterMatrix = maskMatrix.toArray(true); + this.uniforms.filterMatrix = maskMatrix; this.uniforms.scale = { x: 1, y: 1 }; if (scale === null || scale === undefined) diff --git a/src/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/InteractionData.js b/src/interaction/InteractionData.js index 02d53a3..818da35 100644 --- a/src/interaction/InteractionData.js +++ b/src/interaction/InteractionData.js @@ -21,9 +21,9 @@ this.global = new core.Point(); /** - * The target Sprite that was interacted with + * The target DisplayObject that was interacted with * - * @member {PIXI.Sprite} + * @member {PIXI.DisplayObject} */ this.target = null; @@ -33,6 +33,13 @@ * @member {Event} */ this.originalEvent = null; + + /** + * Unique identifier for this interaction + * + * @member {number} + */ + this.identifier = null; } /** diff --git a/src/interaction/InteractionEvent.js b/src/interaction/InteractionEvent.js index 75b2966..dc554f9 100644 --- a/src/interaction/InteractionEvent.js +++ b/src/interaction/InteractionEvent.js @@ -58,7 +58,7 @@ } /** - * Prevents event from reaching any objects other than the current object. + * Resets the event. * * @private */ diff --git a/src/interaction/InteractionManager.js b/src/interaction/InteractionManager.js index 7d51134..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/src/prepare/BasePrepare.js b/src/prepare/BasePrepare.js index a2005f5..09eb5e5 100644 --- a/src/prepare/BasePrepare.js +++ b/src/prepare/BasePrepare.js @@ -20,7 +20,7 @@ * * @abstract * @class - * @memberof PIXI + * @memberof PIXI.prepare */ export default class BasePrepare { @@ -107,9 +107,9 @@ /** * Upload all the textures and graphics to the GPU. * - * @param {Function|PIXI.DisplayObject|PIXI.Container} item - Either - * the container or display object to search for items to upload or - * the callback function, if items have been added using `prepare.add`. + * @param {Function|PIXI.DisplayObject|PIXI.Container|PIXI.BaseTexture|PIXI.Texture|PIXI.Graphics|PIXI.Text} item - + * Either the container or display object to search for items to upload, the items to upload themselves, + * or the callback function, if items have been added using `prepare.add`. * @param {Function} [done] - Optional callback when all queued uploads have completed */ upload(item, done) @@ -236,7 +236,8 @@ /** * Manually add an item to the uploading queue. * - * @param {PIXI.DisplayObject|PIXI.Container|*} item - Object to add to the queue + * @param {PIXI.DisplayObject|PIXI.Container|PIXI.BaseTexture|PIXI.Texture|PIXI.Graphics|PIXI.Text|*} item - Object to + * add to the queue * @return {PIXI.CanvasPrepare} Instance of plugin for chaining. */ add(item) diff --git a/src/prepare/canvas/CanvasPrepare.js b/src/prepare/canvas/CanvasPrepare.js index 4b99d22..0224bb5 100644 --- a/src/prepare/canvas/CanvasPrepare.js +++ b/src/prepare/canvas/CanvasPrepare.js @@ -12,7 +12,8 @@ * An instance of this class is automatically created by default, and can be found at renderer.plugins.prepare * * @class - * @memberof PIXI + * @extends PIXI.prepare.BasePrepare + * @memberof PIXI.prepare */ export default class CanvasPrepare extends BasePrepare { diff --git a/src/prepare/webgl/WebGLPrepare.js b/src/prepare/webgl/WebGLPrepare.js index 1ef7467..eb7023f 100644 --- a/src/prepare/webgl/WebGLPrepare.js +++ b/src/prepare/webgl/WebGLPrepare.js @@ -7,7 +7,8 @@ * An instance of this class is automatically created by default, and can be found at renderer.plugins.prepare * * @class - * @memberof PIXI + * @extends PIXI.prepare.BasePrepare + * @memberof PIXI.prepare */ export default class WebGLPrepare extends BasePrepare { diff --git a/test/core/Circle.js b/test/core/Circle.js index 183f98f..f621992 100644 --- a/test/core/Circle.js +++ b/test/core/Circle.js @@ -53,6 +53,10 @@ expect(circ1.contains(10, 16)).to.be.false; expect(circ1.contains(11, 15)).to.be.false; expect(circ1.contains(0, 0)).to.be.false; + + const circ2 = new PIXI.Circle(10, 10, 0); + + expect(circ2.contains(10, 10)).to.be.false; }); it('should return framing rectangle', function () diff --git a/test/core/Container.js b/test/core/Container.js index 8a10b40..cefde54 100644 --- a/test/core/Container.js +++ b/test/core/Container.js @@ -1,5 +1,39 @@ 'use strict'; +function testAddChild(fn) +{ + return function () + { + fn(function (container, obj) + { + container.addChild(obj); + }); + fn(function (container, obj) + { + container.addChildAt(obj); + }); + }; +} + +function testRemoveChild(fn) +{ + return function () + { + 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 +104,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 +279,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/Ellipse.js b/test/core/Ellipse.js index f2e8234..3f47442 100644 --- a/test/core/Ellipse.js +++ b/test/core/Ellipse.js @@ -56,6 +56,10 @@ expect(ellipse1.contains(10, 16)).to.be.false; expect(ellipse1.contains(11, 15)).to.be.false; expect(ellipse1.contains(0, 0)).to.be.false; + + const ellipse2 = new PIXI.Ellipse(10, 10, 0, 0); + + expect(ellipse2.contains(10, 10)).to.be.false; }); it('should return framing rectangle', function () diff --git a/package.json b/package.json index 1271fef..71ade15 100644 --- a/package.json +++ b/package.json @@ -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/scripts/release.js b/scripts/release.js index c4c8e36..156620c 100644 --- a/scripts/release.js +++ b/scripts/release.js @@ -12,7 +12,8 @@ 'dist/**/*', 'lib/**/*', 'src/**/*', - 'scripts/**/*', + 'scripts/*', + 'scripts/renders/*', 'test/**/*', '*.json', '*.md', diff --git a/scripts/renders/client.js b/scripts/renders/client.js index 16c9565..6faee34 100755 --- a/scripts/renders/client.js +++ b/scripts/renders/client.js @@ -1,6 +1,6 @@ 'use strict'; -require('../../bin/pixi'); +require('../../dist/pixi'); PIXI.utils.skipHello(); const fs = require('fs'); 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 bc730db..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. @@ -33,8 +33,11 @@ * need to call toDataUrl on the webgl context * @param {number} [options.resolution=1] - The resolution / device pixel ratio of the renderer, retina would be 2 * @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 @@ -43,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. */ @@ -73,7 +99,7 @@ */ stop() { - this.ticker.stop(); + this._ticker.stop(); } /** @@ -81,7 +107,7 @@ */ start() { - this.ticker.start(); + this._ticker.start(); } /** @@ -95,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 index 81e076b..78b17e6 100644 --- a/src/core/Shader.js +++ b/src/core/Shader.js @@ -1,9 +1,7 @@ import { GLShader } from 'pixi-gl-core'; import settings from './settings'; -const { PRECISION } = settings; - -function checkPrecision(src) +function checkPrecision(src, def) { if (src instanceof Array) { @@ -11,14 +9,14 @@ { const copy = src.slice(0); - copy.unshift(`precision ${PRECISION} float;`); + copy.unshift(`precision ${def} float;`); return copy; } } else if (src.substring(0, 9) !== 'precision') { - return `precision ${PRECISION} float;\n${src}`; + return `precision ${def} float;\n${src}`; } return src; @@ -42,6 +40,7 @@ */ constructor(gl, vertexSrc, fragmentSrc) { - super(gl, checkPrecision(vertexSrc), checkPrecision(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 3425b41..f9235e4 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -181,7 +181,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 b3792ef..17fde8d 100644 --- a/src/core/display/Container.js +++ b/src/core/display/Container.js @@ -73,13 +73,14 @@ } child.parent = this; - - // ensure a transform will be recalculated.. - this.transform._parentID = -1; - this._boundsID++; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.push(child); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(this.children.length - 1); child.emit('added', this); @@ -108,9 +109,14 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.splice(index, 0, child); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(index); child.emit('added', this); @@ -174,6 +180,7 @@ removeItems(this.children, currentIndex, 1); // remove from old position this.children.splice(index, 0, child); // add at new position + this.onChildrenChange(index); } @@ -220,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! @@ -244,9 +252,14 @@ { const child = this.getChildAt(index); + // ensure child transform will be recalculated.. child.parent = null; + child.transform._parentID = -1; removeItems(this.children, index, 1); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(index); child.emit('removed', this); @@ -275,8 +288,14 @@ for (let i = 0; i < removed.length; ++i) { removed[i].parent = null; + if (removed[i].transform) + { + removed[i].transform._parentID = -1; + } } + this._boundsID++; + this.onChildrenChange(beginIndex); for (let i = 0; i < removed.length; ++i) diff --git a/src/core/display/DisplayObject.js b/src/core/display/DisplayObject.js index 3affe36..4e6c77d 100644 --- a/src/core/display/DisplayObject.js +++ b/src/core/display/DisplayObject.js @@ -109,7 +109,7 @@ /** * The original, cached mask of the object * - * @member {PIXI.Rectangle} + * @member {PIXI.Graphics|PIXI.Sprite} * @private */ this._mask = null; diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index 95f2af4..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 ); @@ -1065,10 +1061,15 @@ canvasRenderer = new CanvasRenderer(); } - tempMatrix.tx = -bounds.x; - tempMatrix.ty = -bounds.y; + this.transform.updateLocalTransform(); + this.transform.localTransform.copy(tempMatrix); - canvasRenderer.render(this, canvasBuffer, false, tempMatrix); + tempMatrix.invert(); + + tempMatrix.tx -= bounds.x; + tempMatrix.ty -= bounds.y; + + canvasRenderer.render(this, canvasBuffer, true, tempMatrix); const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode); 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 e67680f..867dde2 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/buildCircle.js b/src/core/graphics/webgl/utils/buildCircle.js index 0400d13..abf5d02 100644 --- a/src/core/graphics/webgl/utils/buildCircle.js +++ b/src/core/graphics/webgl/utils/buildCircle.js @@ -33,6 +33,11 @@ height = circleData.height; } + if (width === 0 || height === 0) + { + return; + } + const totalSegs = Math.floor(30 * Math.sqrt(circleData.radius)) || Math.floor(15 * Math.sqrt(circleData.width + circleData.height)); diff --git a/src/core/graphics/webgl/utils/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 90155f9..f03fe15 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -30,6 +30,7 @@ export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; export { default as CanvasGraphicsRenderer } from './graphics/canvas/CanvasGraphicsRenderer'; +export { default as Spritesheet } from './textures/Spritesheet'; export { default as Texture } from './textures/Texture'; export { default as BaseTexture } from './textures/BaseTexture'; export { default as RenderTexture } from './textures/RenderTexture'; diff --git a/src/core/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 ccce680..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); } /** @@ -162,6 +162,9 @@ if (transform) { transform.copy(tempWt); + + // lets not forget to flag the parent transform as dirty... + this._tempDisplayObjectParent.transform._worldID = -1; } else { @@ -169,6 +172,7 @@ } displayObject.parent = this._tempDisplayObjectParent; + displayObject.updateTransform(); displayObject.parent = cacheParent; // displayObject.hitArea = //TODO add a temp hit area @@ -279,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/canvas/utils/CanvasRenderTarget.js b/src/core/renderers/canvas/utils/CanvasRenderTarget.js index 3de0aef..b409891 100644 --- a/src/core/renderers/canvas/utils/CanvasRenderTarget.js +++ b/src/core/renderers/canvas/utils/CanvasRenderTarget.js @@ -1,5 +1,4 @@ import settings from '../../../settings'; -const { RESOLUTION } = settings; /** * Creates a Canvas element of the given size. @@ -30,7 +29,7 @@ */ this.context = this.canvas.getContext('2d'); - this.resolution = resolution || RESOLUTION; + this.resolution = resolution || settings.RESOLUTION; this.resize(width, height); } 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 ce0e1c4..788ed6c 100644 --- a/src/core/renderers/webgl/WebGLRenderer.js +++ b/src/core/renderers/webgl/WebGLRenderer.js @@ -30,8 +30,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 @@ -49,10 +49,19 @@ * enable this if you need to call toDataUrl on the webgl context. * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when * rendering, stopping pixel interpolation. + * @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; + + if (this.legacy) + { + glCore.VertexArrayObject.FORCE_NATIVE = true; + } /** * The type of this renderer as a standardised const @@ -229,7 +238,7 @@ this.emit('context', gl); // setup the width/height properties and gl viewport - this.resize(this.width, this.height); + this.resize(this.screen.width, this.screen.height); } /** @@ -323,16 +332,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) { @@ -376,6 +385,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 @@ -440,9 +469,10 @@ * Changes the current shader to the one given in parameter * * @param {PIXI.Shader} shader - the new shader + * @param {boolean} [autoProject=true] - Whether automatically set the projection matrix * @return {PIXI.WebGLRenderer} Returns itself. */ - bindShader(shader) + bindShader(shader, autoProject) { // TODO cache if (this._activeShader !== shader) @@ -450,8 +480,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 bbab3b3..944f41d 100644 --- a/src/core/renderers/webgl/filters/Filter.js +++ b/src/core/renderers/webgl/filters/Filter.js @@ -1,6 +1,7 @@ import extractUniformsFromSrc from './extractUniformsFromSrc'; import { uid } from '../../../utils'; import { BLEND_MODES } from '../../../const'; +import settings from '../../../settings'; const SOURCE_KEY_MAP = {}; @@ -10,7 +11,7 @@ * @memberof PIXI * @extends PIXI.Shader */ -class Filter +export default class Filter { /** * @param {string} [vertexSrc] - The source of the vertex shader. @@ -35,8 +36,6 @@ this.blendMode = BLEND_MODES.NORMAL; - // pull out the vertex and shader uniforms if they are not specified.. - // currently this does not extract structs only default types this.uniformData = uniforms || extractUniformsFromSrc(this.vertexSrc, this.fragmentSrc, 'projectionMatrix|uSampler'); /** @@ -80,7 +79,7 @@ * * @member {number} */ - this.resolution = 1; + this.resolution = settings.RESOLUTION; /** * If enabled is true the filter is applied, if false it will not. @@ -97,8 +96,11 @@ * @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 { // --- // // this.uniforms.filterMatrix = filterManager.calculateSpriteMatrix(tempMatrix, window.panda ); @@ -170,5 +172,3 @@ ].join('\n'); } } - -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 dc7072a..6c7422d 100644 --- a/src/core/renderers/webgl/managers/FilterManager.js +++ b/src/core/renderers/webgl/managers/FilterManager.js @@ -154,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 @@ -176,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; @@ -184,7 +184,7 @@ flop = t; } - filters[i].apply(this, flip, lastState.renderTarget, true); + filters[i].apply(this, flip, lastState.renderTarget, false, currentState); this.freePotRenderTarget(flip); this.freePotRenderTarget(flop); @@ -291,9 +291,13 @@ let textureCount = 1; let currentState; - if (shader.uniforms.data.filterArea) + // 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; @@ -306,9 +310,9 @@ // use this to clamp displaced texture coords so they belong to filterArea // see displacementFilter fragment shader for an example - if (shader.uniforms.data.filterClamp) + if (shader.uniforms.filterClamp) { - currentState = this.filterData.stack[this.filterData.index]; + currentState = currentState || this.filterData.stack[this.filterData.index]; const filterClamp = shader.uniforms.filterClamp; @@ -485,7 +489,7 @@ */ destroy() { - this.shaderCache = []; + this.shaderCache = {}; this.emptyPool(); } diff --git a/src/core/renderers/webgl/utils/RenderTarget.js b/src/core/renderers/webgl/utils/RenderTarget.js index afe51ab..6d2467f 100644 --- a/src/core/renderers/webgl/utils/RenderTarget.js +++ b/src/core/renderers/webgl/utils/RenderTarget.js @@ -124,7 +124,7 @@ * @default PIXI.settings.SCALE_MODE * @see PIXI.SCALE_MODES */ - this.scaleMode = scaleMode || settings.SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; /** * Whether this object is the root element or not 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 36ea5bb..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; @@ -346,8 +346,8 @@ /** * Gets the local bounds of the sprite object. * - * @param {Rectangle} rect - The output rectangle. - * @return {Rectangle} The bounds. + * @param {PIXI.Rectangle} rect - The output rectangle. + * @return {PIXI.Rectangle} The bounds. */ getLocalBounds(rect) { @@ -440,7 +440,7 @@ * * @static * @param {number|string|PIXI.BaseTexture|HTMLCanvasElement|HTMLVideoElement} source Source to create texture from - * @return {PIXI.Texture} 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 e77b39a..90b08c0 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++) @@ -103,11 +102,18 @@ { const gl = this.renderer.gl; - // step 1: first check max textures the GPU can handle. - this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); + if (this.renderer.legacy) + { + this.MAX_TEXTURES = 1; + } + else + { + // step 1: first check max textures the GPU can handle. + this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); - // step 2: check the maximum number of if statements the shader can have too.. - this.MAX_TEXTURES = checkMaxIfStatmentsInShader(this.MAX_TEXTURES, gl); + // step 2: check the maximum number of if statements the shader can have too.. + this.MAX_TEXTURES = checkMaxIfStatmentsInShader(this.MAX_TEXTURES, gl); + } const shader = this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); @@ -130,8 +136,12 @@ .addIndex(this.indexBuffer) .addAttribute(this.vertexBuffers[i], shader.attributes.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) .addAttribute(this.vertexBuffers[i], shader.attributes.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) - .addAttribute(this.vertexBuffers[i], shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4) - .addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + .addAttribute(this.vertexBuffers[i], shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); + + if (shader.attributes.aTextureId) + { + this.vaos[i].addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + } /* eslint-enable max-len */ } @@ -378,8 +388,12 @@ .addIndex(this.indexBuffer) .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) - .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4) - .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); + + if (this.shader.attributes.aTextureId) + { + this.vaos[this.vertexCount].addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + } /* eslint-enable max-len */ } diff --git a/src/core/text/Text.js b/src/core/text/Text.js index 013625a..cec8c32 100644 --- a/src/core/text/Text.js +++ b/src/core/text/Text.js @@ -6,6 +6,7 @@ import { TEXT_GRADIENT } from '../const'; import settings from '../settings'; import TextStyle from './TextStyle'; +import trimCanvas from '../utils/trimCanvas'; const defaultDestroyOptions = { texture: true, @@ -57,7 +58,7 @@ /** * The canvas 2d context that everything is drawn with - * @member {HTMLCanvasElement} + * @member {CanvasRenderingContext2D} */ this.context = this.canvas.getContext('2d'); @@ -189,10 +190,12 @@ if (style.dropShadow) { + this.context.shadowBlur = style.dropShadowBlur; + this.context.globalAlpha = style.dropShadowAlpha; + if (style.dropShadowBlur > 0) { this.context.shadowColor = style.dropShadowColor; - this.context.shadowBlur = style.dropShadowBlur; } else { @@ -237,6 +240,10 @@ } } + // reset the shadow blur and alpha that was set by the drop shadow, for the regular text + this.context.shadowBlur = 0; + this.context.globalAlpha = 1; + // set canvas text styles this.context.fillStyle = this._generateFillStyle(style, lines); @@ -326,6 +333,15 @@ */ updateTexture() { + if (this._style.trim) + { + const trimmed = trimCanvas(this.canvas); + + this.canvas.width = trimmed.width; + this.canvas.height = trimmed.height; + this.context.putImageData(trimmed.data, 0, 0); + } + const texture = this._texture; const style = this._style; @@ -475,6 +491,19 @@ } /** + * Gets the local bounds of the text object. + * + * @param {Rectangle} rect - The output rectangle. + * @return {Rectangle} The bounds. + */ + getLocalBounds(rect) + { + this.updateText(true); + + return super.getLocalBounds.call(this, rect); + } + + /** * calculates the bounds of the Text as a rectangle. The bounds calculation takes the worldTransform into account. */ _calculateBounds() @@ -525,6 +554,29 @@ const width = this.canvas.width / this.resolution; const height = this.canvas.height / this.resolution; + // make a copy of the style settings, so we can manipulate them later + const fill = style.fill.slice(); + const fillGradientStops = style.fillGradientStops.slice(); + + // wanting to evenly distribute the fills. So an array of 4 colours should give fills of 0.25, 0.5 and 0.75 + if (!fillGradientStops.length) + { + const lengthPlus1 = fill.length + 1; + + for (let i = 1; i < lengthPlus1; ++i) + { + fillGradientStops.push(i / lengthPlus1); + } + } + + // stop the bleeding of the last gradient on the line above to the top gradient of the this line + // by hard defining the first gradient colour at point 0, and last gradient colour at point 1 + fill.unshift(style.fill[0]); + fillGradientStops.unshift(0); + + fill.push(style.fill[style.fill.length - 1]); + fillGradientStops.push(1); + if (style.fillGradientType === TEXT_GRADIENT.LINEAR_VERTICAL) { // start the gradient at the top center of the canvas, and end at the bottom middle of the canvas @@ -532,15 +584,22 @@ // we need to repeat the gradient so that each individual line of text has the same vertical gradient effect // ['#FF0000', '#00FF00', '#0000FF'] over 2 lines would create stops at 0.125, 0.25, 0.375, 0.625, 0.75, 0.875 - totalIterations = (style.fill.length + 1) * lines.length; + totalIterations = (fill.length + 1) * lines.length; currentIteration = 0; for (let i = 0; i < lines.length; i++) { currentIteration += 1; - for (let j = 0; j < style.fill.length; j++) + for (let j = 0; j < fill.length; j++) { - stop = (currentIteration / totalIterations); - gradient.addColorStop(stop, style.fill[j]); + if (fillGradientStops[j]) + { + stop = (fillGradientStops[j] / lines.length) + (i / lines.length); + } + else + { + stop = currentIteration / totalIterations; + } + gradient.addColorStop(stop, fill[j]); currentIteration++; } } @@ -552,13 +611,20 @@ // can just evenly space out the gradients in this case, as multiple lines makes no difference // to an even left to right gradient - totalIterations = style.fill.length + 1; + totalIterations = fill.length + 1; currentIteration = 1; - for (let i = 0; i < style.fill.length; i++) + for (let i = 0; i < fill.length; i++) { - stop = currentIteration / totalIterations; - gradient.addColorStop(stop, style.fill[i]); + if (fillGradientStops[i]) + { + stop = fillGradientStops[i]; + } + else + { + stop = currentIteration / totalIterations; + } + gradient.addColorStop(stop, fill[i]); currentIteration++; } } @@ -569,7 +635,7 @@ /** * Destroys this text object. * Note* Unlike a Sprite, a Text object will automatically destroy its baseTexture and texture as - * the majorety of the time the texture will not be shared with any other Sprites. + * the majority of the time the texture will not be shared with any other Sprites. * * @param {object|boolean} [options] - Options parameter. A boolean will act as if all options * have been set to that value @@ -680,7 +746,7 @@ set text(text) // eslint-disable-line require-jsdoc { - text = String(text || ' '); + text = String(text === '' || text === null || text === undefined ? ' ' : text); if (this._text === text) { diff --git a/src/core/text/TextStyle.js b/src/core/text/TextStyle.js index 3b25528..890950b 100644 --- a/src/core/text/TextStyle.js +++ b/src/core/text/TextStyle.js @@ -8,12 +8,14 @@ align: 'left', breakWords: false, dropShadow: false, + dropShadowAlpha: 1, dropShadowAngle: Math.PI / 6, dropShadowBlur: 0, dropShadowColor: '#000000', dropShadowDistance: 5, fill: 'black', fillGradientType: TEXT_GRADIENT.LINEAR_VERTICAL, + fillGradientStops: [], fontFamily: 'Arial', fontSize: 26, fontStyle: 'normal', @@ -27,6 +29,7 @@ stroke: 'black', strokeThickness: 0, textBaseline: 'alphabetic', + trim: false, wordWrap: false, wordWrapWidth: 100, }; @@ -47,6 +50,7 @@ * @param {boolean} [style.breakWords=false] - Indicates if lines can be wrapped within words, it * needs wordWrap to be set to true * @param {boolean} [style.dropShadow=false] - Set a drop shadow for the text + * @param {number} [style.dropShadowAlpha=1] - Set alpha for the drop shadow * @param {number} [style.dropShadowAngle=Math.PI/6] - Set a angle of the drop shadow * @param {number} [style.dropShadowBlur=0] - Set a shadow blur radius * @param {string} [style.dropShadowColor='#000000'] - A fill style to be used on the dropshadow e.g 'red', '#00FF00' @@ -55,8 +59,10 @@ * fillstyle that will be used on the text e.g 'red', '#00FF00'. Can be an array to create a gradient * eg ['#000000','#FFFFFF'] * {@link https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/fillStyle|MDN} - * @param {number} [style.fillGradientType=PIXI.TEXT_GRADIENT.LINEAR_VERTICAL] - If fills styles are - * supplied, this can change the type/direction of the gradient. See {@link PIXI.TEXT_GRADIENT} for possible values + * @param {number} [style.fillGradientType=PIXI.TEXT_GRADIENT.LINEAR_VERTICAL] - If fill is an array of colours + * to create a gradient, this can change the type/direction of the gradient. See {@link PIXI.TEXT_GRADIENT} + * @param {number[]} [style.fillGradientStops] - If fill is an array of colours to create a gradient, this array can set + * the stop points (numbers between 0 and 1) for the color, overriding the default behaviour of evenly spacing them. * @param {string|string[]} [style.fontFamily='Arial'] - The font family * @param {number|string} [style.fontSize=26] - The font size (as a number it converts to px, but as a string, * equivalents are '26px','20pt','160%' or '1.6em') @@ -76,6 +82,7 @@ * e.g 'blue', '#FCFF00' * @param {number} [style.strokeThickness=0] - A number that represents the thickness of the stroke. * Default is 0 (no stroke) + * @param {boolean} [style.trim=false] - Trim transparent borders * @param {string} [style.textBaseline='alphabetic'] - The baseline of the text that is rendered. * @param {boolean} [style.wordWrap=false] - Indicates if word wrap should be used * @param {number} [style.wordWrapWidth=100] - The width at which text will wrap, it needs wordWrap to be set to true @@ -152,6 +159,19 @@ } } + get dropShadowAlpha() + { + return this._dropShadowAlpha; + } + set dropShadowAlpha(dropShadowAlpha) + { + if (this._dropShadowAlpha !== dropShadowAlpha) + { + this._dropShadowAlpha = dropShadowAlpha; + this.styleID++; + } + } + get dropShadowAngle() { return this._dropShadowAngle; @@ -232,6 +252,19 @@ } } + get fillGradientStops() + { + return this._fillGradientStops; + } + set fillGradientStops(fillGradientStops) + { + if (!areArraysEqual(this._fillGradientStops,fillGradientStops)) + { + this._fillGradientStops = fillGradientStops; + this.styleID++; + } + } + get fontFamily() { return this._fontFamily; @@ -402,6 +435,19 @@ } } + get trim() + { + return this._trim; + } + set trim(trim) + { + if (this._trim !== trim) + { + this._trim = trim; + this.styleID++; + } + } + get wordWrap() { return this._wordWrap; @@ -475,3 +521,34 @@ return color; } } + +/** + * Utility function to convert hexadecimal colors to strings, and simply return the color if it's a string. + * This version can also convert array of colors + * + * @param {Array} array1 First array to compare + * @param {Array} array2 Second array to compare + * @return {boolean} Do the arrays contain the same values in the same order + */ +function areArraysEqual(array1, array2) +{ + if (!Array.isArray(array1) || !Array.isArray(array2)) + { + return false; + } + + if (array1.length !== array2.length) + { + return false; + } + + for (let i = 0; i < array1.length; ++i) + { + if (array1[i] !== array2[i]) + { + return false; + } + } + + return true; +} diff --git a/src/core/textures/BaseRenderTexture.js b/src/core/textures/BaseRenderTexture.js index 5d50146..aa727f0 100644 --- a/src/core/textures/BaseRenderTexture.js +++ b/src/core/textures/BaseRenderTexture.js @@ -1,8 +1,6 @@ import BaseTexture from './BaseTexture'; import settings from '../settings'; -const { RESOLUTION, SCALE_MODE } = settings; - /** * A BaseRenderTexture is a special texture that allows any Pixi display object to be rendered to it. * @@ -54,7 +52,7 @@ { super(null, scaleMode); - this.resolution = resolution || RESOLUTION; + this.resolution = resolution || settings.RESOLUTION; this.width = width; this.height = height; @@ -62,7 +60,7 @@ this.realWidth = this.width * this.resolution; this.realHeight = this.height * this.resolution; - this.scaleMode = scaleMode || SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; this.hasLoaded = true; /** diff --git a/src/core/textures/BaseTexture.js b/src/core/textures/BaseTexture.js index 676dca3..aeccf39 100644 --- a/src/core/textures/BaseTexture.js +++ b/src/core/textures/BaseTexture.js @@ -77,7 +77,7 @@ * @default PIXI.settings.SCALE_MODE * @see PIXI.SCALE_MODES */ - this.scaleMode = scaleMode || settings.SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; /** * Set to true once the base texture has successfully loaded. @@ -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/RenderTexture.js b/src/core/textures/RenderTexture.js index 4c5034f..b3c6297 100644 --- a/src/core/textures/RenderTexture.js +++ b/src/core/textures/RenderTexture.js @@ -54,8 +54,8 @@ /* eslint-disable prefer-rest-params, no-console */ const width = arguments[1]; const height = arguments[2]; - const scaleMode = arguments[3] || 0; - const resolution = arguments[4] || 1; + const scaleMode = arguments[3]; + const resolution = arguments[4]; // we have an old render texture.. console.warn(`Please use RenderTexture.create(${width}, ${height}) instead of the ctor directly.`); 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 ca31456..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 @@ -459,7 +497,9 @@ if (frame.x + frame.width > this.baseTexture.width || frame.y + frame.height > this.baseTexture.height) { - throw new Error(`Texture Error: frame does not fit inside the base Texture dimensions ${this}`); + throw new Error('Texture Error: frame does not fit inside the base Texture dimensions: ' + + `X: ${frame.x} + ${frame.width} > ${this.baseTexture.width} ` + + `Y: ${frame.y} + ${frame.height} > ${this.baseTexture.height}`); } // this.valid = frame && frame.width && frame.height && this.baseTexture.source && this.baseTexture.hasLoaded; @@ -520,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. @@ -528,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 2a7497b..99e585a 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)); } /** @@ -360,3 +362,43 @@ * @private */ export const BaseTextureCache = {}; + +/** + * 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/core/utils/trimCanvas.js b/src/core/utils/trimCanvas.js new file mode 100644 index 0000000..5af6de0 --- /dev/null +++ b/src/core/utils/trimCanvas.js @@ -0,0 +1,83 @@ +/** + * Trim transparent borders from a canvas + * + * @memberof PIXI + * @function trimCanvas + * @private + * @param {HTMLCanvasElement} canvas - the canvas to trim + * @returns {object} Trim data + */ +export default function trimCanvas(canvas) +{ + // https://gist.github.com/remy/784508 + + let width = canvas.width; + let height = canvas.height; + + const context = canvas.getContext('2d'); + const imageData = context.getImageData(0, 0, width, height); + const pixels = imageData.data; + const len = pixels.length; + + const bound = { + top: null, + left: null, + right: null, + bottom: null, + }; + let i; + let x; + let y; + + for (i = 0; i < len; i += 4) + { + if (pixels[i + 3] !== 0) + { + x = (i / 4) % width; + y = ~~((i / 4) / width); + + if (bound.top === null) + { + bound.top = y; + } + + if (bound.left === null) + { + bound.left = x; + } + else if (x < bound.left) + { + bound.left = x; + } + + if (bound.right === null) + { + bound.right = x + 1; + } + else if (bound.right < x) + { + bound.right = x + 1; + } + + if (bound.bottom === null) + { + bound.bottom = y; + } + else if (bound.bottom < y) + { + bound.bottom = y; + } + } + } + + width = bound.right - bound.left; + height = bound.bottom - bound.top + 1; + + const data = context.getImageData(bound.left, bound.top, width, height); + + return { + height, + width, + data, + }; +} diff --git a/src/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/extras/TilingSprite.js b/src/extras/TilingSprite.js index f90d248..fe82451 100644 --- a/src/extras/TilingSprite.js +++ b/src/extras/TilingSprite.js @@ -220,20 +220,33 @@ transform.tx * resolution, transform.ty * resolution); - // TODO - this should be rolled into the setTransform above.. - context.scale(this.tileScale.x / baseTextureResolution, this.tileScale.y / baseTextureResolution); - - context.translate(modX + (this.anchor.x * -this._width), - modY + (this.anchor.y * -this._height)); - renderer.setBlendMode(this.blendMode); // fill the pattern! context.fillStyle = this._canvasPattern; - context.fillRect(-modX, - -modY, - this._width / this.tileScale.x * baseTextureResolution, - this._height / this.tileScale.y * baseTextureResolution); + + // TODO - this should be rolled into the setTransform above.. + context.scale(this.tileScale.x / baseTextureResolution, this.tileScale.y / baseTextureResolution); + + const anchorX = this.anchor.x * -this._width; + const anchorY = this.anchor.y * -this._height; + + if (this.uvRespectAnchor) + { + context.translate(modX, modY); + + context.fillRect(-modX + anchorX, -modY + anchorY, + this._width / this.tileScale.x * baseTextureResolution, + this._height / this.tileScale.y * baseTextureResolution); + } + else + { + context.translate(modX + anchorX, modY + anchorY); + + context.fillRect(-modX, -modY, + this._width / this.tileScale.x * baseTextureResolution, + this._height / this.tileScale.y * baseTextureResolution); + } } /** diff --git a/src/extras/cacheAsBitmap.js b/src/extras/cacheAsBitmap.js index 18043a7..5dd5640 100644 --- a/src/extras/cacheAsBitmap.js +++ b/src/extras/cacheAsBitmap.js @@ -41,6 +41,9 @@ * provide a performance benefit for complex static displayObjects. * To remove simply set this property to 'false' * + * IMPORTANT GOTCHA - make sure that all your textures are preloaded BEFORE setting this property to true + * as it will take a snapshot of what is currently there. If the textures have not loaded then they will not appear. + * * @member {boolean} * @memberof PIXI.DisplayObject# */ diff --git a/src/filters/blur/BlurFilter.js b/src/filters/blur/BlurFilter.js index 340fc58..1575db9 100644 --- a/src/filters/blur/BlurFilter.js +++ b/src/filters/blur/BlurFilter.js @@ -24,10 +24,9 @@ this.blurXFilter = new BlurXFilter(strength, quality, resolution, kernelSize); this.blurYFilter = new BlurYFilter(strength, quality, resolution, kernelSize); - this.resolution = 1; this.padding = 0; - this.resolution = resolution || 1; + this.resolution = resolution || core.settings.RESOLUTION; this.quality = quality || 4; this.blur = strength || 8; } diff --git a/src/filters/blur/BlurXFilter.js b/src/filters/blur/BlurXFilter.js index def99c3..0f48889 100644 --- a/src/filters/blur/BlurXFilter.js +++ b/src/filters/blur/BlurXFilter.js @@ -31,7 +31,7 @@ fragSrc ); - this.resolution = resolution || 1; + this.resolution = resolution || core.settings.RESOLUTION; this._quality = 0; diff --git a/src/filters/blur/BlurYFilter.js b/src/filters/blur/BlurYFilter.js index 638e0f0..6d489c7 100644 --- a/src/filters/blur/BlurYFilter.js +++ b/src/filters/blur/BlurYFilter.js @@ -31,7 +31,7 @@ fragSrc ); - this.resolution = resolution || 1; + this.resolution = resolution || core.settings.RESOLUTION; this._quality = 0; diff --git a/src/filters/colormatrix/ColorMatrixFilter.js b/src/filters/colormatrix/ColorMatrixFilter.js index 8509505..679f8ec 100644 --- a/src/filters/colormatrix/ColorMatrixFilter.js +++ b/src/filters/colormatrix/ColorMatrixFilter.js @@ -75,28 +75,28 @@ out[1] = (a[0] * b[1]) + (a[1] * b[6]) + (a[2] * b[11]) + (a[3] * b[16]); out[2] = (a[0] * b[2]) + (a[1] * b[7]) + (a[2] * b[12]) + (a[3] * b[17]); out[3] = (a[0] * b[3]) + (a[1] * b[8]) + (a[2] * b[13]) + (a[3] * b[18]); - out[4] = (a[0] * b[4]) + (a[1] * b[9]) + (a[2] * b[14]) + (a[3] * b[19]); + out[4] = (a[0] * b[4]) + (a[1] * b[9]) + (a[2] * b[14]) + (a[3] * b[19]) + a[4]; // Green Channel out[5] = (a[5] * b[0]) + (a[6] * b[5]) + (a[7] * b[10]) + (a[8] * b[15]); out[6] = (a[5] * b[1]) + (a[6] * b[6]) + (a[7] * b[11]) + (a[8] * b[16]); out[7] = (a[5] * b[2]) + (a[6] * b[7]) + (a[7] * b[12]) + (a[8] * b[17]); out[8] = (a[5] * b[3]) + (a[6] * b[8]) + (a[7] * b[13]) + (a[8] * b[18]); - out[9] = (a[5] * b[4]) + (a[6] * b[9]) + (a[7] * b[14]) + (a[8] * b[19]); + out[9] = (a[5] * b[4]) + (a[6] * b[9]) + (a[7] * b[14]) + (a[8] * b[19]) + a[9]; // Blue Channel out[10] = (a[10] * b[0]) + (a[11] * b[5]) + (a[12] * b[10]) + (a[13] * b[15]); out[11] = (a[10] * b[1]) + (a[11] * b[6]) + (a[12] * b[11]) + (a[13] * b[16]); out[12] = (a[10] * b[2]) + (a[11] * b[7]) + (a[12] * b[12]) + (a[13] * b[17]); out[13] = (a[10] * b[3]) + (a[11] * b[8]) + (a[12] * b[13]) + (a[13] * b[18]); - out[14] = (a[10] * b[4]) + (a[11] * b[9]) + (a[12] * b[14]) + (a[13] * b[19]); + out[14] = (a[10] * b[4]) + (a[11] * b[9]) + (a[12] * b[14]) + (a[13] * b[19]) + a[14]; // Alpha Channel out[15] = (a[15] * b[0]) + (a[16] * b[5]) + (a[17] * b[10]) + (a[18] * b[15]); out[16] = (a[15] * b[1]) + (a[16] * b[6]) + (a[17] * b[11]) + (a[18] * b[16]); out[17] = (a[15] * b[2]) + (a[16] * b[7]) + (a[17] * b[12]) + (a[18] * b[17]); out[18] = (a[15] * b[3]) + (a[16] * b[8]) + (a[17] * b[13]) + (a[18] * b[18]); - out[19] = (a[15] * b[4]) + (a[16] * b[9]) + (a[17] * b[14]) + (a[18] * b[19]); + out[19] = (a[15] * b[4]) + (a[16] * b[9]) + (a[17] * b[14]) + (a[18] * b[19]) + a[19]; return out; } diff --git a/src/filters/colormatrix/colorMatrix.frag b/src/filters/colormatrix/colorMatrix.frag index 3aaf2b3..c73c0e9 100644 --- a/src/filters/colormatrix/colorMatrix.frag +++ b/src/filters/colormatrix/colorMatrix.frag @@ -4,32 +4,38 @@ void main(void) { - vec4 c = texture2D(uSampler, vTextureCoord); + // Un-premultiply alpha before applying the color matrix. See issue #3539. + if (c.a > 0.0) { + c.rgb /= c.a; + } + vec4 result; + result.r = (m[0] * c.r); + result.r += (m[1] * c.g); + result.r += (m[2] * c.b); + result.r += (m[3] * c.a); + result.r += m[4]; - gl_FragColor.r = (m[0] * c.r); - gl_FragColor.r += (m[1] * c.g); - gl_FragColor.r += (m[2] * c.b); - gl_FragColor.r += (m[3] * c.a); - gl_FragColor.r += m[4] * c.a; + result.g = (m[5] * c.r); + result.g += (m[6] * c.g); + result.g += (m[7] * c.b); + result.g += (m[8] * c.a); + result.g += m[9]; - gl_FragColor.g = (m[5] * c.r); - gl_FragColor.g += (m[6] * c.g); - gl_FragColor.g += (m[7] * c.b); - gl_FragColor.g += (m[8] * c.a); - gl_FragColor.g += m[9] * c.a; + result.b = (m[10] * c.r); + result.b += (m[11] * c.g); + result.b += (m[12] * c.b); + result.b += (m[13] * c.a); + result.b += m[14]; - gl_FragColor.b = (m[10] * c.r); - gl_FragColor.b += (m[11] * c.g); - gl_FragColor.b += (m[12] * c.b); - gl_FragColor.b += (m[13] * c.a); - gl_FragColor.b += m[14] * c.a; + result.a = (m[15] * c.r); + result.a += (m[16] * c.g); + result.a += (m[17] * c.b); + result.a += (m[18] * c.a); + result.a += m[19]; - gl_FragColor.a = (m[15] * c.r); - gl_FragColor.a += (m[16] * c.g); - gl_FragColor.a += (m[17] * c.b); - gl_FragColor.a += (m[18] * c.a); - gl_FragColor.a += m[19] * c.a; + // Premultiply alpha again. + result.rgb *= result.a; -// gl_FragColor = vec4(m[0]); + gl_FragColor = result; } diff --git a/src/filters/displacement/DisplacementFilter.js b/src/filters/displacement/DisplacementFilter.js index 112de0b..d5355c3 100644 --- a/src/filters/displacement/DisplacementFilter.js +++ b/src/filters/displacement/DisplacementFilter.js @@ -36,7 +36,7 @@ this.maskMatrix = maskMatrix; this.uniforms.mapSampler = sprite.texture; - this.uniforms.filterMatrix = maskMatrix.toArray(true); + this.uniforms.filterMatrix = maskMatrix; this.uniforms.scale = { x: 1, y: 1 }; if (scale === null || scale === undefined) diff --git a/src/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/InteractionData.js b/src/interaction/InteractionData.js index 02d53a3..818da35 100644 --- a/src/interaction/InteractionData.js +++ b/src/interaction/InteractionData.js @@ -21,9 +21,9 @@ this.global = new core.Point(); /** - * The target Sprite that was interacted with + * The target DisplayObject that was interacted with * - * @member {PIXI.Sprite} + * @member {PIXI.DisplayObject} */ this.target = null; @@ -33,6 +33,13 @@ * @member {Event} */ this.originalEvent = null; + + /** + * Unique identifier for this interaction + * + * @member {number} + */ + this.identifier = null; } /** diff --git a/src/interaction/InteractionEvent.js b/src/interaction/InteractionEvent.js index 75b2966..dc554f9 100644 --- a/src/interaction/InteractionEvent.js +++ b/src/interaction/InteractionEvent.js @@ -58,7 +58,7 @@ } /** - * Prevents event from reaching any objects other than the current object. + * Resets the event. * * @private */ diff --git a/src/interaction/InteractionManager.js b/src/interaction/InteractionManager.js index 7d51134..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/src/prepare/BasePrepare.js b/src/prepare/BasePrepare.js index a2005f5..09eb5e5 100644 --- a/src/prepare/BasePrepare.js +++ b/src/prepare/BasePrepare.js @@ -20,7 +20,7 @@ * * @abstract * @class - * @memberof PIXI + * @memberof PIXI.prepare */ export default class BasePrepare { @@ -107,9 +107,9 @@ /** * Upload all the textures and graphics to the GPU. * - * @param {Function|PIXI.DisplayObject|PIXI.Container} item - Either - * the container or display object to search for items to upload or - * the callback function, if items have been added using `prepare.add`. + * @param {Function|PIXI.DisplayObject|PIXI.Container|PIXI.BaseTexture|PIXI.Texture|PIXI.Graphics|PIXI.Text} item - + * Either the container or display object to search for items to upload, the items to upload themselves, + * or the callback function, if items have been added using `prepare.add`. * @param {Function} [done] - Optional callback when all queued uploads have completed */ upload(item, done) @@ -236,7 +236,8 @@ /** * Manually add an item to the uploading queue. * - * @param {PIXI.DisplayObject|PIXI.Container|*} item - Object to add to the queue + * @param {PIXI.DisplayObject|PIXI.Container|PIXI.BaseTexture|PIXI.Texture|PIXI.Graphics|PIXI.Text|*} item - Object to + * add to the queue * @return {PIXI.CanvasPrepare} Instance of plugin for chaining. */ add(item) diff --git a/src/prepare/canvas/CanvasPrepare.js b/src/prepare/canvas/CanvasPrepare.js index 4b99d22..0224bb5 100644 --- a/src/prepare/canvas/CanvasPrepare.js +++ b/src/prepare/canvas/CanvasPrepare.js @@ -12,7 +12,8 @@ * An instance of this class is automatically created by default, and can be found at renderer.plugins.prepare * * @class - * @memberof PIXI + * @extends PIXI.prepare.BasePrepare + * @memberof PIXI.prepare */ export default class CanvasPrepare extends BasePrepare { diff --git a/src/prepare/webgl/WebGLPrepare.js b/src/prepare/webgl/WebGLPrepare.js index 1ef7467..eb7023f 100644 --- a/src/prepare/webgl/WebGLPrepare.js +++ b/src/prepare/webgl/WebGLPrepare.js @@ -7,7 +7,8 @@ * An instance of this class is automatically created by default, and can be found at renderer.plugins.prepare * * @class - * @memberof PIXI + * @extends PIXI.prepare.BasePrepare + * @memberof PIXI.prepare */ export default class WebGLPrepare extends BasePrepare { diff --git a/test/core/Circle.js b/test/core/Circle.js index 183f98f..f621992 100644 --- a/test/core/Circle.js +++ b/test/core/Circle.js @@ -53,6 +53,10 @@ expect(circ1.contains(10, 16)).to.be.false; expect(circ1.contains(11, 15)).to.be.false; expect(circ1.contains(0, 0)).to.be.false; + + const circ2 = new PIXI.Circle(10, 10, 0); + + expect(circ2.contains(10, 10)).to.be.false; }); it('should return framing rectangle', function () diff --git a/test/core/Container.js b/test/core/Container.js index 8a10b40..cefde54 100644 --- a/test/core/Container.js +++ b/test/core/Container.js @@ -1,5 +1,39 @@ 'use strict'; +function testAddChild(fn) +{ + return function () + { + fn(function (container, obj) + { + container.addChild(obj); + }); + fn(function (container, obj) + { + container.addChildAt(obj); + }); + }; +} + +function testRemoveChild(fn) +{ + return function () + { + 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 +104,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 +279,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/Ellipse.js b/test/core/Ellipse.js index f2e8234..3f47442 100644 --- a/test/core/Ellipse.js +++ b/test/core/Ellipse.js @@ -56,6 +56,10 @@ expect(ellipse1.contains(10, 16)).to.be.false; expect(ellipse1.contains(11, 15)).to.be.false; expect(ellipse1.contains(0, 0)).to.be.false; + + const ellipse2 = new PIXI.Ellipse(10, 10, 0, 0); + + expect(ellipse2.contains(10, 10)).to.be.false; }); it('should return framing rectangle', function () diff --git a/test/core/Graphics.js b/test/core/Graphics.js index 2a64946..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 () @@ -127,6 +130,16 @@ expect(graphics.containsPoint(point)).to.be.false; }); + + it('should return false when no fill', function () + { + const point = new PIXI.Point(1, 1); + const graphics = new PIXI.Graphics(); + + graphics.drawRect(0, 0, 10, 10); + + expect(graphics.containsPoint(point)).to.be.false; + }); }); describe('arc', function () @@ -208,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 1271fef..71ade15 100644 --- a/package.json +++ b/package.json @@ -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/scripts/release.js b/scripts/release.js index c4c8e36..156620c 100644 --- a/scripts/release.js +++ b/scripts/release.js @@ -12,7 +12,8 @@ 'dist/**/*', 'lib/**/*', 'src/**/*', - 'scripts/**/*', + 'scripts/*', + 'scripts/renders/*', 'test/**/*', '*.json', '*.md', diff --git a/scripts/renders/client.js b/scripts/renders/client.js index 16c9565..6faee34 100755 --- a/scripts/renders/client.js +++ b/scripts/renders/client.js @@ -1,6 +1,6 @@ 'use strict'; -require('../../bin/pixi'); +require('../../dist/pixi'); PIXI.utils.skipHello(); const fs = require('fs'); 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 bc730db..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. @@ -33,8 +33,11 @@ * need to call toDataUrl on the webgl context * @param {number} [options.resolution=1] - The resolution / device pixel ratio of the renderer, retina would be 2 * @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 @@ -43,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. */ @@ -73,7 +99,7 @@ */ stop() { - this.ticker.stop(); + this._ticker.stop(); } /** @@ -81,7 +107,7 @@ */ start() { - this.ticker.start(); + this._ticker.start(); } /** @@ -95,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 index 81e076b..78b17e6 100644 --- a/src/core/Shader.js +++ b/src/core/Shader.js @@ -1,9 +1,7 @@ import { GLShader } from 'pixi-gl-core'; import settings from './settings'; -const { PRECISION } = settings; - -function checkPrecision(src) +function checkPrecision(src, def) { if (src instanceof Array) { @@ -11,14 +9,14 @@ { const copy = src.slice(0); - copy.unshift(`precision ${PRECISION} float;`); + copy.unshift(`precision ${def} float;`); return copy; } } else if (src.substring(0, 9) !== 'precision') { - return `precision ${PRECISION} float;\n${src}`; + return `precision ${def} float;\n${src}`; } return src; @@ -42,6 +40,7 @@ */ constructor(gl, vertexSrc, fragmentSrc) { - super(gl, checkPrecision(vertexSrc), checkPrecision(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 3425b41..f9235e4 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -181,7 +181,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 b3792ef..17fde8d 100644 --- a/src/core/display/Container.js +++ b/src/core/display/Container.js @@ -73,13 +73,14 @@ } child.parent = this; - - // ensure a transform will be recalculated.. - this.transform._parentID = -1; - this._boundsID++; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.push(child); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(this.children.length - 1); child.emit('added', this); @@ -108,9 +109,14 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.splice(index, 0, child); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(index); child.emit('added', this); @@ -174,6 +180,7 @@ removeItems(this.children, currentIndex, 1); // remove from old position this.children.splice(index, 0, child); // add at new position + this.onChildrenChange(index); } @@ -220,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! @@ -244,9 +252,14 @@ { const child = this.getChildAt(index); + // ensure child transform will be recalculated.. child.parent = null; + child.transform._parentID = -1; removeItems(this.children, index, 1); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(index); child.emit('removed', this); @@ -275,8 +288,14 @@ for (let i = 0; i < removed.length; ++i) { removed[i].parent = null; + if (removed[i].transform) + { + removed[i].transform._parentID = -1; + } } + this._boundsID++; + this.onChildrenChange(beginIndex); for (let i = 0; i < removed.length; ++i) diff --git a/src/core/display/DisplayObject.js b/src/core/display/DisplayObject.js index 3affe36..4e6c77d 100644 --- a/src/core/display/DisplayObject.js +++ b/src/core/display/DisplayObject.js @@ -109,7 +109,7 @@ /** * The original, cached mask of the object * - * @member {PIXI.Rectangle} + * @member {PIXI.Graphics|PIXI.Sprite} * @private */ this._mask = null; diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index 95f2af4..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 ); @@ -1065,10 +1061,15 @@ canvasRenderer = new CanvasRenderer(); } - tempMatrix.tx = -bounds.x; - tempMatrix.ty = -bounds.y; + this.transform.updateLocalTransform(); + this.transform.localTransform.copy(tempMatrix); - canvasRenderer.render(this, canvasBuffer, false, tempMatrix); + tempMatrix.invert(); + + tempMatrix.tx -= bounds.x; + tempMatrix.ty -= bounds.y; + + canvasRenderer.render(this, canvasBuffer, true, tempMatrix); const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode); 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 e67680f..867dde2 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/buildCircle.js b/src/core/graphics/webgl/utils/buildCircle.js index 0400d13..abf5d02 100644 --- a/src/core/graphics/webgl/utils/buildCircle.js +++ b/src/core/graphics/webgl/utils/buildCircle.js @@ -33,6 +33,11 @@ height = circleData.height; } + if (width === 0 || height === 0) + { + return; + } + const totalSegs = Math.floor(30 * Math.sqrt(circleData.radius)) || Math.floor(15 * Math.sqrt(circleData.width + circleData.height)); diff --git a/src/core/graphics/webgl/utils/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 90155f9..f03fe15 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -30,6 +30,7 @@ export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; export { default as CanvasGraphicsRenderer } from './graphics/canvas/CanvasGraphicsRenderer'; +export { default as Spritesheet } from './textures/Spritesheet'; export { default as Texture } from './textures/Texture'; export { default as BaseTexture } from './textures/BaseTexture'; export { default as RenderTexture } from './textures/RenderTexture'; diff --git a/src/core/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 ccce680..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); } /** @@ -162,6 +162,9 @@ if (transform) { transform.copy(tempWt); + + // lets not forget to flag the parent transform as dirty... + this._tempDisplayObjectParent.transform._worldID = -1; } else { @@ -169,6 +172,7 @@ } displayObject.parent = this._tempDisplayObjectParent; + displayObject.updateTransform(); displayObject.parent = cacheParent; // displayObject.hitArea = //TODO add a temp hit area @@ -279,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/canvas/utils/CanvasRenderTarget.js b/src/core/renderers/canvas/utils/CanvasRenderTarget.js index 3de0aef..b409891 100644 --- a/src/core/renderers/canvas/utils/CanvasRenderTarget.js +++ b/src/core/renderers/canvas/utils/CanvasRenderTarget.js @@ -1,5 +1,4 @@ import settings from '../../../settings'; -const { RESOLUTION } = settings; /** * Creates a Canvas element of the given size. @@ -30,7 +29,7 @@ */ this.context = this.canvas.getContext('2d'); - this.resolution = resolution || RESOLUTION; + this.resolution = resolution || settings.RESOLUTION; this.resize(width, height); } 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 ce0e1c4..788ed6c 100644 --- a/src/core/renderers/webgl/WebGLRenderer.js +++ b/src/core/renderers/webgl/WebGLRenderer.js @@ -30,8 +30,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 @@ -49,10 +49,19 @@ * enable this if you need to call toDataUrl on the webgl context. * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when * rendering, stopping pixel interpolation. + * @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; + + if (this.legacy) + { + glCore.VertexArrayObject.FORCE_NATIVE = true; + } /** * The type of this renderer as a standardised const @@ -229,7 +238,7 @@ this.emit('context', gl); // setup the width/height properties and gl viewport - this.resize(this.width, this.height); + this.resize(this.screen.width, this.screen.height); } /** @@ -323,16 +332,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) { @@ -376,6 +385,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 @@ -440,9 +469,10 @@ * Changes the current shader to the one given in parameter * * @param {PIXI.Shader} shader - the new shader + * @param {boolean} [autoProject=true] - Whether automatically set the projection matrix * @return {PIXI.WebGLRenderer} Returns itself. */ - bindShader(shader) + bindShader(shader, autoProject) { // TODO cache if (this._activeShader !== shader) @@ -450,8 +480,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 bbab3b3..944f41d 100644 --- a/src/core/renderers/webgl/filters/Filter.js +++ b/src/core/renderers/webgl/filters/Filter.js @@ -1,6 +1,7 @@ import extractUniformsFromSrc from './extractUniformsFromSrc'; import { uid } from '../../../utils'; import { BLEND_MODES } from '../../../const'; +import settings from '../../../settings'; const SOURCE_KEY_MAP = {}; @@ -10,7 +11,7 @@ * @memberof PIXI * @extends PIXI.Shader */ -class Filter +export default class Filter { /** * @param {string} [vertexSrc] - The source of the vertex shader. @@ -35,8 +36,6 @@ this.blendMode = BLEND_MODES.NORMAL; - // pull out the vertex and shader uniforms if they are not specified.. - // currently this does not extract structs only default types this.uniformData = uniforms || extractUniformsFromSrc(this.vertexSrc, this.fragmentSrc, 'projectionMatrix|uSampler'); /** @@ -80,7 +79,7 @@ * * @member {number} */ - this.resolution = 1; + this.resolution = settings.RESOLUTION; /** * If enabled is true the filter is applied, if false it will not. @@ -97,8 +96,11 @@ * @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 { // --- // // this.uniforms.filterMatrix = filterManager.calculateSpriteMatrix(tempMatrix, window.panda ); @@ -170,5 +172,3 @@ ].join('\n'); } } - -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 dc7072a..6c7422d 100644 --- a/src/core/renderers/webgl/managers/FilterManager.js +++ b/src/core/renderers/webgl/managers/FilterManager.js @@ -154,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 @@ -176,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; @@ -184,7 +184,7 @@ flop = t; } - filters[i].apply(this, flip, lastState.renderTarget, true); + filters[i].apply(this, flip, lastState.renderTarget, false, currentState); this.freePotRenderTarget(flip); this.freePotRenderTarget(flop); @@ -291,9 +291,13 @@ let textureCount = 1; let currentState; - if (shader.uniforms.data.filterArea) + // 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; @@ -306,9 +310,9 @@ // use this to clamp displaced texture coords so they belong to filterArea // see displacementFilter fragment shader for an example - if (shader.uniforms.data.filterClamp) + if (shader.uniforms.filterClamp) { - currentState = this.filterData.stack[this.filterData.index]; + currentState = currentState || this.filterData.stack[this.filterData.index]; const filterClamp = shader.uniforms.filterClamp; @@ -485,7 +489,7 @@ */ destroy() { - this.shaderCache = []; + this.shaderCache = {}; this.emptyPool(); } diff --git a/src/core/renderers/webgl/utils/RenderTarget.js b/src/core/renderers/webgl/utils/RenderTarget.js index afe51ab..6d2467f 100644 --- a/src/core/renderers/webgl/utils/RenderTarget.js +++ b/src/core/renderers/webgl/utils/RenderTarget.js @@ -124,7 +124,7 @@ * @default PIXI.settings.SCALE_MODE * @see PIXI.SCALE_MODES */ - this.scaleMode = scaleMode || settings.SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; /** * Whether this object is the root element or not 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 36ea5bb..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; @@ -346,8 +346,8 @@ /** * Gets the local bounds of the sprite object. * - * @param {Rectangle} rect - The output rectangle. - * @return {Rectangle} The bounds. + * @param {PIXI.Rectangle} rect - The output rectangle. + * @return {PIXI.Rectangle} The bounds. */ getLocalBounds(rect) { @@ -440,7 +440,7 @@ * * @static * @param {number|string|PIXI.BaseTexture|HTMLCanvasElement|HTMLVideoElement} source Source to create texture from - * @return {PIXI.Texture} 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 e77b39a..90b08c0 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++) @@ -103,11 +102,18 @@ { const gl = this.renderer.gl; - // step 1: first check max textures the GPU can handle. - this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); + if (this.renderer.legacy) + { + this.MAX_TEXTURES = 1; + } + else + { + // step 1: first check max textures the GPU can handle. + this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); - // step 2: check the maximum number of if statements the shader can have too.. - this.MAX_TEXTURES = checkMaxIfStatmentsInShader(this.MAX_TEXTURES, gl); + // step 2: check the maximum number of if statements the shader can have too.. + this.MAX_TEXTURES = checkMaxIfStatmentsInShader(this.MAX_TEXTURES, gl); + } const shader = this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); @@ -130,8 +136,12 @@ .addIndex(this.indexBuffer) .addAttribute(this.vertexBuffers[i], shader.attributes.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) .addAttribute(this.vertexBuffers[i], shader.attributes.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) - .addAttribute(this.vertexBuffers[i], shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4) - .addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + .addAttribute(this.vertexBuffers[i], shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); + + if (shader.attributes.aTextureId) + { + this.vaos[i].addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + } /* eslint-enable max-len */ } @@ -378,8 +388,12 @@ .addIndex(this.indexBuffer) .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) - .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4) - .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); + + if (this.shader.attributes.aTextureId) + { + this.vaos[this.vertexCount].addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + } /* eslint-enable max-len */ } diff --git a/src/core/text/Text.js b/src/core/text/Text.js index 013625a..cec8c32 100644 --- a/src/core/text/Text.js +++ b/src/core/text/Text.js @@ -6,6 +6,7 @@ import { TEXT_GRADIENT } from '../const'; import settings from '../settings'; import TextStyle from './TextStyle'; +import trimCanvas from '../utils/trimCanvas'; const defaultDestroyOptions = { texture: true, @@ -57,7 +58,7 @@ /** * The canvas 2d context that everything is drawn with - * @member {HTMLCanvasElement} + * @member {CanvasRenderingContext2D} */ this.context = this.canvas.getContext('2d'); @@ -189,10 +190,12 @@ if (style.dropShadow) { + this.context.shadowBlur = style.dropShadowBlur; + this.context.globalAlpha = style.dropShadowAlpha; + if (style.dropShadowBlur > 0) { this.context.shadowColor = style.dropShadowColor; - this.context.shadowBlur = style.dropShadowBlur; } else { @@ -237,6 +240,10 @@ } } + // reset the shadow blur and alpha that was set by the drop shadow, for the regular text + this.context.shadowBlur = 0; + this.context.globalAlpha = 1; + // set canvas text styles this.context.fillStyle = this._generateFillStyle(style, lines); @@ -326,6 +333,15 @@ */ updateTexture() { + if (this._style.trim) + { + const trimmed = trimCanvas(this.canvas); + + this.canvas.width = trimmed.width; + this.canvas.height = trimmed.height; + this.context.putImageData(trimmed.data, 0, 0); + } + const texture = this._texture; const style = this._style; @@ -475,6 +491,19 @@ } /** + * Gets the local bounds of the text object. + * + * @param {Rectangle} rect - The output rectangle. + * @return {Rectangle} The bounds. + */ + getLocalBounds(rect) + { + this.updateText(true); + + return super.getLocalBounds.call(this, rect); + } + + /** * calculates the bounds of the Text as a rectangle. The bounds calculation takes the worldTransform into account. */ _calculateBounds() @@ -525,6 +554,29 @@ const width = this.canvas.width / this.resolution; const height = this.canvas.height / this.resolution; + // make a copy of the style settings, so we can manipulate them later + const fill = style.fill.slice(); + const fillGradientStops = style.fillGradientStops.slice(); + + // wanting to evenly distribute the fills. So an array of 4 colours should give fills of 0.25, 0.5 and 0.75 + if (!fillGradientStops.length) + { + const lengthPlus1 = fill.length + 1; + + for (let i = 1; i < lengthPlus1; ++i) + { + fillGradientStops.push(i / lengthPlus1); + } + } + + // stop the bleeding of the last gradient on the line above to the top gradient of the this line + // by hard defining the first gradient colour at point 0, and last gradient colour at point 1 + fill.unshift(style.fill[0]); + fillGradientStops.unshift(0); + + fill.push(style.fill[style.fill.length - 1]); + fillGradientStops.push(1); + if (style.fillGradientType === TEXT_GRADIENT.LINEAR_VERTICAL) { // start the gradient at the top center of the canvas, and end at the bottom middle of the canvas @@ -532,15 +584,22 @@ // we need to repeat the gradient so that each individual line of text has the same vertical gradient effect // ['#FF0000', '#00FF00', '#0000FF'] over 2 lines would create stops at 0.125, 0.25, 0.375, 0.625, 0.75, 0.875 - totalIterations = (style.fill.length + 1) * lines.length; + totalIterations = (fill.length + 1) * lines.length; currentIteration = 0; for (let i = 0; i < lines.length; i++) { currentIteration += 1; - for (let j = 0; j < style.fill.length; j++) + for (let j = 0; j < fill.length; j++) { - stop = (currentIteration / totalIterations); - gradient.addColorStop(stop, style.fill[j]); + if (fillGradientStops[j]) + { + stop = (fillGradientStops[j] / lines.length) + (i / lines.length); + } + else + { + stop = currentIteration / totalIterations; + } + gradient.addColorStop(stop, fill[j]); currentIteration++; } } @@ -552,13 +611,20 @@ // can just evenly space out the gradients in this case, as multiple lines makes no difference // to an even left to right gradient - totalIterations = style.fill.length + 1; + totalIterations = fill.length + 1; currentIteration = 1; - for (let i = 0; i < style.fill.length; i++) + for (let i = 0; i < fill.length; i++) { - stop = currentIteration / totalIterations; - gradient.addColorStop(stop, style.fill[i]); + if (fillGradientStops[i]) + { + stop = fillGradientStops[i]; + } + else + { + stop = currentIteration / totalIterations; + } + gradient.addColorStop(stop, fill[i]); currentIteration++; } } @@ -569,7 +635,7 @@ /** * Destroys this text object. * Note* Unlike a Sprite, a Text object will automatically destroy its baseTexture and texture as - * the majorety of the time the texture will not be shared with any other Sprites. + * the majority of the time the texture will not be shared with any other Sprites. * * @param {object|boolean} [options] - Options parameter. A boolean will act as if all options * have been set to that value @@ -680,7 +746,7 @@ set text(text) // eslint-disable-line require-jsdoc { - text = String(text || ' '); + text = String(text === '' || text === null || text === undefined ? ' ' : text); if (this._text === text) { diff --git a/src/core/text/TextStyle.js b/src/core/text/TextStyle.js index 3b25528..890950b 100644 --- a/src/core/text/TextStyle.js +++ b/src/core/text/TextStyle.js @@ -8,12 +8,14 @@ align: 'left', breakWords: false, dropShadow: false, + dropShadowAlpha: 1, dropShadowAngle: Math.PI / 6, dropShadowBlur: 0, dropShadowColor: '#000000', dropShadowDistance: 5, fill: 'black', fillGradientType: TEXT_GRADIENT.LINEAR_VERTICAL, + fillGradientStops: [], fontFamily: 'Arial', fontSize: 26, fontStyle: 'normal', @@ -27,6 +29,7 @@ stroke: 'black', strokeThickness: 0, textBaseline: 'alphabetic', + trim: false, wordWrap: false, wordWrapWidth: 100, }; @@ -47,6 +50,7 @@ * @param {boolean} [style.breakWords=false] - Indicates if lines can be wrapped within words, it * needs wordWrap to be set to true * @param {boolean} [style.dropShadow=false] - Set a drop shadow for the text + * @param {number} [style.dropShadowAlpha=1] - Set alpha for the drop shadow * @param {number} [style.dropShadowAngle=Math.PI/6] - Set a angle of the drop shadow * @param {number} [style.dropShadowBlur=0] - Set a shadow blur radius * @param {string} [style.dropShadowColor='#000000'] - A fill style to be used on the dropshadow e.g 'red', '#00FF00' @@ -55,8 +59,10 @@ * fillstyle that will be used on the text e.g 'red', '#00FF00'. Can be an array to create a gradient * eg ['#000000','#FFFFFF'] * {@link https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/fillStyle|MDN} - * @param {number} [style.fillGradientType=PIXI.TEXT_GRADIENT.LINEAR_VERTICAL] - If fills styles are - * supplied, this can change the type/direction of the gradient. See {@link PIXI.TEXT_GRADIENT} for possible values + * @param {number} [style.fillGradientType=PIXI.TEXT_GRADIENT.LINEAR_VERTICAL] - If fill is an array of colours + * to create a gradient, this can change the type/direction of the gradient. See {@link PIXI.TEXT_GRADIENT} + * @param {number[]} [style.fillGradientStops] - If fill is an array of colours to create a gradient, this array can set + * the stop points (numbers between 0 and 1) for the color, overriding the default behaviour of evenly spacing them. * @param {string|string[]} [style.fontFamily='Arial'] - The font family * @param {number|string} [style.fontSize=26] - The font size (as a number it converts to px, but as a string, * equivalents are '26px','20pt','160%' or '1.6em') @@ -76,6 +82,7 @@ * e.g 'blue', '#FCFF00' * @param {number} [style.strokeThickness=0] - A number that represents the thickness of the stroke. * Default is 0 (no stroke) + * @param {boolean} [style.trim=false] - Trim transparent borders * @param {string} [style.textBaseline='alphabetic'] - The baseline of the text that is rendered. * @param {boolean} [style.wordWrap=false] - Indicates if word wrap should be used * @param {number} [style.wordWrapWidth=100] - The width at which text will wrap, it needs wordWrap to be set to true @@ -152,6 +159,19 @@ } } + get dropShadowAlpha() + { + return this._dropShadowAlpha; + } + set dropShadowAlpha(dropShadowAlpha) + { + if (this._dropShadowAlpha !== dropShadowAlpha) + { + this._dropShadowAlpha = dropShadowAlpha; + this.styleID++; + } + } + get dropShadowAngle() { return this._dropShadowAngle; @@ -232,6 +252,19 @@ } } + get fillGradientStops() + { + return this._fillGradientStops; + } + set fillGradientStops(fillGradientStops) + { + if (!areArraysEqual(this._fillGradientStops,fillGradientStops)) + { + this._fillGradientStops = fillGradientStops; + this.styleID++; + } + } + get fontFamily() { return this._fontFamily; @@ -402,6 +435,19 @@ } } + get trim() + { + return this._trim; + } + set trim(trim) + { + if (this._trim !== trim) + { + this._trim = trim; + this.styleID++; + } + } + get wordWrap() { return this._wordWrap; @@ -475,3 +521,34 @@ return color; } } + +/** + * Utility function to convert hexadecimal colors to strings, and simply return the color if it's a string. + * This version can also convert array of colors + * + * @param {Array} array1 First array to compare + * @param {Array} array2 Second array to compare + * @return {boolean} Do the arrays contain the same values in the same order + */ +function areArraysEqual(array1, array2) +{ + if (!Array.isArray(array1) || !Array.isArray(array2)) + { + return false; + } + + if (array1.length !== array2.length) + { + return false; + } + + for (let i = 0; i < array1.length; ++i) + { + if (array1[i] !== array2[i]) + { + return false; + } + } + + return true; +} diff --git a/src/core/textures/BaseRenderTexture.js b/src/core/textures/BaseRenderTexture.js index 5d50146..aa727f0 100644 --- a/src/core/textures/BaseRenderTexture.js +++ b/src/core/textures/BaseRenderTexture.js @@ -1,8 +1,6 @@ import BaseTexture from './BaseTexture'; import settings from '../settings'; -const { RESOLUTION, SCALE_MODE } = settings; - /** * A BaseRenderTexture is a special texture that allows any Pixi display object to be rendered to it. * @@ -54,7 +52,7 @@ { super(null, scaleMode); - this.resolution = resolution || RESOLUTION; + this.resolution = resolution || settings.RESOLUTION; this.width = width; this.height = height; @@ -62,7 +60,7 @@ this.realWidth = this.width * this.resolution; this.realHeight = this.height * this.resolution; - this.scaleMode = scaleMode || SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; this.hasLoaded = true; /** diff --git a/src/core/textures/BaseTexture.js b/src/core/textures/BaseTexture.js index 676dca3..aeccf39 100644 --- a/src/core/textures/BaseTexture.js +++ b/src/core/textures/BaseTexture.js @@ -77,7 +77,7 @@ * @default PIXI.settings.SCALE_MODE * @see PIXI.SCALE_MODES */ - this.scaleMode = scaleMode || settings.SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; /** * Set to true once the base texture has successfully loaded. @@ -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/RenderTexture.js b/src/core/textures/RenderTexture.js index 4c5034f..b3c6297 100644 --- a/src/core/textures/RenderTexture.js +++ b/src/core/textures/RenderTexture.js @@ -54,8 +54,8 @@ /* eslint-disable prefer-rest-params, no-console */ const width = arguments[1]; const height = arguments[2]; - const scaleMode = arguments[3] || 0; - const resolution = arguments[4] || 1; + const scaleMode = arguments[3]; + const resolution = arguments[4]; // we have an old render texture.. console.warn(`Please use RenderTexture.create(${width}, ${height}) instead of the ctor directly.`); 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 ca31456..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 @@ -459,7 +497,9 @@ if (frame.x + frame.width > this.baseTexture.width || frame.y + frame.height > this.baseTexture.height) { - throw new Error(`Texture Error: frame does not fit inside the base Texture dimensions ${this}`); + throw new Error('Texture Error: frame does not fit inside the base Texture dimensions: ' + + `X: ${frame.x} + ${frame.width} > ${this.baseTexture.width} ` + + `Y: ${frame.y} + ${frame.height} > ${this.baseTexture.height}`); } // this.valid = frame && frame.width && frame.height && this.baseTexture.source && this.baseTexture.hasLoaded; @@ -520,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. @@ -528,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 2a7497b..99e585a 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)); } /** @@ -360,3 +362,43 @@ * @private */ export const BaseTextureCache = {}; + +/** + * 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/core/utils/trimCanvas.js b/src/core/utils/trimCanvas.js new file mode 100644 index 0000000..5af6de0 --- /dev/null +++ b/src/core/utils/trimCanvas.js @@ -0,0 +1,83 @@ +/** + * Trim transparent borders from a canvas + * + * @memberof PIXI + * @function trimCanvas + * @private + * @param {HTMLCanvasElement} canvas - the canvas to trim + * @returns {object} Trim data + */ +export default function trimCanvas(canvas) +{ + // https://gist.github.com/remy/784508 + + let width = canvas.width; + let height = canvas.height; + + const context = canvas.getContext('2d'); + const imageData = context.getImageData(0, 0, width, height); + const pixels = imageData.data; + const len = pixels.length; + + const bound = { + top: null, + left: null, + right: null, + bottom: null, + }; + let i; + let x; + let y; + + for (i = 0; i < len; i += 4) + { + if (pixels[i + 3] !== 0) + { + x = (i / 4) % width; + y = ~~((i / 4) / width); + + if (bound.top === null) + { + bound.top = y; + } + + if (bound.left === null) + { + bound.left = x; + } + else if (x < bound.left) + { + bound.left = x; + } + + if (bound.right === null) + { + bound.right = x + 1; + } + else if (bound.right < x) + { + bound.right = x + 1; + } + + if (bound.bottom === null) + { + bound.bottom = y; + } + else if (bound.bottom < y) + { + bound.bottom = y; + } + } + } + + width = bound.right - bound.left; + height = bound.bottom - bound.top + 1; + + const data = context.getImageData(bound.left, bound.top, width, height); + + return { + height, + width, + data, + }; +} diff --git a/src/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/extras/TilingSprite.js b/src/extras/TilingSprite.js index f90d248..fe82451 100644 --- a/src/extras/TilingSprite.js +++ b/src/extras/TilingSprite.js @@ -220,20 +220,33 @@ transform.tx * resolution, transform.ty * resolution); - // TODO - this should be rolled into the setTransform above.. - context.scale(this.tileScale.x / baseTextureResolution, this.tileScale.y / baseTextureResolution); - - context.translate(modX + (this.anchor.x * -this._width), - modY + (this.anchor.y * -this._height)); - renderer.setBlendMode(this.blendMode); // fill the pattern! context.fillStyle = this._canvasPattern; - context.fillRect(-modX, - -modY, - this._width / this.tileScale.x * baseTextureResolution, - this._height / this.tileScale.y * baseTextureResolution); + + // TODO - this should be rolled into the setTransform above.. + context.scale(this.tileScale.x / baseTextureResolution, this.tileScale.y / baseTextureResolution); + + const anchorX = this.anchor.x * -this._width; + const anchorY = this.anchor.y * -this._height; + + if (this.uvRespectAnchor) + { + context.translate(modX, modY); + + context.fillRect(-modX + anchorX, -modY + anchorY, + this._width / this.tileScale.x * baseTextureResolution, + this._height / this.tileScale.y * baseTextureResolution); + } + else + { + context.translate(modX + anchorX, modY + anchorY); + + context.fillRect(-modX, -modY, + this._width / this.tileScale.x * baseTextureResolution, + this._height / this.tileScale.y * baseTextureResolution); + } } /** diff --git a/src/extras/cacheAsBitmap.js b/src/extras/cacheAsBitmap.js index 18043a7..5dd5640 100644 --- a/src/extras/cacheAsBitmap.js +++ b/src/extras/cacheAsBitmap.js @@ -41,6 +41,9 @@ * provide a performance benefit for complex static displayObjects. * To remove simply set this property to 'false' * + * IMPORTANT GOTCHA - make sure that all your textures are preloaded BEFORE setting this property to true + * as it will take a snapshot of what is currently there. If the textures have not loaded then they will not appear. + * * @member {boolean} * @memberof PIXI.DisplayObject# */ diff --git a/src/filters/blur/BlurFilter.js b/src/filters/blur/BlurFilter.js index 340fc58..1575db9 100644 --- a/src/filters/blur/BlurFilter.js +++ b/src/filters/blur/BlurFilter.js @@ -24,10 +24,9 @@ this.blurXFilter = new BlurXFilter(strength, quality, resolution, kernelSize); this.blurYFilter = new BlurYFilter(strength, quality, resolution, kernelSize); - this.resolution = 1; this.padding = 0; - this.resolution = resolution || 1; + this.resolution = resolution || core.settings.RESOLUTION; this.quality = quality || 4; this.blur = strength || 8; } diff --git a/src/filters/blur/BlurXFilter.js b/src/filters/blur/BlurXFilter.js index def99c3..0f48889 100644 --- a/src/filters/blur/BlurXFilter.js +++ b/src/filters/blur/BlurXFilter.js @@ -31,7 +31,7 @@ fragSrc ); - this.resolution = resolution || 1; + this.resolution = resolution || core.settings.RESOLUTION; this._quality = 0; diff --git a/src/filters/blur/BlurYFilter.js b/src/filters/blur/BlurYFilter.js index 638e0f0..6d489c7 100644 --- a/src/filters/blur/BlurYFilter.js +++ b/src/filters/blur/BlurYFilter.js @@ -31,7 +31,7 @@ fragSrc ); - this.resolution = resolution || 1; + this.resolution = resolution || core.settings.RESOLUTION; this._quality = 0; diff --git a/src/filters/colormatrix/ColorMatrixFilter.js b/src/filters/colormatrix/ColorMatrixFilter.js index 8509505..679f8ec 100644 --- a/src/filters/colormatrix/ColorMatrixFilter.js +++ b/src/filters/colormatrix/ColorMatrixFilter.js @@ -75,28 +75,28 @@ out[1] = (a[0] * b[1]) + (a[1] * b[6]) + (a[2] * b[11]) + (a[3] * b[16]); out[2] = (a[0] * b[2]) + (a[1] * b[7]) + (a[2] * b[12]) + (a[3] * b[17]); out[3] = (a[0] * b[3]) + (a[1] * b[8]) + (a[2] * b[13]) + (a[3] * b[18]); - out[4] = (a[0] * b[4]) + (a[1] * b[9]) + (a[2] * b[14]) + (a[3] * b[19]); + out[4] = (a[0] * b[4]) + (a[1] * b[9]) + (a[2] * b[14]) + (a[3] * b[19]) + a[4]; // Green Channel out[5] = (a[5] * b[0]) + (a[6] * b[5]) + (a[7] * b[10]) + (a[8] * b[15]); out[6] = (a[5] * b[1]) + (a[6] * b[6]) + (a[7] * b[11]) + (a[8] * b[16]); out[7] = (a[5] * b[2]) + (a[6] * b[7]) + (a[7] * b[12]) + (a[8] * b[17]); out[8] = (a[5] * b[3]) + (a[6] * b[8]) + (a[7] * b[13]) + (a[8] * b[18]); - out[9] = (a[5] * b[4]) + (a[6] * b[9]) + (a[7] * b[14]) + (a[8] * b[19]); + out[9] = (a[5] * b[4]) + (a[6] * b[9]) + (a[7] * b[14]) + (a[8] * b[19]) + a[9]; // Blue Channel out[10] = (a[10] * b[0]) + (a[11] * b[5]) + (a[12] * b[10]) + (a[13] * b[15]); out[11] = (a[10] * b[1]) + (a[11] * b[6]) + (a[12] * b[11]) + (a[13] * b[16]); out[12] = (a[10] * b[2]) + (a[11] * b[7]) + (a[12] * b[12]) + (a[13] * b[17]); out[13] = (a[10] * b[3]) + (a[11] * b[8]) + (a[12] * b[13]) + (a[13] * b[18]); - out[14] = (a[10] * b[4]) + (a[11] * b[9]) + (a[12] * b[14]) + (a[13] * b[19]); + out[14] = (a[10] * b[4]) + (a[11] * b[9]) + (a[12] * b[14]) + (a[13] * b[19]) + a[14]; // Alpha Channel out[15] = (a[15] * b[0]) + (a[16] * b[5]) + (a[17] * b[10]) + (a[18] * b[15]); out[16] = (a[15] * b[1]) + (a[16] * b[6]) + (a[17] * b[11]) + (a[18] * b[16]); out[17] = (a[15] * b[2]) + (a[16] * b[7]) + (a[17] * b[12]) + (a[18] * b[17]); out[18] = (a[15] * b[3]) + (a[16] * b[8]) + (a[17] * b[13]) + (a[18] * b[18]); - out[19] = (a[15] * b[4]) + (a[16] * b[9]) + (a[17] * b[14]) + (a[18] * b[19]); + out[19] = (a[15] * b[4]) + (a[16] * b[9]) + (a[17] * b[14]) + (a[18] * b[19]) + a[19]; return out; } diff --git a/src/filters/colormatrix/colorMatrix.frag b/src/filters/colormatrix/colorMatrix.frag index 3aaf2b3..c73c0e9 100644 --- a/src/filters/colormatrix/colorMatrix.frag +++ b/src/filters/colormatrix/colorMatrix.frag @@ -4,32 +4,38 @@ void main(void) { - vec4 c = texture2D(uSampler, vTextureCoord); + // Un-premultiply alpha before applying the color matrix. See issue #3539. + if (c.a > 0.0) { + c.rgb /= c.a; + } + vec4 result; + result.r = (m[0] * c.r); + result.r += (m[1] * c.g); + result.r += (m[2] * c.b); + result.r += (m[3] * c.a); + result.r += m[4]; - gl_FragColor.r = (m[0] * c.r); - gl_FragColor.r += (m[1] * c.g); - gl_FragColor.r += (m[2] * c.b); - gl_FragColor.r += (m[3] * c.a); - gl_FragColor.r += m[4] * c.a; + result.g = (m[5] * c.r); + result.g += (m[6] * c.g); + result.g += (m[7] * c.b); + result.g += (m[8] * c.a); + result.g += m[9]; - gl_FragColor.g = (m[5] * c.r); - gl_FragColor.g += (m[6] * c.g); - gl_FragColor.g += (m[7] * c.b); - gl_FragColor.g += (m[8] * c.a); - gl_FragColor.g += m[9] * c.a; + result.b = (m[10] * c.r); + result.b += (m[11] * c.g); + result.b += (m[12] * c.b); + result.b += (m[13] * c.a); + result.b += m[14]; - gl_FragColor.b = (m[10] * c.r); - gl_FragColor.b += (m[11] * c.g); - gl_FragColor.b += (m[12] * c.b); - gl_FragColor.b += (m[13] * c.a); - gl_FragColor.b += m[14] * c.a; + result.a = (m[15] * c.r); + result.a += (m[16] * c.g); + result.a += (m[17] * c.b); + result.a += (m[18] * c.a); + result.a += m[19]; - gl_FragColor.a = (m[15] * c.r); - gl_FragColor.a += (m[16] * c.g); - gl_FragColor.a += (m[17] * c.b); - gl_FragColor.a += (m[18] * c.a); - gl_FragColor.a += m[19] * c.a; + // Premultiply alpha again. + result.rgb *= result.a; -// gl_FragColor = vec4(m[0]); + gl_FragColor = result; } diff --git a/src/filters/displacement/DisplacementFilter.js b/src/filters/displacement/DisplacementFilter.js index 112de0b..d5355c3 100644 --- a/src/filters/displacement/DisplacementFilter.js +++ b/src/filters/displacement/DisplacementFilter.js @@ -36,7 +36,7 @@ this.maskMatrix = maskMatrix; this.uniforms.mapSampler = sprite.texture; - this.uniforms.filterMatrix = maskMatrix.toArray(true); + this.uniforms.filterMatrix = maskMatrix; this.uniforms.scale = { x: 1, y: 1 }; if (scale === null || scale === undefined) diff --git a/src/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/InteractionData.js b/src/interaction/InteractionData.js index 02d53a3..818da35 100644 --- a/src/interaction/InteractionData.js +++ b/src/interaction/InteractionData.js @@ -21,9 +21,9 @@ this.global = new core.Point(); /** - * The target Sprite that was interacted with + * The target DisplayObject that was interacted with * - * @member {PIXI.Sprite} + * @member {PIXI.DisplayObject} */ this.target = null; @@ -33,6 +33,13 @@ * @member {Event} */ this.originalEvent = null; + + /** + * Unique identifier for this interaction + * + * @member {number} + */ + this.identifier = null; } /** diff --git a/src/interaction/InteractionEvent.js b/src/interaction/InteractionEvent.js index 75b2966..dc554f9 100644 --- a/src/interaction/InteractionEvent.js +++ b/src/interaction/InteractionEvent.js @@ -58,7 +58,7 @@ } /** - * Prevents event from reaching any objects other than the current object. + * Resets the event. * * @private */ diff --git a/src/interaction/InteractionManager.js b/src/interaction/InteractionManager.js index 7d51134..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/src/prepare/BasePrepare.js b/src/prepare/BasePrepare.js index a2005f5..09eb5e5 100644 --- a/src/prepare/BasePrepare.js +++ b/src/prepare/BasePrepare.js @@ -20,7 +20,7 @@ * * @abstract * @class - * @memberof PIXI + * @memberof PIXI.prepare */ export default class BasePrepare { @@ -107,9 +107,9 @@ /** * Upload all the textures and graphics to the GPU. * - * @param {Function|PIXI.DisplayObject|PIXI.Container} item - Either - * the container or display object to search for items to upload or - * the callback function, if items have been added using `prepare.add`. + * @param {Function|PIXI.DisplayObject|PIXI.Container|PIXI.BaseTexture|PIXI.Texture|PIXI.Graphics|PIXI.Text} item - + * Either the container or display object to search for items to upload, the items to upload themselves, + * or the callback function, if items have been added using `prepare.add`. * @param {Function} [done] - Optional callback when all queued uploads have completed */ upload(item, done) @@ -236,7 +236,8 @@ /** * Manually add an item to the uploading queue. * - * @param {PIXI.DisplayObject|PIXI.Container|*} item - Object to add to the queue + * @param {PIXI.DisplayObject|PIXI.Container|PIXI.BaseTexture|PIXI.Texture|PIXI.Graphics|PIXI.Text|*} item - Object to + * add to the queue * @return {PIXI.CanvasPrepare} Instance of plugin for chaining. */ add(item) diff --git a/src/prepare/canvas/CanvasPrepare.js b/src/prepare/canvas/CanvasPrepare.js index 4b99d22..0224bb5 100644 --- a/src/prepare/canvas/CanvasPrepare.js +++ b/src/prepare/canvas/CanvasPrepare.js @@ -12,7 +12,8 @@ * An instance of this class is automatically created by default, and can be found at renderer.plugins.prepare * * @class - * @memberof PIXI + * @extends PIXI.prepare.BasePrepare + * @memberof PIXI.prepare */ export default class CanvasPrepare extends BasePrepare { diff --git a/src/prepare/webgl/WebGLPrepare.js b/src/prepare/webgl/WebGLPrepare.js index 1ef7467..eb7023f 100644 --- a/src/prepare/webgl/WebGLPrepare.js +++ b/src/prepare/webgl/WebGLPrepare.js @@ -7,7 +7,8 @@ * An instance of this class is automatically created by default, and can be found at renderer.plugins.prepare * * @class - * @memberof PIXI + * @extends PIXI.prepare.BasePrepare + * @memberof PIXI.prepare */ export default class WebGLPrepare extends BasePrepare { diff --git a/test/core/Circle.js b/test/core/Circle.js index 183f98f..f621992 100644 --- a/test/core/Circle.js +++ b/test/core/Circle.js @@ -53,6 +53,10 @@ expect(circ1.contains(10, 16)).to.be.false; expect(circ1.contains(11, 15)).to.be.false; expect(circ1.contains(0, 0)).to.be.false; + + const circ2 = new PIXI.Circle(10, 10, 0); + + expect(circ2.contains(10, 10)).to.be.false; }); it('should return framing rectangle', function () diff --git a/test/core/Container.js b/test/core/Container.js index 8a10b40..cefde54 100644 --- a/test/core/Container.js +++ b/test/core/Container.js @@ -1,5 +1,39 @@ 'use strict'; +function testAddChild(fn) +{ + return function () + { + fn(function (container, obj) + { + container.addChild(obj); + }); + fn(function (container, obj) + { + container.addChildAt(obj); + }); + }; +} + +function testRemoveChild(fn) +{ + return function () + { + 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 +104,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 +279,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/Ellipse.js b/test/core/Ellipse.js index f2e8234..3f47442 100644 --- a/test/core/Ellipse.js +++ b/test/core/Ellipse.js @@ -56,6 +56,10 @@ expect(ellipse1.contains(10, 16)).to.be.false; expect(ellipse1.contains(11, 15)).to.be.false; expect(ellipse1.contains(0, 0)).to.be.false; + + const ellipse2 = new PIXI.Ellipse(10, 10, 0, 0); + + expect(ellipse2.contains(10, 10)).to.be.false; }); it('should return framing rectangle', function () diff --git a/test/core/Graphics.js b/test/core/Graphics.js index 2a64946..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 () @@ -127,6 +130,16 @@ expect(graphics.containsPoint(point)).to.be.false; }); + + it('should return false when no fill', function () + { + const point = new PIXI.Point(1, 1); + const graphics = new PIXI.Graphics(); + + graphics.drawRect(0, 0, 10, 10); + + expect(graphics.containsPoint(point)).to.be.false; + }); }); describe('arc', function () @@ -208,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/Matrix.js b/test/core/Matrix.js index 9d2694b..76bd57c 100644 --- a/test/core/Matrix.js +++ b/test/core/Matrix.js @@ -107,10 +107,60 @@ expect(m1.ty).to.equal(m2.ty); }); + it('should prepend matrix', function () + { + const m1 = new PIXI.Matrix(); + const m2 = new PIXI.Matrix(); + + m2.set(2, 3, 4, 5, 100, 200); + m1.prepend(m2); + + expect(m1.a).to.equal(m2.a); + expect(m1.b).to.equal(m2.b); + expect(m1.c).to.equal(m2.c); + expect(m1.d).to.equal(m2.d); + expect(m1.tx).to.equal(m2.tx); + expect(m1.ty).to.equal(m2.ty); + + const m3 = new PIXI.Matrix(); + const m4 = new PIXI.Matrix(); + + m3.prepend(m4); + + expect(m3.a).to.equal(m4.a); + expect(m3.b).to.equal(m4.b); + expect(m3.c).to.equal(m4.c); + expect(m3.d).to.equal(m4.d); + expect(m3.tx).to.equal(m4.tx); + expect(m3.ty).to.equal(m4.ty); + }); + it('should get IDENTITY and TEMP_MATRIX', function () { expect(PIXI.Matrix.IDENTITY instanceof PIXI.Matrix).to.be.true; expect(PIXI.Matrix.TEMP_MATRIX instanceof PIXI.Matrix).to.be.true; }); -}); + it('should reset matrix to default when identity() is called', function () + { + const matrix = new PIXI.Matrix(); + + matrix.set(2, 3, 4, 5, 100, 200); + + expect(matrix.a).to.equal(2); + expect(matrix.b).to.equal(3); + expect(matrix.c).to.equal(4); + expect(matrix.d).to.equal(5); + expect(matrix.tx).to.equal(100); + expect(matrix.ty).to.equal(200); + + matrix.identity(); + + expect(matrix.a).to.equal(1); + expect(matrix.b).to.equal(0); + expect(matrix.c).to.equal(0); + expect(matrix.d).to.equal(1); + expect(matrix.tx).to.equal(0); + expect(matrix.ty).to.equal(0); + }); +}); diff --git a/package.json b/package.json index 1271fef..71ade15 100644 --- a/package.json +++ b/package.json @@ -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/scripts/release.js b/scripts/release.js index c4c8e36..156620c 100644 --- a/scripts/release.js +++ b/scripts/release.js @@ -12,7 +12,8 @@ 'dist/**/*', 'lib/**/*', 'src/**/*', - 'scripts/**/*', + 'scripts/*', + 'scripts/renders/*', 'test/**/*', '*.json', '*.md', diff --git a/scripts/renders/client.js b/scripts/renders/client.js index 16c9565..6faee34 100755 --- a/scripts/renders/client.js +++ b/scripts/renders/client.js @@ -1,6 +1,6 @@ 'use strict'; -require('../../bin/pixi'); +require('../../dist/pixi'); PIXI.utils.skipHello(); const fs = require('fs'); 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 bc730db..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. @@ -33,8 +33,11 @@ * need to call toDataUrl on the webgl context * @param {number} [options.resolution=1] - The resolution / device pixel ratio of the renderer, retina would be 2 * @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 @@ -43,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. */ @@ -73,7 +99,7 @@ */ stop() { - this.ticker.stop(); + this._ticker.stop(); } /** @@ -81,7 +107,7 @@ */ start() { - this.ticker.start(); + this._ticker.start(); } /** @@ -95,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 index 81e076b..78b17e6 100644 --- a/src/core/Shader.js +++ b/src/core/Shader.js @@ -1,9 +1,7 @@ import { GLShader } from 'pixi-gl-core'; import settings from './settings'; -const { PRECISION } = settings; - -function checkPrecision(src) +function checkPrecision(src, def) { if (src instanceof Array) { @@ -11,14 +9,14 @@ { const copy = src.slice(0); - copy.unshift(`precision ${PRECISION} float;`); + copy.unshift(`precision ${def} float;`); return copy; } } else if (src.substring(0, 9) !== 'precision') { - return `precision ${PRECISION} float;\n${src}`; + return `precision ${def} float;\n${src}`; } return src; @@ -42,6 +40,7 @@ */ constructor(gl, vertexSrc, fragmentSrc) { - super(gl, checkPrecision(vertexSrc), checkPrecision(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 3425b41..f9235e4 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -181,7 +181,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 b3792ef..17fde8d 100644 --- a/src/core/display/Container.js +++ b/src/core/display/Container.js @@ -73,13 +73,14 @@ } child.parent = this; - - // ensure a transform will be recalculated.. - this.transform._parentID = -1; - this._boundsID++; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.push(child); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(this.children.length - 1); child.emit('added', this); @@ -108,9 +109,14 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.splice(index, 0, child); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(index); child.emit('added', this); @@ -174,6 +180,7 @@ removeItems(this.children, currentIndex, 1); // remove from old position this.children.splice(index, 0, child); // add at new position + this.onChildrenChange(index); } @@ -220,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! @@ -244,9 +252,14 @@ { const child = this.getChildAt(index); + // ensure child transform will be recalculated.. child.parent = null; + child.transform._parentID = -1; removeItems(this.children, index, 1); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(index); child.emit('removed', this); @@ -275,8 +288,14 @@ for (let i = 0; i < removed.length; ++i) { removed[i].parent = null; + if (removed[i].transform) + { + removed[i].transform._parentID = -1; + } } + this._boundsID++; + this.onChildrenChange(beginIndex); for (let i = 0; i < removed.length; ++i) diff --git a/src/core/display/DisplayObject.js b/src/core/display/DisplayObject.js index 3affe36..4e6c77d 100644 --- a/src/core/display/DisplayObject.js +++ b/src/core/display/DisplayObject.js @@ -109,7 +109,7 @@ /** * The original, cached mask of the object * - * @member {PIXI.Rectangle} + * @member {PIXI.Graphics|PIXI.Sprite} * @private */ this._mask = null; diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index 95f2af4..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 ); @@ -1065,10 +1061,15 @@ canvasRenderer = new CanvasRenderer(); } - tempMatrix.tx = -bounds.x; - tempMatrix.ty = -bounds.y; + this.transform.updateLocalTransform(); + this.transform.localTransform.copy(tempMatrix); - canvasRenderer.render(this, canvasBuffer, false, tempMatrix); + tempMatrix.invert(); + + tempMatrix.tx -= bounds.x; + tempMatrix.ty -= bounds.y; + + canvasRenderer.render(this, canvasBuffer, true, tempMatrix); const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode); 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 e67680f..867dde2 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/buildCircle.js b/src/core/graphics/webgl/utils/buildCircle.js index 0400d13..abf5d02 100644 --- a/src/core/graphics/webgl/utils/buildCircle.js +++ b/src/core/graphics/webgl/utils/buildCircle.js @@ -33,6 +33,11 @@ height = circleData.height; } + if (width === 0 || height === 0) + { + return; + } + const totalSegs = Math.floor(30 * Math.sqrt(circleData.radius)) || Math.floor(15 * Math.sqrt(circleData.width + circleData.height)); diff --git a/src/core/graphics/webgl/utils/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 90155f9..f03fe15 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -30,6 +30,7 @@ export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; export { default as CanvasGraphicsRenderer } from './graphics/canvas/CanvasGraphicsRenderer'; +export { default as Spritesheet } from './textures/Spritesheet'; export { default as Texture } from './textures/Texture'; export { default as BaseTexture } from './textures/BaseTexture'; export { default as RenderTexture } from './textures/RenderTexture'; diff --git a/src/core/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 ccce680..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); } /** @@ -162,6 +162,9 @@ if (transform) { transform.copy(tempWt); + + // lets not forget to flag the parent transform as dirty... + this._tempDisplayObjectParent.transform._worldID = -1; } else { @@ -169,6 +172,7 @@ } displayObject.parent = this._tempDisplayObjectParent; + displayObject.updateTransform(); displayObject.parent = cacheParent; // displayObject.hitArea = //TODO add a temp hit area @@ -279,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/canvas/utils/CanvasRenderTarget.js b/src/core/renderers/canvas/utils/CanvasRenderTarget.js index 3de0aef..b409891 100644 --- a/src/core/renderers/canvas/utils/CanvasRenderTarget.js +++ b/src/core/renderers/canvas/utils/CanvasRenderTarget.js @@ -1,5 +1,4 @@ import settings from '../../../settings'; -const { RESOLUTION } = settings; /** * Creates a Canvas element of the given size. @@ -30,7 +29,7 @@ */ this.context = this.canvas.getContext('2d'); - this.resolution = resolution || RESOLUTION; + this.resolution = resolution || settings.RESOLUTION; this.resize(width, height); } 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 ce0e1c4..788ed6c 100644 --- a/src/core/renderers/webgl/WebGLRenderer.js +++ b/src/core/renderers/webgl/WebGLRenderer.js @@ -30,8 +30,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 @@ -49,10 +49,19 @@ * enable this if you need to call toDataUrl on the webgl context. * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when * rendering, stopping pixel interpolation. + * @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; + + if (this.legacy) + { + glCore.VertexArrayObject.FORCE_NATIVE = true; + } /** * The type of this renderer as a standardised const @@ -229,7 +238,7 @@ this.emit('context', gl); // setup the width/height properties and gl viewport - this.resize(this.width, this.height); + this.resize(this.screen.width, this.screen.height); } /** @@ -323,16 +332,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) { @@ -376,6 +385,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 @@ -440,9 +469,10 @@ * Changes the current shader to the one given in parameter * * @param {PIXI.Shader} shader - the new shader + * @param {boolean} [autoProject=true] - Whether automatically set the projection matrix * @return {PIXI.WebGLRenderer} Returns itself. */ - bindShader(shader) + bindShader(shader, autoProject) { // TODO cache if (this._activeShader !== shader) @@ -450,8 +480,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 bbab3b3..944f41d 100644 --- a/src/core/renderers/webgl/filters/Filter.js +++ b/src/core/renderers/webgl/filters/Filter.js @@ -1,6 +1,7 @@ import extractUniformsFromSrc from './extractUniformsFromSrc'; import { uid } from '../../../utils'; import { BLEND_MODES } from '../../../const'; +import settings from '../../../settings'; const SOURCE_KEY_MAP = {}; @@ -10,7 +11,7 @@ * @memberof PIXI * @extends PIXI.Shader */ -class Filter +export default class Filter { /** * @param {string} [vertexSrc] - The source of the vertex shader. @@ -35,8 +36,6 @@ this.blendMode = BLEND_MODES.NORMAL; - // pull out the vertex and shader uniforms if they are not specified.. - // currently this does not extract structs only default types this.uniformData = uniforms || extractUniformsFromSrc(this.vertexSrc, this.fragmentSrc, 'projectionMatrix|uSampler'); /** @@ -80,7 +79,7 @@ * * @member {number} */ - this.resolution = 1; + this.resolution = settings.RESOLUTION; /** * If enabled is true the filter is applied, if false it will not. @@ -97,8 +96,11 @@ * @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 { // --- // // this.uniforms.filterMatrix = filterManager.calculateSpriteMatrix(tempMatrix, window.panda ); @@ -170,5 +172,3 @@ ].join('\n'); } } - -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 dc7072a..6c7422d 100644 --- a/src/core/renderers/webgl/managers/FilterManager.js +++ b/src/core/renderers/webgl/managers/FilterManager.js @@ -154,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 @@ -176,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; @@ -184,7 +184,7 @@ flop = t; } - filters[i].apply(this, flip, lastState.renderTarget, true); + filters[i].apply(this, flip, lastState.renderTarget, false, currentState); this.freePotRenderTarget(flip); this.freePotRenderTarget(flop); @@ -291,9 +291,13 @@ let textureCount = 1; let currentState; - if (shader.uniforms.data.filterArea) + // 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; @@ -306,9 +310,9 @@ // use this to clamp displaced texture coords so they belong to filterArea // see displacementFilter fragment shader for an example - if (shader.uniforms.data.filterClamp) + if (shader.uniforms.filterClamp) { - currentState = this.filterData.stack[this.filterData.index]; + currentState = currentState || this.filterData.stack[this.filterData.index]; const filterClamp = shader.uniforms.filterClamp; @@ -485,7 +489,7 @@ */ destroy() { - this.shaderCache = []; + this.shaderCache = {}; this.emptyPool(); } diff --git a/src/core/renderers/webgl/utils/RenderTarget.js b/src/core/renderers/webgl/utils/RenderTarget.js index afe51ab..6d2467f 100644 --- a/src/core/renderers/webgl/utils/RenderTarget.js +++ b/src/core/renderers/webgl/utils/RenderTarget.js @@ -124,7 +124,7 @@ * @default PIXI.settings.SCALE_MODE * @see PIXI.SCALE_MODES */ - this.scaleMode = scaleMode || settings.SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; /** * Whether this object is the root element or not 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 36ea5bb..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; @@ -346,8 +346,8 @@ /** * Gets the local bounds of the sprite object. * - * @param {Rectangle} rect - The output rectangle. - * @return {Rectangle} The bounds. + * @param {PIXI.Rectangle} rect - The output rectangle. + * @return {PIXI.Rectangle} The bounds. */ getLocalBounds(rect) { @@ -440,7 +440,7 @@ * * @static * @param {number|string|PIXI.BaseTexture|HTMLCanvasElement|HTMLVideoElement} source Source to create texture from - * @return {PIXI.Texture} 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 e77b39a..90b08c0 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++) @@ -103,11 +102,18 @@ { const gl = this.renderer.gl; - // step 1: first check max textures the GPU can handle. - this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); + if (this.renderer.legacy) + { + this.MAX_TEXTURES = 1; + } + else + { + // step 1: first check max textures the GPU can handle. + this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); - // step 2: check the maximum number of if statements the shader can have too.. - this.MAX_TEXTURES = checkMaxIfStatmentsInShader(this.MAX_TEXTURES, gl); + // step 2: check the maximum number of if statements the shader can have too.. + this.MAX_TEXTURES = checkMaxIfStatmentsInShader(this.MAX_TEXTURES, gl); + } const shader = this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); @@ -130,8 +136,12 @@ .addIndex(this.indexBuffer) .addAttribute(this.vertexBuffers[i], shader.attributes.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) .addAttribute(this.vertexBuffers[i], shader.attributes.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) - .addAttribute(this.vertexBuffers[i], shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4) - .addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + .addAttribute(this.vertexBuffers[i], shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); + + if (shader.attributes.aTextureId) + { + this.vaos[i].addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + } /* eslint-enable max-len */ } @@ -378,8 +388,12 @@ .addIndex(this.indexBuffer) .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) - .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4) - .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); + + if (this.shader.attributes.aTextureId) + { + this.vaos[this.vertexCount].addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + } /* eslint-enable max-len */ } diff --git a/src/core/text/Text.js b/src/core/text/Text.js index 013625a..cec8c32 100644 --- a/src/core/text/Text.js +++ b/src/core/text/Text.js @@ -6,6 +6,7 @@ import { TEXT_GRADIENT } from '../const'; import settings from '../settings'; import TextStyle from './TextStyle'; +import trimCanvas from '../utils/trimCanvas'; const defaultDestroyOptions = { texture: true, @@ -57,7 +58,7 @@ /** * The canvas 2d context that everything is drawn with - * @member {HTMLCanvasElement} + * @member {CanvasRenderingContext2D} */ this.context = this.canvas.getContext('2d'); @@ -189,10 +190,12 @@ if (style.dropShadow) { + this.context.shadowBlur = style.dropShadowBlur; + this.context.globalAlpha = style.dropShadowAlpha; + if (style.dropShadowBlur > 0) { this.context.shadowColor = style.dropShadowColor; - this.context.shadowBlur = style.dropShadowBlur; } else { @@ -237,6 +240,10 @@ } } + // reset the shadow blur and alpha that was set by the drop shadow, for the regular text + this.context.shadowBlur = 0; + this.context.globalAlpha = 1; + // set canvas text styles this.context.fillStyle = this._generateFillStyle(style, lines); @@ -326,6 +333,15 @@ */ updateTexture() { + if (this._style.trim) + { + const trimmed = trimCanvas(this.canvas); + + this.canvas.width = trimmed.width; + this.canvas.height = trimmed.height; + this.context.putImageData(trimmed.data, 0, 0); + } + const texture = this._texture; const style = this._style; @@ -475,6 +491,19 @@ } /** + * Gets the local bounds of the text object. + * + * @param {Rectangle} rect - The output rectangle. + * @return {Rectangle} The bounds. + */ + getLocalBounds(rect) + { + this.updateText(true); + + return super.getLocalBounds.call(this, rect); + } + + /** * calculates the bounds of the Text as a rectangle. The bounds calculation takes the worldTransform into account. */ _calculateBounds() @@ -525,6 +554,29 @@ const width = this.canvas.width / this.resolution; const height = this.canvas.height / this.resolution; + // make a copy of the style settings, so we can manipulate them later + const fill = style.fill.slice(); + const fillGradientStops = style.fillGradientStops.slice(); + + // wanting to evenly distribute the fills. So an array of 4 colours should give fills of 0.25, 0.5 and 0.75 + if (!fillGradientStops.length) + { + const lengthPlus1 = fill.length + 1; + + for (let i = 1; i < lengthPlus1; ++i) + { + fillGradientStops.push(i / lengthPlus1); + } + } + + // stop the bleeding of the last gradient on the line above to the top gradient of the this line + // by hard defining the first gradient colour at point 0, and last gradient colour at point 1 + fill.unshift(style.fill[0]); + fillGradientStops.unshift(0); + + fill.push(style.fill[style.fill.length - 1]); + fillGradientStops.push(1); + if (style.fillGradientType === TEXT_GRADIENT.LINEAR_VERTICAL) { // start the gradient at the top center of the canvas, and end at the bottom middle of the canvas @@ -532,15 +584,22 @@ // we need to repeat the gradient so that each individual line of text has the same vertical gradient effect // ['#FF0000', '#00FF00', '#0000FF'] over 2 lines would create stops at 0.125, 0.25, 0.375, 0.625, 0.75, 0.875 - totalIterations = (style.fill.length + 1) * lines.length; + totalIterations = (fill.length + 1) * lines.length; currentIteration = 0; for (let i = 0; i < lines.length; i++) { currentIteration += 1; - for (let j = 0; j < style.fill.length; j++) + for (let j = 0; j < fill.length; j++) { - stop = (currentIteration / totalIterations); - gradient.addColorStop(stop, style.fill[j]); + if (fillGradientStops[j]) + { + stop = (fillGradientStops[j] / lines.length) + (i / lines.length); + } + else + { + stop = currentIteration / totalIterations; + } + gradient.addColorStop(stop, fill[j]); currentIteration++; } } @@ -552,13 +611,20 @@ // can just evenly space out the gradients in this case, as multiple lines makes no difference // to an even left to right gradient - totalIterations = style.fill.length + 1; + totalIterations = fill.length + 1; currentIteration = 1; - for (let i = 0; i < style.fill.length; i++) + for (let i = 0; i < fill.length; i++) { - stop = currentIteration / totalIterations; - gradient.addColorStop(stop, style.fill[i]); + if (fillGradientStops[i]) + { + stop = fillGradientStops[i]; + } + else + { + stop = currentIteration / totalIterations; + } + gradient.addColorStop(stop, fill[i]); currentIteration++; } } @@ -569,7 +635,7 @@ /** * Destroys this text object. * Note* Unlike a Sprite, a Text object will automatically destroy its baseTexture and texture as - * the majorety of the time the texture will not be shared with any other Sprites. + * the majority of the time the texture will not be shared with any other Sprites. * * @param {object|boolean} [options] - Options parameter. A boolean will act as if all options * have been set to that value @@ -680,7 +746,7 @@ set text(text) // eslint-disable-line require-jsdoc { - text = String(text || ' '); + text = String(text === '' || text === null || text === undefined ? ' ' : text); if (this._text === text) { diff --git a/src/core/text/TextStyle.js b/src/core/text/TextStyle.js index 3b25528..890950b 100644 --- a/src/core/text/TextStyle.js +++ b/src/core/text/TextStyle.js @@ -8,12 +8,14 @@ align: 'left', breakWords: false, dropShadow: false, + dropShadowAlpha: 1, dropShadowAngle: Math.PI / 6, dropShadowBlur: 0, dropShadowColor: '#000000', dropShadowDistance: 5, fill: 'black', fillGradientType: TEXT_GRADIENT.LINEAR_VERTICAL, + fillGradientStops: [], fontFamily: 'Arial', fontSize: 26, fontStyle: 'normal', @@ -27,6 +29,7 @@ stroke: 'black', strokeThickness: 0, textBaseline: 'alphabetic', + trim: false, wordWrap: false, wordWrapWidth: 100, }; @@ -47,6 +50,7 @@ * @param {boolean} [style.breakWords=false] - Indicates if lines can be wrapped within words, it * needs wordWrap to be set to true * @param {boolean} [style.dropShadow=false] - Set a drop shadow for the text + * @param {number} [style.dropShadowAlpha=1] - Set alpha for the drop shadow * @param {number} [style.dropShadowAngle=Math.PI/6] - Set a angle of the drop shadow * @param {number} [style.dropShadowBlur=0] - Set a shadow blur radius * @param {string} [style.dropShadowColor='#000000'] - A fill style to be used on the dropshadow e.g 'red', '#00FF00' @@ -55,8 +59,10 @@ * fillstyle that will be used on the text e.g 'red', '#00FF00'. Can be an array to create a gradient * eg ['#000000','#FFFFFF'] * {@link https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/fillStyle|MDN} - * @param {number} [style.fillGradientType=PIXI.TEXT_GRADIENT.LINEAR_VERTICAL] - If fills styles are - * supplied, this can change the type/direction of the gradient. See {@link PIXI.TEXT_GRADIENT} for possible values + * @param {number} [style.fillGradientType=PIXI.TEXT_GRADIENT.LINEAR_VERTICAL] - If fill is an array of colours + * to create a gradient, this can change the type/direction of the gradient. See {@link PIXI.TEXT_GRADIENT} + * @param {number[]} [style.fillGradientStops] - If fill is an array of colours to create a gradient, this array can set + * the stop points (numbers between 0 and 1) for the color, overriding the default behaviour of evenly spacing them. * @param {string|string[]} [style.fontFamily='Arial'] - The font family * @param {number|string} [style.fontSize=26] - The font size (as a number it converts to px, but as a string, * equivalents are '26px','20pt','160%' or '1.6em') @@ -76,6 +82,7 @@ * e.g 'blue', '#FCFF00' * @param {number} [style.strokeThickness=0] - A number that represents the thickness of the stroke. * Default is 0 (no stroke) + * @param {boolean} [style.trim=false] - Trim transparent borders * @param {string} [style.textBaseline='alphabetic'] - The baseline of the text that is rendered. * @param {boolean} [style.wordWrap=false] - Indicates if word wrap should be used * @param {number} [style.wordWrapWidth=100] - The width at which text will wrap, it needs wordWrap to be set to true @@ -152,6 +159,19 @@ } } + get dropShadowAlpha() + { + return this._dropShadowAlpha; + } + set dropShadowAlpha(dropShadowAlpha) + { + if (this._dropShadowAlpha !== dropShadowAlpha) + { + this._dropShadowAlpha = dropShadowAlpha; + this.styleID++; + } + } + get dropShadowAngle() { return this._dropShadowAngle; @@ -232,6 +252,19 @@ } } + get fillGradientStops() + { + return this._fillGradientStops; + } + set fillGradientStops(fillGradientStops) + { + if (!areArraysEqual(this._fillGradientStops,fillGradientStops)) + { + this._fillGradientStops = fillGradientStops; + this.styleID++; + } + } + get fontFamily() { return this._fontFamily; @@ -402,6 +435,19 @@ } } + get trim() + { + return this._trim; + } + set trim(trim) + { + if (this._trim !== trim) + { + this._trim = trim; + this.styleID++; + } + } + get wordWrap() { return this._wordWrap; @@ -475,3 +521,34 @@ return color; } } + +/** + * Utility function to convert hexadecimal colors to strings, and simply return the color if it's a string. + * This version can also convert array of colors + * + * @param {Array} array1 First array to compare + * @param {Array} array2 Second array to compare + * @return {boolean} Do the arrays contain the same values in the same order + */ +function areArraysEqual(array1, array2) +{ + if (!Array.isArray(array1) || !Array.isArray(array2)) + { + return false; + } + + if (array1.length !== array2.length) + { + return false; + } + + for (let i = 0; i < array1.length; ++i) + { + if (array1[i] !== array2[i]) + { + return false; + } + } + + return true; +} diff --git a/src/core/textures/BaseRenderTexture.js b/src/core/textures/BaseRenderTexture.js index 5d50146..aa727f0 100644 --- a/src/core/textures/BaseRenderTexture.js +++ b/src/core/textures/BaseRenderTexture.js @@ -1,8 +1,6 @@ import BaseTexture from './BaseTexture'; import settings from '../settings'; -const { RESOLUTION, SCALE_MODE } = settings; - /** * A BaseRenderTexture is a special texture that allows any Pixi display object to be rendered to it. * @@ -54,7 +52,7 @@ { super(null, scaleMode); - this.resolution = resolution || RESOLUTION; + this.resolution = resolution || settings.RESOLUTION; this.width = width; this.height = height; @@ -62,7 +60,7 @@ this.realWidth = this.width * this.resolution; this.realHeight = this.height * this.resolution; - this.scaleMode = scaleMode || SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; this.hasLoaded = true; /** diff --git a/src/core/textures/BaseTexture.js b/src/core/textures/BaseTexture.js index 676dca3..aeccf39 100644 --- a/src/core/textures/BaseTexture.js +++ b/src/core/textures/BaseTexture.js @@ -77,7 +77,7 @@ * @default PIXI.settings.SCALE_MODE * @see PIXI.SCALE_MODES */ - this.scaleMode = scaleMode || settings.SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; /** * Set to true once the base texture has successfully loaded. @@ -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/RenderTexture.js b/src/core/textures/RenderTexture.js index 4c5034f..b3c6297 100644 --- a/src/core/textures/RenderTexture.js +++ b/src/core/textures/RenderTexture.js @@ -54,8 +54,8 @@ /* eslint-disable prefer-rest-params, no-console */ const width = arguments[1]; const height = arguments[2]; - const scaleMode = arguments[3] || 0; - const resolution = arguments[4] || 1; + const scaleMode = arguments[3]; + const resolution = arguments[4]; // we have an old render texture.. console.warn(`Please use RenderTexture.create(${width}, ${height}) instead of the ctor directly.`); 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 ca31456..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 @@ -459,7 +497,9 @@ if (frame.x + frame.width > this.baseTexture.width || frame.y + frame.height > this.baseTexture.height) { - throw new Error(`Texture Error: frame does not fit inside the base Texture dimensions ${this}`); + throw new Error('Texture Error: frame does not fit inside the base Texture dimensions: ' + + `X: ${frame.x} + ${frame.width} > ${this.baseTexture.width} ` + + `Y: ${frame.y} + ${frame.height} > ${this.baseTexture.height}`); } // this.valid = frame && frame.width && frame.height && this.baseTexture.source && this.baseTexture.hasLoaded; @@ -520,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. @@ -528,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 2a7497b..99e585a 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)); } /** @@ -360,3 +362,43 @@ * @private */ export const BaseTextureCache = {}; + +/** + * 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/core/utils/trimCanvas.js b/src/core/utils/trimCanvas.js new file mode 100644 index 0000000..5af6de0 --- /dev/null +++ b/src/core/utils/trimCanvas.js @@ -0,0 +1,83 @@ +/** + * Trim transparent borders from a canvas + * + * @memberof PIXI + * @function trimCanvas + * @private + * @param {HTMLCanvasElement} canvas - the canvas to trim + * @returns {object} Trim data + */ +export default function trimCanvas(canvas) +{ + // https://gist.github.com/remy/784508 + + let width = canvas.width; + let height = canvas.height; + + const context = canvas.getContext('2d'); + const imageData = context.getImageData(0, 0, width, height); + const pixels = imageData.data; + const len = pixels.length; + + const bound = { + top: null, + left: null, + right: null, + bottom: null, + }; + let i; + let x; + let y; + + for (i = 0; i < len; i += 4) + { + if (pixels[i + 3] !== 0) + { + x = (i / 4) % width; + y = ~~((i / 4) / width); + + if (bound.top === null) + { + bound.top = y; + } + + if (bound.left === null) + { + bound.left = x; + } + else if (x < bound.left) + { + bound.left = x; + } + + if (bound.right === null) + { + bound.right = x + 1; + } + else if (bound.right < x) + { + bound.right = x + 1; + } + + if (bound.bottom === null) + { + bound.bottom = y; + } + else if (bound.bottom < y) + { + bound.bottom = y; + } + } + } + + width = bound.right - bound.left; + height = bound.bottom - bound.top + 1; + + const data = context.getImageData(bound.left, bound.top, width, height); + + return { + height, + width, + data, + }; +} diff --git a/src/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/extras/TilingSprite.js b/src/extras/TilingSprite.js index f90d248..fe82451 100644 --- a/src/extras/TilingSprite.js +++ b/src/extras/TilingSprite.js @@ -220,20 +220,33 @@ transform.tx * resolution, transform.ty * resolution); - // TODO - this should be rolled into the setTransform above.. - context.scale(this.tileScale.x / baseTextureResolution, this.tileScale.y / baseTextureResolution); - - context.translate(modX + (this.anchor.x * -this._width), - modY + (this.anchor.y * -this._height)); - renderer.setBlendMode(this.blendMode); // fill the pattern! context.fillStyle = this._canvasPattern; - context.fillRect(-modX, - -modY, - this._width / this.tileScale.x * baseTextureResolution, - this._height / this.tileScale.y * baseTextureResolution); + + // TODO - this should be rolled into the setTransform above.. + context.scale(this.tileScale.x / baseTextureResolution, this.tileScale.y / baseTextureResolution); + + const anchorX = this.anchor.x * -this._width; + const anchorY = this.anchor.y * -this._height; + + if (this.uvRespectAnchor) + { + context.translate(modX, modY); + + context.fillRect(-modX + anchorX, -modY + anchorY, + this._width / this.tileScale.x * baseTextureResolution, + this._height / this.tileScale.y * baseTextureResolution); + } + else + { + context.translate(modX + anchorX, modY + anchorY); + + context.fillRect(-modX, -modY, + this._width / this.tileScale.x * baseTextureResolution, + this._height / this.tileScale.y * baseTextureResolution); + } } /** diff --git a/src/extras/cacheAsBitmap.js b/src/extras/cacheAsBitmap.js index 18043a7..5dd5640 100644 --- a/src/extras/cacheAsBitmap.js +++ b/src/extras/cacheAsBitmap.js @@ -41,6 +41,9 @@ * provide a performance benefit for complex static displayObjects. * To remove simply set this property to 'false' * + * IMPORTANT GOTCHA - make sure that all your textures are preloaded BEFORE setting this property to true + * as it will take a snapshot of what is currently there. If the textures have not loaded then they will not appear. + * * @member {boolean} * @memberof PIXI.DisplayObject# */ diff --git a/src/filters/blur/BlurFilter.js b/src/filters/blur/BlurFilter.js index 340fc58..1575db9 100644 --- a/src/filters/blur/BlurFilter.js +++ b/src/filters/blur/BlurFilter.js @@ -24,10 +24,9 @@ this.blurXFilter = new BlurXFilter(strength, quality, resolution, kernelSize); this.blurYFilter = new BlurYFilter(strength, quality, resolution, kernelSize); - this.resolution = 1; this.padding = 0; - this.resolution = resolution || 1; + this.resolution = resolution || core.settings.RESOLUTION; this.quality = quality || 4; this.blur = strength || 8; } diff --git a/src/filters/blur/BlurXFilter.js b/src/filters/blur/BlurXFilter.js index def99c3..0f48889 100644 --- a/src/filters/blur/BlurXFilter.js +++ b/src/filters/blur/BlurXFilter.js @@ -31,7 +31,7 @@ fragSrc ); - this.resolution = resolution || 1; + this.resolution = resolution || core.settings.RESOLUTION; this._quality = 0; diff --git a/src/filters/blur/BlurYFilter.js b/src/filters/blur/BlurYFilter.js index 638e0f0..6d489c7 100644 --- a/src/filters/blur/BlurYFilter.js +++ b/src/filters/blur/BlurYFilter.js @@ -31,7 +31,7 @@ fragSrc ); - this.resolution = resolution || 1; + this.resolution = resolution || core.settings.RESOLUTION; this._quality = 0; diff --git a/src/filters/colormatrix/ColorMatrixFilter.js b/src/filters/colormatrix/ColorMatrixFilter.js index 8509505..679f8ec 100644 --- a/src/filters/colormatrix/ColorMatrixFilter.js +++ b/src/filters/colormatrix/ColorMatrixFilter.js @@ -75,28 +75,28 @@ out[1] = (a[0] * b[1]) + (a[1] * b[6]) + (a[2] * b[11]) + (a[3] * b[16]); out[2] = (a[0] * b[2]) + (a[1] * b[7]) + (a[2] * b[12]) + (a[3] * b[17]); out[3] = (a[0] * b[3]) + (a[1] * b[8]) + (a[2] * b[13]) + (a[3] * b[18]); - out[4] = (a[0] * b[4]) + (a[1] * b[9]) + (a[2] * b[14]) + (a[3] * b[19]); + out[4] = (a[0] * b[4]) + (a[1] * b[9]) + (a[2] * b[14]) + (a[3] * b[19]) + a[4]; // Green Channel out[5] = (a[5] * b[0]) + (a[6] * b[5]) + (a[7] * b[10]) + (a[8] * b[15]); out[6] = (a[5] * b[1]) + (a[6] * b[6]) + (a[7] * b[11]) + (a[8] * b[16]); out[7] = (a[5] * b[2]) + (a[6] * b[7]) + (a[7] * b[12]) + (a[8] * b[17]); out[8] = (a[5] * b[3]) + (a[6] * b[8]) + (a[7] * b[13]) + (a[8] * b[18]); - out[9] = (a[5] * b[4]) + (a[6] * b[9]) + (a[7] * b[14]) + (a[8] * b[19]); + out[9] = (a[5] * b[4]) + (a[6] * b[9]) + (a[7] * b[14]) + (a[8] * b[19]) + a[9]; // Blue Channel out[10] = (a[10] * b[0]) + (a[11] * b[5]) + (a[12] * b[10]) + (a[13] * b[15]); out[11] = (a[10] * b[1]) + (a[11] * b[6]) + (a[12] * b[11]) + (a[13] * b[16]); out[12] = (a[10] * b[2]) + (a[11] * b[7]) + (a[12] * b[12]) + (a[13] * b[17]); out[13] = (a[10] * b[3]) + (a[11] * b[8]) + (a[12] * b[13]) + (a[13] * b[18]); - out[14] = (a[10] * b[4]) + (a[11] * b[9]) + (a[12] * b[14]) + (a[13] * b[19]); + out[14] = (a[10] * b[4]) + (a[11] * b[9]) + (a[12] * b[14]) + (a[13] * b[19]) + a[14]; // Alpha Channel out[15] = (a[15] * b[0]) + (a[16] * b[5]) + (a[17] * b[10]) + (a[18] * b[15]); out[16] = (a[15] * b[1]) + (a[16] * b[6]) + (a[17] * b[11]) + (a[18] * b[16]); out[17] = (a[15] * b[2]) + (a[16] * b[7]) + (a[17] * b[12]) + (a[18] * b[17]); out[18] = (a[15] * b[3]) + (a[16] * b[8]) + (a[17] * b[13]) + (a[18] * b[18]); - out[19] = (a[15] * b[4]) + (a[16] * b[9]) + (a[17] * b[14]) + (a[18] * b[19]); + out[19] = (a[15] * b[4]) + (a[16] * b[9]) + (a[17] * b[14]) + (a[18] * b[19]) + a[19]; return out; } diff --git a/src/filters/colormatrix/colorMatrix.frag b/src/filters/colormatrix/colorMatrix.frag index 3aaf2b3..c73c0e9 100644 --- a/src/filters/colormatrix/colorMatrix.frag +++ b/src/filters/colormatrix/colorMatrix.frag @@ -4,32 +4,38 @@ void main(void) { - vec4 c = texture2D(uSampler, vTextureCoord); + // Un-premultiply alpha before applying the color matrix. See issue #3539. + if (c.a > 0.0) { + c.rgb /= c.a; + } + vec4 result; + result.r = (m[0] * c.r); + result.r += (m[1] * c.g); + result.r += (m[2] * c.b); + result.r += (m[3] * c.a); + result.r += m[4]; - gl_FragColor.r = (m[0] * c.r); - gl_FragColor.r += (m[1] * c.g); - gl_FragColor.r += (m[2] * c.b); - gl_FragColor.r += (m[3] * c.a); - gl_FragColor.r += m[4] * c.a; + result.g = (m[5] * c.r); + result.g += (m[6] * c.g); + result.g += (m[7] * c.b); + result.g += (m[8] * c.a); + result.g += m[9]; - gl_FragColor.g = (m[5] * c.r); - gl_FragColor.g += (m[6] * c.g); - gl_FragColor.g += (m[7] * c.b); - gl_FragColor.g += (m[8] * c.a); - gl_FragColor.g += m[9] * c.a; + result.b = (m[10] * c.r); + result.b += (m[11] * c.g); + result.b += (m[12] * c.b); + result.b += (m[13] * c.a); + result.b += m[14]; - gl_FragColor.b = (m[10] * c.r); - gl_FragColor.b += (m[11] * c.g); - gl_FragColor.b += (m[12] * c.b); - gl_FragColor.b += (m[13] * c.a); - gl_FragColor.b += m[14] * c.a; + result.a = (m[15] * c.r); + result.a += (m[16] * c.g); + result.a += (m[17] * c.b); + result.a += (m[18] * c.a); + result.a += m[19]; - gl_FragColor.a = (m[15] * c.r); - gl_FragColor.a += (m[16] * c.g); - gl_FragColor.a += (m[17] * c.b); - gl_FragColor.a += (m[18] * c.a); - gl_FragColor.a += m[19] * c.a; + // Premultiply alpha again. + result.rgb *= result.a; -// gl_FragColor = vec4(m[0]); + gl_FragColor = result; } diff --git a/src/filters/displacement/DisplacementFilter.js b/src/filters/displacement/DisplacementFilter.js index 112de0b..d5355c3 100644 --- a/src/filters/displacement/DisplacementFilter.js +++ b/src/filters/displacement/DisplacementFilter.js @@ -36,7 +36,7 @@ this.maskMatrix = maskMatrix; this.uniforms.mapSampler = sprite.texture; - this.uniforms.filterMatrix = maskMatrix.toArray(true); + this.uniforms.filterMatrix = maskMatrix; this.uniforms.scale = { x: 1, y: 1 }; if (scale === null || scale === undefined) diff --git a/src/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/InteractionData.js b/src/interaction/InteractionData.js index 02d53a3..818da35 100644 --- a/src/interaction/InteractionData.js +++ b/src/interaction/InteractionData.js @@ -21,9 +21,9 @@ this.global = new core.Point(); /** - * The target Sprite that was interacted with + * The target DisplayObject that was interacted with * - * @member {PIXI.Sprite} + * @member {PIXI.DisplayObject} */ this.target = null; @@ -33,6 +33,13 @@ * @member {Event} */ this.originalEvent = null; + + /** + * Unique identifier for this interaction + * + * @member {number} + */ + this.identifier = null; } /** diff --git a/src/interaction/InteractionEvent.js b/src/interaction/InteractionEvent.js index 75b2966..dc554f9 100644 --- a/src/interaction/InteractionEvent.js +++ b/src/interaction/InteractionEvent.js @@ -58,7 +58,7 @@ } /** - * Prevents event from reaching any objects other than the current object. + * Resets the event. * * @private */ diff --git a/src/interaction/InteractionManager.js b/src/interaction/InteractionManager.js index 7d51134..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/src/prepare/BasePrepare.js b/src/prepare/BasePrepare.js index a2005f5..09eb5e5 100644 --- a/src/prepare/BasePrepare.js +++ b/src/prepare/BasePrepare.js @@ -20,7 +20,7 @@ * * @abstract * @class - * @memberof PIXI + * @memberof PIXI.prepare */ export default class BasePrepare { @@ -107,9 +107,9 @@ /** * Upload all the textures and graphics to the GPU. * - * @param {Function|PIXI.DisplayObject|PIXI.Container} item - Either - * the container or display object to search for items to upload or - * the callback function, if items have been added using `prepare.add`. + * @param {Function|PIXI.DisplayObject|PIXI.Container|PIXI.BaseTexture|PIXI.Texture|PIXI.Graphics|PIXI.Text} item - + * Either the container or display object to search for items to upload, the items to upload themselves, + * or the callback function, if items have been added using `prepare.add`. * @param {Function} [done] - Optional callback when all queued uploads have completed */ upload(item, done) @@ -236,7 +236,8 @@ /** * Manually add an item to the uploading queue. * - * @param {PIXI.DisplayObject|PIXI.Container|*} item - Object to add to the queue + * @param {PIXI.DisplayObject|PIXI.Container|PIXI.BaseTexture|PIXI.Texture|PIXI.Graphics|PIXI.Text|*} item - Object to + * add to the queue * @return {PIXI.CanvasPrepare} Instance of plugin for chaining. */ add(item) diff --git a/src/prepare/canvas/CanvasPrepare.js b/src/prepare/canvas/CanvasPrepare.js index 4b99d22..0224bb5 100644 --- a/src/prepare/canvas/CanvasPrepare.js +++ b/src/prepare/canvas/CanvasPrepare.js @@ -12,7 +12,8 @@ * An instance of this class is automatically created by default, and can be found at renderer.plugins.prepare * * @class - * @memberof PIXI + * @extends PIXI.prepare.BasePrepare + * @memberof PIXI.prepare */ export default class CanvasPrepare extends BasePrepare { diff --git a/src/prepare/webgl/WebGLPrepare.js b/src/prepare/webgl/WebGLPrepare.js index 1ef7467..eb7023f 100644 --- a/src/prepare/webgl/WebGLPrepare.js +++ b/src/prepare/webgl/WebGLPrepare.js @@ -7,7 +7,8 @@ * An instance of this class is automatically created by default, and can be found at renderer.plugins.prepare * * @class - * @memberof PIXI + * @extends PIXI.prepare.BasePrepare + * @memberof PIXI.prepare */ export default class WebGLPrepare extends BasePrepare { diff --git a/test/core/Circle.js b/test/core/Circle.js index 183f98f..f621992 100644 --- a/test/core/Circle.js +++ b/test/core/Circle.js @@ -53,6 +53,10 @@ expect(circ1.contains(10, 16)).to.be.false; expect(circ1.contains(11, 15)).to.be.false; expect(circ1.contains(0, 0)).to.be.false; + + const circ2 = new PIXI.Circle(10, 10, 0); + + expect(circ2.contains(10, 10)).to.be.false; }); it('should return framing rectangle', function () diff --git a/test/core/Container.js b/test/core/Container.js index 8a10b40..cefde54 100644 --- a/test/core/Container.js +++ b/test/core/Container.js @@ -1,5 +1,39 @@ 'use strict'; +function testAddChild(fn) +{ + return function () + { + fn(function (container, obj) + { + container.addChild(obj); + }); + fn(function (container, obj) + { + container.addChildAt(obj); + }); + }; +} + +function testRemoveChild(fn) +{ + return function () + { + 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 +104,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 +279,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/Ellipse.js b/test/core/Ellipse.js index f2e8234..3f47442 100644 --- a/test/core/Ellipse.js +++ b/test/core/Ellipse.js @@ -56,6 +56,10 @@ expect(ellipse1.contains(10, 16)).to.be.false; expect(ellipse1.contains(11, 15)).to.be.false; expect(ellipse1.contains(0, 0)).to.be.false; + + const ellipse2 = new PIXI.Ellipse(10, 10, 0, 0); + + expect(ellipse2.contains(10, 10)).to.be.false; }); it('should return framing rectangle', function () diff --git a/test/core/Graphics.js b/test/core/Graphics.js index 2a64946..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 () @@ -127,6 +130,16 @@ expect(graphics.containsPoint(point)).to.be.false; }); + + it('should return false when no fill', function () + { + const point = new PIXI.Point(1, 1); + const graphics = new PIXI.Graphics(); + + graphics.drawRect(0, 0, 10, 10); + + expect(graphics.containsPoint(point)).to.be.false; + }); }); describe('arc', function () @@ -208,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/Matrix.js b/test/core/Matrix.js index 9d2694b..76bd57c 100644 --- a/test/core/Matrix.js +++ b/test/core/Matrix.js @@ -107,10 +107,60 @@ expect(m1.ty).to.equal(m2.ty); }); + it('should prepend matrix', function () + { + const m1 = new PIXI.Matrix(); + const m2 = new PIXI.Matrix(); + + m2.set(2, 3, 4, 5, 100, 200); + m1.prepend(m2); + + expect(m1.a).to.equal(m2.a); + expect(m1.b).to.equal(m2.b); + expect(m1.c).to.equal(m2.c); + expect(m1.d).to.equal(m2.d); + expect(m1.tx).to.equal(m2.tx); + expect(m1.ty).to.equal(m2.ty); + + const m3 = new PIXI.Matrix(); + const m4 = new PIXI.Matrix(); + + m3.prepend(m4); + + expect(m3.a).to.equal(m4.a); + expect(m3.b).to.equal(m4.b); + expect(m3.c).to.equal(m4.c); + expect(m3.d).to.equal(m4.d); + expect(m3.tx).to.equal(m4.tx); + expect(m3.ty).to.equal(m4.ty); + }); + it('should get IDENTITY and TEMP_MATRIX', function () { expect(PIXI.Matrix.IDENTITY instanceof PIXI.Matrix).to.be.true; expect(PIXI.Matrix.TEMP_MATRIX instanceof PIXI.Matrix).to.be.true; }); -}); + it('should reset matrix to default when identity() is called', function () + { + const matrix = new PIXI.Matrix(); + + matrix.set(2, 3, 4, 5, 100, 200); + + expect(matrix.a).to.equal(2); + expect(matrix.b).to.equal(3); + expect(matrix.c).to.equal(4); + expect(matrix.d).to.equal(5); + expect(matrix.tx).to.equal(100); + expect(matrix.ty).to.equal(200); + + matrix.identity(); + + expect(matrix.a).to.equal(1); + expect(matrix.b).to.equal(0); + expect(matrix.c).to.equal(0); + expect(matrix.d).to.equal(1); + expect(matrix.tx).to.equal(0); + expect(matrix.ty).to.equal(0); + }); +}); 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 1271fef..71ade15 100644 --- a/package.json +++ b/package.json @@ -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/scripts/release.js b/scripts/release.js index c4c8e36..156620c 100644 --- a/scripts/release.js +++ b/scripts/release.js @@ -12,7 +12,8 @@ 'dist/**/*', 'lib/**/*', 'src/**/*', - 'scripts/**/*', + 'scripts/*', + 'scripts/renders/*', 'test/**/*', '*.json', '*.md', diff --git a/scripts/renders/client.js b/scripts/renders/client.js index 16c9565..6faee34 100755 --- a/scripts/renders/client.js +++ b/scripts/renders/client.js @@ -1,6 +1,6 @@ 'use strict'; -require('../../bin/pixi'); +require('../../dist/pixi'); PIXI.utils.skipHello(); const fs = require('fs'); 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 bc730db..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. @@ -33,8 +33,11 @@ * need to call toDataUrl on the webgl context * @param {number} [options.resolution=1] - The resolution / device pixel ratio of the renderer, retina would be 2 * @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 @@ -43,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. */ @@ -73,7 +99,7 @@ */ stop() { - this.ticker.stop(); + this._ticker.stop(); } /** @@ -81,7 +107,7 @@ */ start() { - this.ticker.start(); + this._ticker.start(); } /** @@ -95,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 index 81e076b..78b17e6 100644 --- a/src/core/Shader.js +++ b/src/core/Shader.js @@ -1,9 +1,7 @@ import { GLShader } from 'pixi-gl-core'; import settings from './settings'; -const { PRECISION } = settings; - -function checkPrecision(src) +function checkPrecision(src, def) { if (src instanceof Array) { @@ -11,14 +9,14 @@ { const copy = src.slice(0); - copy.unshift(`precision ${PRECISION} float;`); + copy.unshift(`precision ${def} float;`); return copy; } } else if (src.substring(0, 9) !== 'precision') { - return `precision ${PRECISION} float;\n${src}`; + return `precision ${def} float;\n${src}`; } return src; @@ -42,6 +40,7 @@ */ constructor(gl, vertexSrc, fragmentSrc) { - super(gl, checkPrecision(vertexSrc), checkPrecision(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 3425b41..f9235e4 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -181,7 +181,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 b3792ef..17fde8d 100644 --- a/src/core/display/Container.js +++ b/src/core/display/Container.js @@ -73,13 +73,14 @@ } child.parent = this; - - // ensure a transform will be recalculated.. - this.transform._parentID = -1; - this._boundsID++; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.push(child); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(this.children.length - 1); child.emit('added', this); @@ -108,9 +109,14 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.splice(index, 0, child); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(index); child.emit('added', this); @@ -174,6 +180,7 @@ removeItems(this.children, currentIndex, 1); // remove from old position this.children.splice(index, 0, child); // add at new position + this.onChildrenChange(index); } @@ -220,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! @@ -244,9 +252,14 @@ { const child = this.getChildAt(index); + // ensure child transform will be recalculated.. child.parent = null; + child.transform._parentID = -1; removeItems(this.children, index, 1); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(index); child.emit('removed', this); @@ -275,8 +288,14 @@ for (let i = 0; i < removed.length; ++i) { removed[i].parent = null; + if (removed[i].transform) + { + removed[i].transform._parentID = -1; + } } + this._boundsID++; + this.onChildrenChange(beginIndex); for (let i = 0; i < removed.length; ++i) diff --git a/src/core/display/DisplayObject.js b/src/core/display/DisplayObject.js index 3affe36..4e6c77d 100644 --- a/src/core/display/DisplayObject.js +++ b/src/core/display/DisplayObject.js @@ -109,7 +109,7 @@ /** * The original, cached mask of the object * - * @member {PIXI.Rectangle} + * @member {PIXI.Graphics|PIXI.Sprite} * @private */ this._mask = null; diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index 95f2af4..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 ); @@ -1065,10 +1061,15 @@ canvasRenderer = new CanvasRenderer(); } - tempMatrix.tx = -bounds.x; - tempMatrix.ty = -bounds.y; + this.transform.updateLocalTransform(); + this.transform.localTransform.copy(tempMatrix); - canvasRenderer.render(this, canvasBuffer, false, tempMatrix); + tempMatrix.invert(); + + tempMatrix.tx -= bounds.x; + tempMatrix.ty -= bounds.y; + + canvasRenderer.render(this, canvasBuffer, true, tempMatrix); const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode); 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 e67680f..867dde2 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/buildCircle.js b/src/core/graphics/webgl/utils/buildCircle.js index 0400d13..abf5d02 100644 --- a/src/core/graphics/webgl/utils/buildCircle.js +++ b/src/core/graphics/webgl/utils/buildCircle.js @@ -33,6 +33,11 @@ height = circleData.height; } + if (width === 0 || height === 0) + { + return; + } + const totalSegs = Math.floor(30 * Math.sqrt(circleData.radius)) || Math.floor(15 * Math.sqrt(circleData.width + circleData.height)); diff --git a/src/core/graphics/webgl/utils/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 90155f9..f03fe15 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -30,6 +30,7 @@ export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; export { default as CanvasGraphicsRenderer } from './graphics/canvas/CanvasGraphicsRenderer'; +export { default as Spritesheet } from './textures/Spritesheet'; export { default as Texture } from './textures/Texture'; export { default as BaseTexture } from './textures/BaseTexture'; export { default as RenderTexture } from './textures/RenderTexture'; diff --git a/src/core/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 ccce680..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); } /** @@ -162,6 +162,9 @@ if (transform) { transform.copy(tempWt); + + // lets not forget to flag the parent transform as dirty... + this._tempDisplayObjectParent.transform._worldID = -1; } else { @@ -169,6 +172,7 @@ } displayObject.parent = this._tempDisplayObjectParent; + displayObject.updateTransform(); displayObject.parent = cacheParent; // displayObject.hitArea = //TODO add a temp hit area @@ -279,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/canvas/utils/CanvasRenderTarget.js b/src/core/renderers/canvas/utils/CanvasRenderTarget.js index 3de0aef..b409891 100644 --- a/src/core/renderers/canvas/utils/CanvasRenderTarget.js +++ b/src/core/renderers/canvas/utils/CanvasRenderTarget.js @@ -1,5 +1,4 @@ import settings from '../../../settings'; -const { RESOLUTION } = settings; /** * Creates a Canvas element of the given size. @@ -30,7 +29,7 @@ */ this.context = this.canvas.getContext('2d'); - this.resolution = resolution || RESOLUTION; + this.resolution = resolution || settings.RESOLUTION; this.resize(width, height); } 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 ce0e1c4..788ed6c 100644 --- a/src/core/renderers/webgl/WebGLRenderer.js +++ b/src/core/renderers/webgl/WebGLRenderer.js @@ -30,8 +30,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 @@ -49,10 +49,19 @@ * enable this if you need to call toDataUrl on the webgl context. * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when * rendering, stopping pixel interpolation. + * @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; + + if (this.legacy) + { + glCore.VertexArrayObject.FORCE_NATIVE = true; + } /** * The type of this renderer as a standardised const @@ -229,7 +238,7 @@ this.emit('context', gl); // setup the width/height properties and gl viewport - this.resize(this.width, this.height); + this.resize(this.screen.width, this.screen.height); } /** @@ -323,16 +332,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) { @@ -376,6 +385,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 @@ -440,9 +469,10 @@ * Changes the current shader to the one given in parameter * * @param {PIXI.Shader} shader - the new shader + * @param {boolean} [autoProject=true] - Whether automatically set the projection matrix * @return {PIXI.WebGLRenderer} Returns itself. */ - bindShader(shader) + bindShader(shader, autoProject) { // TODO cache if (this._activeShader !== shader) @@ -450,8 +480,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 bbab3b3..944f41d 100644 --- a/src/core/renderers/webgl/filters/Filter.js +++ b/src/core/renderers/webgl/filters/Filter.js @@ -1,6 +1,7 @@ import extractUniformsFromSrc from './extractUniformsFromSrc'; import { uid } from '../../../utils'; import { BLEND_MODES } from '../../../const'; +import settings from '../../../settings'; const SOURCE_KEY_MAP = {}; @@ -10,7 +11,7 @@ * @memberof PIXI * @extends PIXI.Shader */ -class Filter +export default class Filter { /** * @param {string} [vertexSrc] - The source of the vertex shader. @@ -35,8 +36,6 @@ this.blendMode = BLEND_MODES.NORMAL; - // pull out the vertex and shader uniforms if they are not specified.. - // currently this does not extract structs only default types this.uniformData = uniforms || extractUniformsFromSrc(this.vertexSrc, this.fragmentSrc, 'projectionMatrix|uSampler'); /** @@ -80,7 +79,7 @@ * * @member {number} */ - this.resolution = 1; + this.resolution = settings.RESOLUTION; /** * If enabled is true the filter is applied, if false it will not. @@ -97,8 +96,11 @@ * @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 { // --- // // this.uniforms.filterMatrix = filterManager.calculateSpriteMatrix(tempMatrix, window.panda ); @@ -170,5 +172,3 @@ ].join('\n'); } } - -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 dc7072a..6c7422d 100644 --- a/src/core/renderers/webgl/managers/FilterManager.js +++ b/src/core/renderers/webgl/managers/FilterManager.js @@ -154,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 @@ -176,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; @@ -184,7 +184,7 @@ flop = t; } - filters[i].apply(this, flip, lastState.renderTarget, true); + filters[i].apply(this, flip, lastState.renderTarget, false, currentState); this.freePotRenderTarget(flip); this.freePotRenderTarget(flop); @@ -291,9 +291,13 @@ let textureCount = 1; let currentState; - if (shader.uniforms.data.filterArea) + // 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; @@ -306,9 +310,9 @@ // use this to clamp displaced texture coords so they belong to filterArea // see displacementFilter fragment shader for an example - if (shader.uniforms.data.filterClamp) + if (shader.uniforms.filterClamp) { - currentState = this.filterData.stack[this.filterData.index]; + currentState = currentState || this.filterData.stack[this.filterData.index]; const filterClamp = shader.uniforms.filterClamp; @@ -485,7 +489,7 @@ */ destroy() { - this.shaderCache = []; + this.shaderCache = {}; this.emptyPool(); } diff --git a/src/core/renderers/webgl/utils/RenderTarget.js b/src/core/renderers/webgl/utils/RenderTarget.js index afe51ab..6d2467f 100644 --- a/src/core/renderers/webgl/utils/RenderTarget.js +++ b/src/core/renderers/webgl/utils/RenderTarget.js @@ -124,7 +124,7 @@ * @default PIXI.settings.SCALE_MODE * @see PIXI.SCALE_MODES */ - this.scaleMode = scaleMode || settings.SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; /** * Whether this object is the root element or not 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 36ea5bb..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; @@ -346,8 +346,8 @@ /** * Gets the local bounds of the sprite object. * - * @param {Rectangle} rect - The output rectangle. - * @return {Rectangle} The bounds. + * @param {PIXI.Rectangle} rect - The output rectangle. + * @return {PIXI.Rectangle} The bounds. */ getLocalBounds(rect) { @@ -440,7 +440,7 @@ * * @static * @param {number|string|PIXI.BaseTexture|HTMLCanvasElement|HTMLVideoElement} source Source to create texture from - * @return {PIXI.Texture} 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 e77b39a..90b08c0 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++) @@ -103,11 +102,18 @@ { const gl = this.renderer.gl; - // step 1: first check max textures the GPU can handle. - this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); + if (this.renderer.legacy) + { + this.MAX_TEXTURES = 1; + } + else + { + // step 1: first check max textures the GPU can handle. + this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); - // step 2: check the maximum number of if statements the shader can have too.. - this.MAX_TEXTURES = checkMaxIfStatmentsInShader(this.MAX_TEXTURES, gl); + // step 2: check the maximum number of if statements the shader can have too.. + this.MAX_TEXTURES = checkMaxIfStatmentsInShader(this.MAX_TEXTURES, gl); + } const shader = this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); @@ -130,8 +136,12 @@ .addIndex(this.indexBuffer) .addAttribute(this.vertexBuffers[i], shader.attributes.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) .addAttribute(this.vertexBuffers[i], shader.attributes.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) - .addAttribute(this.vertexBuffers[i], shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4) - .addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + .addAttribute(this.vertexBuffers[i], shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); + + if (shader.attributes.aTextureId) + { + this.vaos[i].addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + } /* eslint-enable max-len */ } @@ -378,8 +388,12 @@ .addIndex(this.indexBuffer) .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) - .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4) - .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); + + if (this.shader.attributes.aTextureId) + { + this.vaos[this.vertexCount].addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + } /* eslint-enable max-len */ } diff --git a/src/core/text/Text.js b/src/core/text/Text.js index 013625a..cec8c32 100644 --- a/src/core/text/Text.js +++ b/src/core/text/Text.js @@ -6,6 +6,7 @@ import { TEXT_GRADIENT } from '../const'; import settings from '../settings'; import TextStyle from './TextStyle'; +import trimCanvas from '../utils/trimCanvas'; const defaultDestroyOptions = { texture: true, @@ -57,7 +58,7 @@ /** * The canvas 2d context that everything is drawn with - * @member {HTMLCanvasElement} + * @member {CanvasRenderingContext2D} */ this.context = this.canvas.getContext('2d'); @@ -189,10 +190,12 @@ if (style.dropShadow) { + this.context.shadowBlur = style.dropShadowBlur; + this.context.globalAlpha = style.dropShadowAlpha; + if (style.dropShadowBlur > 0) { this.context.shadowColor = style.dropShadowColor; - this.context.shadowBlur = style.dropShadowBlur; } else { @@ -237,6 +240,10 @@ } } + // reset the shadow blur and alpha that was set by the drop shadow, for the regular text + this.context.shadowBlur = 0; + this.context.globalAlpha = 1; + // set canvas text styles this.context.fillStyle = this._generateFillStyle(style, lines); @@ -326,6 +333,15 @@ */ updateTexture() { + if (this._style.trim) + { + const trimmed = trimCanvas(this.canvas); + + this.canvas.width = trimmed.width; + this.canvas.height = trimmed.height; + this.context.putImageData(trimmed.data, 0, 0); + } + const texture = this._texture; const style = this._style; @@ -475,6 +491,19 @@ } /** + * Gets the local bounds of the text object. + * + * @param {Rectangle} rect - The output rectangle. + * @return {Rectangle} The bounds. + */ + getLocalBounds(rect) + { + this.updateText(true); + + return super.getLocalBounds.call(this, rect); + } + + /** * calculates the bounds of the Text as a rectangle. The bounds calculation takes the worldTransform into account. */ _calculateBounds() @@ -525,6 +554,29 @@ const width = this.canvas.width / this.resolution; const height = this.canvas.height / this.resolution; + // make a copy of the style settings, so we can manipulate them later + const fill = style.fill.slice(); + const fillGradientStops = style.fillGradientStops.slice(); + + // wanting to evenly distribute the fills. So an array of 4 colours should give fills of 0.25, 0.5 and 0.75 + if (!fillGradientStops.length) + { + const lengthPlus1 = fill.length + 1; + + for (let i = 1; i < lengthPlus1; ++i) + { + fillGradientStops.push(i / lengthPlus1); + } + } + + // stop the bleeding of the last gradient on the line above to the top gradient of the this line + // by hard defining the first gradient colour at point 0, and last gradient colour at point 1 + fill.unshift(style.fill[0]); + fillGradientStops.unshift(0); + + fill.push(style.fill[style.fill.length - 1]); + fillGradientStops.push(1); + if (style.fillGradientType === TEXT_GRADIENT.LINEAR_VERTICAL) { // start the gradient at the top center of the canvas, and end at the bottom middle of the canvas @@ -532,15 +584,22 @@ // we need to repeat the gradient so that each individual line of text has the same vertical gradient effect // ['#FF0000', '#00FF00', '#0000FF'] over 2 lines would create stops at 0.125, 0.25, 0.375, 0.625, 0.75, 0.875 - totalIterations = (style.fill.length + 1) * lines.length; + totalIterations = (fill.length + 1) * lines.length; currentIteration = 0; for (let i = 0; i < lines.length; i++) { currentIteration += 1; - for (let j = 0; j < style.fill.length; j++) + for (let j = 0; j < fill.length; j++) { - stop = (currentIteration / totalIterations); - gradient.addColorStop(stop, style.fill[j]); + if (fillGradientStops[j]) + { + stop = (fillGradientStops[j] / lines.length) + (i / lines.length); + } + else + { + stop = currentIteration / totalIterations; + } + gradient.addColorStop(stop, fill[j]); currentIteration++; } } @@ -552,13 +611,20 @@ // can just evenly space out the gradients in this case, as multiple lines makes no difference // to an even left to right gradient - totalIterations = style.fill.length + 1; + totalIterations = fill.length + 1; currentIteration = 1; - for (let i = 0; i < style.fill.length; i++) + for (let i = 0; i < fill.length; i++) { - stop = currentIteration / totalIterations; - gradient.addColorStop(stop, style.fill[i]); + if (fillGradientStops[i]) + { + stop = fillGradientStops[i]; + } + else + { + stop = currentIteration / totalIterations; + } + gradient.addColorStop(stop, fill[i]); currentIteration++; } } @@ -569,7 +635,7 @@ /** * Destroys this text object. * Note* Unlike a Sprite, a Text object will automatically destroy its baseTexture and texture as - * the majorety of the time the texture will not be shared with any other Sprites. + * the majority of the time the texture will not be shared with any other Sprites. * * @param {object|boolean} [options] - Options parameter. A boolean will act as if all options * have been set to that value @@ -680,7 +746,7 @@ set text(text) // eslint-disable-line require-jsdoc { - text = String(text || ' '); + text = String(text === '' || text === null || text === undefined ? ' ' : text); if (this._text === text) { diff --git a/src/core/text/TextStyle.js b/src/core/text/TextStyle.js index 3b25528..890950b 100644 --- a/src/core/text/TextStyle.js +++ b/src/core/text/TextStyle.js @@ -8,12 +8,14 @@ align: 'left', breakWords: false, dropShadow: false, + dropShadowAlpha: 1, dropShadowAngle: Math.PI / 6, dropShadowBlur: 0, dropShadowColor: '#000000', dropShadowDistance: 5, fill: 'black', fillGradientType: TEXT_GRADIENT.LINEAR_VERTICAL, + fillGradientStops: [], fontFamily: 'Arial', fontSize: 26, fontStyle: 'normal', @@ -27,6 +29,7 @@ stroke: 'black', strokeThickness: 0, textBaseline: 'alphabetic', + trim: false, wordWrap: false, wordWrapWidth: 100, }; @@ -47,6 +50,7 @@ * @param {boolean} [style.breakWords=false] - Indicates if lines can be wrapped within words, it * needs wordWrap to be set to true * @param {boolean} [style.dropShadow=false] - Set a drop shadow for the text + * @param {number} [style.dropShadowAlpha=1] - Set alpha for the drop shadow * @param {number} [style.dropShadowAngle=Math.PI/6] - Set a angle of the drop shadow * @param {number} [style.dropShadowBlur=0] - Set a shadow blur radius * @param {string} [style.dropShadowColor='#000000'] - A fill style to be used on the dropshadow e.g 'red', '#00FF00' @@ -55,8 +59,10 @@ * fillstyle that will be used on the text e.g 'red', '#00FF00'. Can be an array to create a gradient * eg ['#000000','#FFFFFF'] * {@link https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/fillStyle|MDN} - * @param {number} [style.fillGradientType=PIXI.TEXT_GRADIENT.LINEAR_VERTICAL] - If fills styles are - * supplied, this can change the type/direction of the gradient. See {@link PIXI.TEXT_GRADIENT} for possible values + * @param {number} [style.fillGradientType=PIXI.TEXT_GRADIENT.LINEAR_VERTICAL] - If fill is an array of colours + * to create a gradient, this can change the type/direction of the gradient. See {@link PIXI.TEXT_GRADIENT} + * @param {number[]} [style.fillGradientStops] - If fill is an array of colours to create a gradient, this array can set + * the stop points (numbers between 0 and 1) for the color, overriding the default behaviour of evenly spacing them. * @param {string|string[]} [style.fontFamily='Arial'] - The font family * @param {number|string} [style.fontSize=26] - The font size (as a number it converts to px, but as a string, * equivalents are '26px','20pt','160%' or '1.6em') @@ -76,6 +82,7 @@ * e.g 'blue', '#FCFF00' * @param {number} [style.strokeThickness=0] - A number that represents the thickness of the stroke. * Default is 0 (no stroke) + * @param {boolean} [style.trim=false] - Trim transparent borders * @param {string} [style.textBaseline='alphabetic'] - The baseline of the text that is rendered. * @param {boolean} [style.wordWrap=false] - Indicates if word wrap should be used * @param {number} [style.wordWrapWidth=100] - The width at which text will wrap, it needs wordWrap to be set to true @@ -152,6 +159,19 @@ } } + get dropShadowAlpha() + { + return this._dropShadowAlpha; + } + set dropShadowAlpha(dropShadowAlpha) + { + if (this._dropShadowAlpha !== dropShadowAlpha) + { + this._dropShadowAlpha = dropShadowAlpha; + this.styleID++; + } + } + get dropShadowAngle() { return this._dropShadowAngle; @@ -232,6 +252,19 @@ } } + get fillGradientStops() + { + return this._fillGradientStops; + } + set fillGradientStops(fillGradientStops) + { + if (!areArraysEqual(this._fillGradientStops,fillGradientStops)) + { + this._fillGradientStops = fillGradientStops; + this.styleID++; + } + } + get fontFamily() { return this._fontFamily; @@ -402,6 +435,19 @@ } } + get trim() + { + return this._trim; + } + set trim(trim) + { + if (this._trim !== trim) + { + this._trim = trim; + this.styleID++; + } + } + get wordWrap() { return this._wordWrap; @@ -475,3 +521,34 @@ return color; } } + +/** + * Utility function to convert hexadecimal colors to strings, and simply return the color if it's a string. + * This version can also convert array of colors + * + * @param {Array} array1 First array to compare + * @param {Array} array2 Second array to compare + * @return {boolean} Do the arrays contain the same values in the same order + */ +function areArraysEqual(array1, array2) +{ + if (!Array.isArray(array1) || !Array.isArray(array2)) + { + return false; + } + + if (array1.length !== array2.length) + { + return false; + } + + for (let i = 0; i < array1.length; ++i) + { + if (array1[i] !== array2[i]) + { + return false; + } + } + + return true; +} diff --git a/src/core/textures/BaseRenderTexture.js b/src/core/textures/BaseRenderTexture.js index 5d50146..aa727f0 100644 --- a/src/core/textures/BaseRenderTexture.js +++ b/src/core/textures/BaseRenderTexture.js @@ -1,8 +1,6 @@ import BaseTexture from './BaseTexture'; import settings from '../settings'; -const { RESOLUTION, SCALE_MODE } = settings; - /** * A BaseRenderTexture is a special texture that allows any Pixi display object to be rendered to it. * @@ -54,7 +52,7 @@ { super(null, scaleMode); - this.resolution = resolution || RESOLUTION; + this.resolution = resolution || settings.RESOLUTION; this.width = width; this.height = height; @@ -62,7 +60,7 @@ this.realWidth = this.width * this.resolution; this.realHeight = this.height * this.resolution; - this.scaleMode = scaleMode || SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; this.hasLoaded = true; /** diff --git a/src/core/textures/BaseTexture.js b/src/core/textures/BaseTexture.js index 676dca3..aeccf39 100644 --- a/src/core/textures/BaseTexture.js +++ b/src/core/textures/BaseTexture.js @@ -77,7 +77,7 @@ * @default PIXI.settings.SCALE_MODE * @see PIXI.SCALE_MODES */ - this.scaleMode = scaleMode || settings.SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; /** * Set to true once the base texture has successfully loaded. @@ -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/RenderTexture.js b/src/core/textures/RenderTexture.js index 4c5034f..b3c6297 100644 --- a/src/core/textures/RenderTexture.js +++ b/src/core/textures/RenderTexture.js @@ -54,8 +54,8 @@ /* eslint-disable prefer-rest-params, no-console */ const width = arguments[1]; const height = arguments[2]; - const scaleMode = arguments[3] || 0; - const resolution = arguments[4] || 1; + const scaleMode = arguments[3]; + const resolution = arguments[4]; // we have an old render texture.. console.warn(`Please use RenderTexture.create(${width}, ${height}) instead of the ctor directly.`); 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 ca31456..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 @@ -459,7 +497,9 @@ if (frame.x + frame.width > this.baseTexture.width || frame.y + frame.height > this.baseTexture.height) { - throw new Error(`Texture Error: frame does not fit inside the base Texture dimensions ${this}`); + throw new Error('Texture Error: frame does not fit inside the base Texture dimensions: ' + + `X: ${frame.x} + ${frame.width} > ${this.baseTexture.width} ` + + `Y: ${frame.y} + ${frame.height} > ${this.baseTexture.height}`); } // this.valid = frame && frame.width && frame.height && this.baseTexture.source && this.baseTexture.hasLoaded; @@ -520,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. @@ -528,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 2a7497b..99e585a 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)); } /** @@ -360,3 +362,43 @@ * @private */ export const BaseTextureCache = {}; + +/** + * 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/core/utils/trimCanvas.js b/src/core/utils/trimCanvas.js new file mode 100644 index 0000000..5af6de0 --- /dev/null +++ b/src/core/utils/trimCanvas.js @@ -0,0 +1,83 @@ +/** + * Trim transparent borders from a canvas + * + * @memberof PIXI + * @function trimCanvas + * @private + * @param {HTMLCanvasElement} canvas - the canvas to trim + * @returns {object} Trim data + */ +export default function trimCanvas(canvas) +{ + // https://gist.github.com/remy/784508 + + let width = canvas.width; + let height = canvas.height; + + const context = canvas.getContext('2d'); + const imageData = context.getImageData(0, 0, width, height); + const pixels = imageData.data; + const len = pixels.length; + + const bound = { + top: null, + left: null, + right: null, + bottom: null, + }; + let i; + let x; + let y; + + for (i = 0; i < len; i += 4) + { + if (pixels[i + 3] !== 0) + { + x = (i / 4) % width; + y = ~~((i / 4) / width); + + if (bound.top === null) + { + bound.top = y; + } + + if (bound.left === null) + { + bound.left = x; + } + else if (x < bound.left) + { + bound.left = x; + } + + if (bound.right === null) + { + bound.right = x + 1; + } + else if (bound.right < x) + { + bound.right = x + 1; + } + + if (bound.bottom === null) + { + bound.bottom = y; + } + else if (bound.bottom < y) + { + bound.bottom = y; + } + } + } + + width = bound.right - bound.left; + height = bound.bottom - bound.top + 1; + + const data = context.getImageData(bound.left, bound.top, width, height); + + return { + height, + width, + data, + }; +} diff --git a/src/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/extras/TilingSprite.js b/src/extras/TilingSprite.js index f90d248..fe82451 100644 --- a/src/extras/TilingSprite.js +++ b/src/extras/TilingSprite.js @@ -220,20 +220,33 @@ transform.tx * resolution, transform.ty * resolution); - // TODO - this should be rolled into the setTransform above.. - context.scale(this.tileScale.x / baseTextureResolution, this.tileScale.y / baseTextureResolution); - - context.translate(modX + (this.anchor.x * -this._width), - modY + (this.anchor.y * -this._height)); - renderer.setBlendMode(this.blendMode); // fill the pattern! context.fillStyle = this._canvasPattern; - context.fillRect(-modX, - -modY, - this._width / this.tileScale.x * baseTextureResolution, - this._height / this.tileScale.y * baseTextureResolution); + + // TODO - this should be rolled into the setTransform above.. + context.scale(this.tileScale.x / baseTextureResolution, this.tileScale.y / baseTextureResolution); + + const anchorX = this.anchor.x * -this._width; + const anchorY = this.anchor.y * -this._height; + + if (this.uvRespectAnchor) + { + context.translate(modX, modY); + + context.fillRect(-modX + anchorX, -modY + anchorY, + this._width / this.tileScale.x * baseTextureResolution, + this._height / this.tileScale.y * baseTextureResolution); + } + else + { + context.translate(modX + anchorX, modY + anchorY); + + context.fillRect(-modX, -modY, + this._width / this.tileScale.x * baseTextureResolution, + this._height / this.tileScale.y * baseTextureResolution); + } } /** diff --git a/src/extras/cacheAsBitmap.js b/src/extras/cacheAsBitmap.js index 18043a7..5dd5640 100644 --- a/src/extras/cacheAsBitmap.js +++ b/src/extras/cacheAsBitmap.js @@ -41,6 +41,9 @@ * provide a performance benefit for complex static displayObjects. * To remove simply set this property to 'false' * + * IMPORTANT GOTCHA - make sure that all your textures are preloaded BEFORE setting this property to true + * as it will take a snapshot of what is currently there. If the textures have not loaded then they will not appear. + * * @member {boolean} * @memberof PIXI.DisplayObject# */ diff --git a/src/filters/blur/BlurFilter.js b/src/filters/blur/BlurFilter.js index 340fc58..1575db9 100644 --- a/src/filters/blur/BlurFilter.js +++ b/src/filters/blur/BlurFilter.js @@ -24,10 +24,9 @@ this.blurXFilter = new BlurXFilter(strength, quality, resolution, kernelSize); this.blurYFilter = new BlurYFilter(strength, quality, resolution, kernelSize); - this.resolution = 1; this.padding = 0; - this.resolution = resolution || 1; + this.resolution = resolution || core.settings.RESOLUTION; this.quality = quality || 4; this.blur = strength || 8; } diff --git a/src/filters/blur/BlurXFilter.js b/src/filters/blur/BlurXFilter.js index def99c3..0f48889 100644 --- a/src/filters/blur/BlurXFilter.js +++ b/src/filters/blur/BlurXFilter.js @@ -31,7 +31,7 @@ fragSrc ); - this.resolution = resolution || 1; + this.resolution = resolution || core.settings.RESOLUTION; this._quality = 0; diff --git a/src/filters/blur/BlurYFilter.js b/src/filters/blur/BlurYFilter.js index 638e0f0..6d489c7 100644 --- a/src/filters/blur/BlurYFilter.js +++ b/src/filters/blur/BlurYFilter.js @@ -31,7 +31,7 @@ fragSrc ); - this.resolution = resolution || 1; + this.resolution = resolution || core.settings.RESOLUTION; this._quality = 0; diff --git a/src/filters/colormatrix/ColorMatrixFilter.js b/src/filters/colormatrix/ColorMatrixFilter.js index 8509505..679f8ec 100644 --- a/src/filters/colormatrix/ColorMatrixFilter.js +++ b/src/filters/colormatrix/ColorMatrixFilter.js @@ -75,28 +75,28 @@ out[1] = (a[0] * b[1]) + (a[1] * b[6]) + (a[2] * b[11]) + (a[3] * b[16]); out[2] = (a[0] * b[2]) + (a[1] * b[7]) + (a[2] * b[12]) + (a[3] * b[17]); out[3] = (a[0] * b[3]) + (a[1] * b[8]) + (a[2] * b[13]) + (a[3] * b[18]); - out[4] = (a[0] * b[4]) + (a[1] * b[9]) + (a[2] * b[14]) + (a[3] * b[19]); + out[4] = (a[0] * b[4]) + (a[1] * b[9]) + (a[2] * b[14]) + (a[3] * b[19]) + a[4]; // Green Channel out[5] = (a[5] * b[0]) + (a[6] * b[5]) + (a[7] * b[10]) + (a[8] * b[15]); out[6] = (a[5] * b[1]) + (a[6] * b[6]) + (a[7] * b[11]) + (a[8] * b[16]); out[7] = (a[5] * b[2]) + (a[6] * b[7]) + (a[7] * b[12]) + (a[8] * b[17]); out[8] = (a[5] * b[3]) + (a[6] * b[8]) + (a[7] * b[13]) + (a[8] * b[18]); - out[9] = (a[5] * b[4]) + (a[6] * b[9]) + (a[7] * b[14]) + (a[8] * b[19]); + out[9] = (a[5] * b[4]) + (a[6] * b[9]) + (a[7] * b[14]) + (a[8] * b[19]) + a[9]; // Blue Channel out[10] = (a[10] * b[0]) + (a[11] * b[5]) + (a[12] * b[10]) + (a[13] * b[15]); out[11] = (a[10] * b[1]) + (a[11] * b[6]) + (a[12] * b[11]) + (a[13] * b[16]); out[12] = (a[10] * b[2]) + (a[11] * b[7]) + (a[12] * b[12]) + (a[13] * b[17]); out[13] = (a[10] * b[3]) + (a[11] * b[8]) + (a[12] * b[13]) + (a[13] * b[18]); - out[14] = (a[10] * b[4]) + (a[11] * b[9]) + (a[12] * b[14]) + (a[13] * b[19]); + out[14] = (a[10] * b[4]) + (a[11] * b[9]) + (a[12] * b[14]) + (a[13] * b[19]) + a[14]; // Alpha Channel out[15] = (a[15] * b[0]) + (a[16] * b[5]) + (a[17] * b[10]) + (a[18] * b[15]); out[16] = (a[15] * b[1]) + (a[16] * b[6]) + (a[17] * b[11]) + (a[18] * b[16]); out[17] = (a[15] * b[2]) + (a[16] * b[7]) + (a[17] * b[12]) + (a[18] * b[17]); out[18] = (a[15] * b[3]) + (a[16] * b[8]) + (a[17] * b[13]) + (a[18] * b[18]); - out[19] = (a[15] * b[4]) + (a[16] * b[9]) + (a[17] * b[14]) + (a[18] * b[19]); + out[19] = (a[15] * b[4]) + (a[16] * b[9]) + (a[17] * b[14]) + (a[18] * b[19]) + a[19]; return out; } diff --git a/src/filters/colormatrix/colorMatrix.frag b/src/filters/colormatrix/colorMatrix.frag index 3aaf2b3..c73c0e9 100644 --- a/src/filters/colormatrix/colorMatrix.frag +++ b/src/filters/colormatrix/colorMatrix.frag @@ -4,32 +4,38 @@ void main(void) { - vec4 c = texture2D(uSampler, vTextureCoord); + // Un-premultiply alpha before applying the color matrix. See issue #3539. + if (c.a > 0.0) { + c.rgb /= c.a; + } + vec4 result; + result.r = (m[0] * c.r); + result.r += (m[1] * c.g); + result.r += (m[2] * c.b); + result.r += (m[3] * c.a); + result.r += m[4]; - gl_FragColor.r = (m[0] * c.r); - gl_FragColor.r += (m[1] * c.g); - gl_FragColor.r += (m[2] * c.b); - gl_FragColor.r += (m[3] * c.a); - gl_FragColor.r += m[4] * c.a; + result.g = (m[5] * c.r); + result.g += (m[6] * c.g); + result.g += (m[7] * c.b); + result.g += (m[8] * c.a); + result.g += m[9]; - gl_FragColor.g = (m[5] * c.r); - gl_FragColor.g += (m[6] * c.g); - gl_FragColor.g += (m[7] * c.b); - gl_FragColor.g += (m[8] * c.a); - gl_FragColor.g += m[9] * c.a; + result.b = (m[10] * c.r); + result.b += (m[11] * c.g); + result.b += (m[12] * c.b); + result.b += (m[13] * c.a); + result.b += m[14]; - gl_FragColor.b = (m[10] * c.r); - gl_FragColor.b += (m[11] * c.g); - gl_FragColor.b += (m[12] * c.b); - gl_FragColor.b += (m[13] * c.a); - gl_FragColor.b += m[14] * c.a; + result.a = (m[15] * c.r); + result.a += (m[16] * c.g); + result.a += (m[17] * c.b); + result.a += (m[18] * c.a); + result.a += m[19]; - gl_FragColor.a = (m[15] * c.r); - gl_FragColor.a += (m[16] * c.g); - gl_FragColor.a += (m[17] * c.b); - gl_FragColor.a += (m[18] * c.a); - gl_FragColor.a += m[19] * c.a; + // Premultiply alpha again. + result.rgb *= result.a; -// gl_FragColor = vec4(m[0]); + gl_FragColor = result; } diff --git a/src/filters/displacement/DisplacementFilter.js b/src/filters/displacement/DisplacementFilter.js index 112de0b..d5355c3 100644 --- a/src/filters/displacement/DisplacementFilter.js +++ b/src/filters/displacement/DisplacementFilter.js @@ -36,7 +36,7 @@ this.maskMatrix = maskMatrix; this.uniforms.mapSampler = sprite.texture; - this.uniforms.filterMatrix = maskMatrix.toArray(true); + this.uniforms.filterMatrix = maskMatrix; this.uniforms.scale = { x: 1, y: 1 }; if (scale === null || scale === undefined) diff --git a/src/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/InteractionData.js b/src/interaction/InteractionData.js index 02d53a3..818da35 100644 --- a/src/interaction/InteractionData.js +++ b/src/interaction/InteractionData.js @@ -21,9 +21,9 @@ this.global = new core.Point(); /** - * The target Sprite that was interacted with + * The target DisplayObject that was interacted with * - * @member {PIXI.Sprite} + * @member {PIXI.DisplayObject} */ this.target = null; @@ -33,6 +33,13 @@ * @member {Event} */ this.originalEvent = null; + + /** + * Unique identifier for this interaction + * + * @member {number} + */ + this.identifier = null; } /** diff --git a/src/interaction/InteractionEvent.js b/src/interaction/InteractionEvent.js index 75b2966..dc554f9 100644 --- a/src/interaction/InteractionEvent.js +++ b/src/interaction/InteractionEvent.js @@ -58,7 +58,7 @@ } /** - * Prevents event from reaching any objects other than the current object. + * Resets the event. * * @private */ diff --git a/src/interaction/InteractionManager.js b/src/interaction/InteractionManager.js index 7d51134..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/src/prepare/BasePrepare.js b/src/prepare/BasePrepare.js index a2005f5..09eb5e5 100644 --- a/src/prepare/BasePrepare.js +++ b/src/prepare/BasePrepare.js @@ -20,7 +20,7 @@ * * @abstract * @class - * @memberof PIXI + * @memberof PIXI.prepare */ export default class BasePrepare { @@ -107,9 +107,9 @@ /** * Upload all the textures and graphics to the GPU. * - * @param {Function|PIXI.DisplayObject|PIXI.Container} item - Either - * the container or display object to search for items to upload or - * the callback function, if items have been added using `prepare.add`. + * @param {Function|PIXI.DisplayObject|PIXI.Container|PIXI.BaseTexture|PIXI.Texture|PIXI.Graphics|PIXI.Text} item - + * Either the container or display object to search for items to upload, the items to upload themselves, + * or the callback function, if items have been added using `prepare.add`. * @param {Function} [done] - Optional callback when all queued uploads have completed */ upload(item, done) @@ -236,7 +236,8 @@ /** * Manually add an item to the uploading queue. * - * @param {PIXI.DisplayObject|PIXI.Container|*} item - Object to add to the queue + * @param {PIXI.DisplayObject|PIXI.Container|PIXI.BaseTexture|PIXI.Texture|PIXI.Graphics|PIXI.Text|*} item - Object to + * add to the queue * @return {PIXI.CanvasPrepare} Instance of plugin for chaining. */ add(item) diff --git a/src/prepare/canvas/CanvasPrepare.js b/src/prepare/canvas/CanvasPrepare.js index 4b99d22..0224bb5 100644 --- a/src/prepare/canvas/CanvasPrepare.js +++ b/src/prepare/canvas/CanvasPrepare.js @@ -12,7 +12,8 @@ * An instance of this class is automatically created by default, and can be found at renderer.plugins.prepare * * @class - * @memberof PIXI + * @extends PIXI.prepare.BasePrepare + * @memberof PIXI.prepare */ export default class CanvasPrepare extends BasePrepare { diff --git a/src/prepare/webgl/WebGLPrepare.js b/src/prepare/webgl/WebGLPrepare.js index 1ef7467..eb7023f 100644 --- a/src/prepare/webgl/WebGLPrepare.js +++ b/src/prepare/webgl/WebGLPrepare.js @@ -7,7 +7,8 @@ * An instance of this class is automatically created by default, and can be found at renderer.plugins.prepare * * @class - * @memberof PIXI + * @extends PIXI.prepare.BasePrepare + * @memberof PIXI.prepare */ export default class WebGLPrepare extends BasePrepare { diff --git a/test/core/Circle.js b/test/core/Circle.js index 183f98f..f621992 100644 --- a/test/core/Circle.js +++ b/test/core/Circle.js @@ -53,6 +53,10 @@ expect(circ1.contains(10, 16)).to.be.false; expect(circ1.contains(11, 15)).to.be.false; expect(circ1.contains(0, 0)).to.be.false; + + const circ2 = new PIXI.Circle(10, 10, 0); + + expect(circ2.contains(10, 10)).to.be.false; }); it('should return framing rectangle', function () diff --git a/test/core/Container.js b/test/core/Container.js index 8a10b40..cefde54 100644 --- a/test/core/Container.js +++ b/test/core/Container.js @@ -1,5 +1,39 @@ 'use strict'; +function testAddChild(fn) +{ + return function () + { + fn(function (container, obj) + { + container.addChild(obj); + }); + fn(function (container, obj) + { + container.addChildAt(obj); + }); + }; +} + +function testRemoveChild(fn) +{ + return function () + { + 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 +104,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 +279,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/Ellipse.js b/test/core/Ellipse.js index f2e8234..3f47442 100644 --- a/test/core/Ellipse.js +++ b/test/core/Ellipse.js @@ -56,6 +56,10 @@ expect(ellipse1.contains(10, 16)).to.be.false; expect(ellipse1.contains(11, 15)).to.be.false; expect(ellipse1.contains(0, 0)).to.be.false; + + const ellipse2 = new PIXI.Ellipse(10, 10, 0, 0); + + expect(ellipse2.contains(10, 10)).to.be.false; }); it('should return framing rectangle', function () diff --git a/test/core/Graphics.js b/test/core/Graphics.js index 2a64946..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 () @@ -127,6 +130,16 @@ expect(graphics.containsPoint(point)).to.be.false; }); + + it('should return false when no fill', function () + { + const point = new PIXI.Point(1, 1); + const graphics = new PIXI.Graphics(); + + graphics.drawRect(0, 0, 10, 10); + + expect(graphics.containsPoint(point)).to.be.false; + }); }); describe('arc', function () @@ -208,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/Matrix.js b/test/core/Matrix.js index 9d2694b..76bd57c 100644 --- a/test/core/Matrix.js +++ b/test/core/Matrix.js @@ -107,10 +107,60 @@ expect(m1.ty).to.equal(m2.ty); }); + it('should prepend matrix', function () + { + const m1 = new PIXI.Matrix(); + const m2 = new PIXI.Matrix(); + + m2.set(2, 3, 4, 5, 100, 200); + m1.prepend(m2); + + expect(m1.a).to.equal(m2.a); + expect(m1.b).to.equal(m2.b); + expect(m1.c).to.equal(m2.c); + expect(m1.d).to.equal(m2.d); + expect(m1.tx).to.equal(m2.tx); + expect(m1.ty).to.equal(m2.ty); + + const m3 = new PIXI.Matrix(); + const m4 = new PIXI.Matrix(); + + m3.prepend(m4); + + expect(m3.a).to.equal(m4.a); + expect(m3.b).to.equal(m4.b); + expect(m3.c).to.equal(m4.c); + expect(m3.d).to.equal(m4.d); + expect(m3.tx).to.equal(m4.tx); + expect(m3.ty).to.equal(m4.ty); + }); + it('should get IDENTITY and TEMP_MATRIX', function () { expect(PIXI.Matrix.IDENTITY instanceof PIXI.Matrix).to.be.true; expect(PIXI.Matrix.TEMP_MATRIX instanceof PIXI.Matrix).to.be.true; }); -}); + it('should reset matrix to default when identity() is called', function () + { + const matrix = new PIXI.Matrix(); + + matrix.set(2, 3, 4, 5, 100, 200); + + expect(matrix.a).to.equal(2); + expect(matrix.b).to.equal(3); + expect(matrix.c).to.equal(4); + expect(matrix.d).to.equal(5); + expect(matrix.tx).to.equal(100); + expect(matrix.ty).to.equal(200); + + matrix.identity(); + + expect(matrix.a).to.equal(1); + expect(matrix.b).to.equal(0); + expect(matrix.c).to.equal(0); + expect(matrix.d).to.equal(1); + expect(matrix.tx).to.equal(0); + expect(matrix.ty).to.equal(0); + }); +}); 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/Text.js b/test/core/Text.js index b8ce561..5799ad4 100644 --- a/test/core/Text.js +++ b/test/core/Text.js @@ -112,4 +112,42 @@ expect(text.width).to.equal(300); }); }); + + describe('text', function () + { + it('should convert numbers into strings', function () + { + const text = new PIXI.Text(2); + + expect(text.text).to.equal('2'); + }); + + it('should not change 0 to \'\'', function () + { + const text = new PIXI.Text(0); + + expect(text.text).to.equal('0'); + }); + + it('should prevent setting null', function () + { + const text = new PIXI.Text(null); + + expect(text.text).to.equal(' '); + }); + + it('should prevent setting undefined', function () + { + const text = new PIXI.Text(); + + expect(text.text).to.equal(' '); + }); + + it('should prevent setting \'\'', function () + { + const text = new PIXI.Text(''); + + expect(text.text).to.equal(' '); + }); + }); }); diff --git a/package.json b/package.json index 1271fef..71ade15 100644 --- a/package.json +++ b/package.json @@ -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/scripts/release.js b/scripts/release.js index c4c8e36..156620c 100644 --- a/scripts/release.js +++ b/scripts/release.js @@ -12,7 +12,8 @@ 'dist/**/*', 'lib/**/*', 'src/**/*', - 'scripts/**/*', + 'scripts/*', + 'scripts/renders/*', 'test/**/*', '*.json', '*.md', diff --git a/scripts/renders/client.js b/scripts/renders/client.js index 16c9565..6faee34 100755 --- a/scripts/renders/client.js +++ b/scripts/renders/client.js @@ -1,6 +1,6 @@ 'use strict'; -require('../../bin/pixi'); +require('../../dist/pixi'); PIXI.utils.skipHello(); const fs = require('fs'); 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 bc730db..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. @@ -33,8 +33,11 @@ * need to call toDataUrl on the webgl context * @param {number} [options.resolution=1] - The resolution / device pixel ratio of the renderer, retina would be 2 * @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 @@ -43,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. */ @@ -73,7 +99,7 @@ */ stop() { - this.ticker.stop(); + this._ticker.stop(); } /** @@ -81,7 +107,7 @@ */ start() { - this.ticker.start(); + this._ticker.start(); } /** @@ -95,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 index 81e076b..78b17e6 100644 --- a/src/core/Shader.js +++ b/src/core/Shader.js @@ -1,9 +1,7 @@ import { GLShader } from 'pixi-gl-core'; import settings from './settings'; -const { PRECISION } = settings; - -function checkPrecision(src) +function checkPrecision(src, def) { if (src instanceof Array) { @@ -11,14 +9,14 @@ { const copy = src.slice(0); - copy.unshift(`precision ${PRECISION} float;`); + copy.unshift(`precision ${def} float;`); return copy; } } else if (src.substring(0, 9) !== 'precision') { - return `precision ${PRECISION} float;\n${src}`; + return `precision ${def} float;\n${src}`; } return src; @@ -42,6 +40,7 @@ */ constructor(gl, vertexSrc, fragmentSrc) { - super(gl, checkPrecision(vertexSrc), checkPrecision(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 3425b41..f9235e4 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -181,7 +181,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 b3792ef..17fde8d 100644 --- a/src/core/display/Container.js +++ b/src/core/display/Container.js @@ -73,13 +73,14 @@ } child.parent = this; - - // ensure a transform will be recalculated.. - this.transform._parentID = -1; - this._boundsID++; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.push(child); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(this.children.length - 1); child.emit('added', this); @@ -108,9 +109,14 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.splice(index, 0, child); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(index); child.emit('added', this); @@ -174,6 +180,7 @@ removeItems(this.children, currentIndex, 1); // remove from old position this.children.splice(index, 0, child); // add at new position + this.onChildrenChange(index); } @@ -220,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! @@ -244,9 +252,14 @@ { const child = this.getChildAt(index); + // ensure child transform will be recalculated.. child.parent = null; + child.transform._parentID = -1; removeItems(this.children, index, 1); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(index); child.emit('removed', this); @@ -275,8 +288,14 @@ for (let i = 0; i < removed.length; ++i) { removed[i].parent = null; + if (removed[i].transform) + { + removed[i].transform._parentID = -1; + } } + this._boundsID++; + this.onChildrenChange(beginIndex); for (let i = 0; i < removed.length; ++i) diff --git a/src/core/display/DisplayObject.js b/src/core/display/DisplayObject.js index 3affe36..4e6c77d 100644 --- a/src/core/display/DisplayObject.js +++ b/src/core/display/DisplayObject.js @@ -109,7 +109,7 @@ /** * The original, cached mask of the object * - * @member {PIXI.Rectangle} + * @member {PIXI.Graphics|PIXI.Sprite} * @private */ this._mask = null; diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index 95f2af4..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 ); @@ -1065,10 +1061,15 @@ canvasRenderer = new CanvasRenderer(); } - tempMatrix.tx = -bounds.x; - tempMatrix.ty = -bounds.y; + this.transform.updateLocalTransform(); + this.transform.localTransform.copy(tempMatrix); - canvasRenderer.render(this, canvasBuffer, false, tempMatrix); + tempMatrix.invert(); + + tempMatrix.tx -= bounds.x; + tempMatrix.ty -= bounds.y; + + canvasRenderer.render(this, canvasBuffer, true, tempMatrix); const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode); 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 e67680f..867dde2 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/buildCircle.js b/src/core/graphics/webgl/utils/buildCircle.js index 0400d13..abf5d02 100644 --- a/src/core/graphics/webgl/utils/buildCircle.js +++ b/src/core/graphics/webgl/utils/buildCircle.js @@ -33,6 +33,11 @@ height = circleData.height; } + if (width === 0 || height === 0) + { + return; + } + const totalSegs = Math.floor(30 * Math.sqrt(circleData.radius)) || Math.floor(15 * Math.sqrt(circleData.width + circleData.height)); diff --git a/src/core/graphics/webgl/utils/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 90155f9..f03fe15 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -30,6 +30,7 @@ export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; export { default as CanvasGraphicsRenderer } from './graphics/canvas/CanvasGraphicsRenderer'; +export { default as Spritesheet } from './textures/Spritesheet'; export { default as Texture } from './textures/Texture'; export { default as BaseTexture } from './textures/BaseTexture'; export { default as RenderTexture } from './textures/RenderTexture'; diff --git a/src/core/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 ccce680..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); } /** @@ -162,6 +162,9 @@ if (transform) { transform.copy(tempWt); + + // lets not forget to flag the parent transform as dirty... + this._tempDisplayObjectParent.transform._worldID = -1; } else { @@ -169,6 +172,7 @@ } displayObject.parent = this._tempDisplayObjectParent; + displayObject.updateTransform(); displayObject.parent = cacheParent; // displayObject.hitArea = //TODO add a temp hit area @@ -279,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/canvas/utils/CanvasRenderTarget.js b/src/core/renderers/canvas/utils/CanvasRenderTarget.js index 3de0aef..b409891 100644 --- a/src/core/renderers/canvas/utils/CanvasRenderTarget.js +++ b/src/core/renderers/canvas/utils/CanvasRenderTarget.js @@ -1,5 +1,4 @@ import settings from '../../../settings'; -const { RESOLUTION } = settings; /** * Creates a Canvas element of the given size. @@ -30,7 +29,7 @@ */ this.context = this.canvas.getContext('2d'); - this.resolution = resolution || RESOLUTION; + this.resolution = resolution || settings.RESOLUTION; this.resize(width, height); } 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 ce0e1c4..788ed6c 100644 --- a/src/core/renderers/webgl/WebGLRenderer.js +++ b/src/core/renderers/webgl/WebGLRenderer.js @@ -30,8 +30,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 @@ -49,10 +49,19 @@ * enable this if you need to call toDataUrl on the webgl context. * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when * rendering, stopping pixel interpolation. + * @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; + + if (this.legacy) + { + glCore.VertexArrayObject.FORCE_NATIVE = true; + } /** * The type of this renderer as a standardised const @@ -229,7 +238,7 @@ this.emit('context', gl); // setup the width/height properties and gl viewport - this.resize(this.width, this.height); + this.resize(this.screen.width, this.screen.height); } /** @@ -323,16 +332,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) { @@ -376,6 +385,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 @@ -440,9 +469,10 @@ * Changes the current shader to the one given in parameter * * @param {PIXI.Shader} shader - the new shader + * @param {boolean} [autoProject=true] - Whether automatically set the projection matrix * @return {PIXI.WebGLRenderer} Returns itself. */ - bindShader(shader) + bindShader(shader, autoProject) { // TODO cache if (this._activeShader !== shader) @@ -450,8 +480,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 bbab3b3..944f41d 100644 --- a/src/core/renderers/webgl/filters/Filter.js +++ b/src/core/renderers/webgl/filters/Filter.js @@ -1,6 +1,7 @@ import extractUniformsFromSrc from './extractUniformsFromSrc'; import { uid } from '../../../utils'; import { BLEND_MODES } from '../../../const'; +import settings from '../../../settings'; const SOURCE_KEY_MAP = {}; @@ -10,7 +11,7 @@ * @memberof PIXI * @extends PIXI.Shader */ -class Filter +export default class Filter { /** * @param {string} [vertexSrc] - The source of the vertex shader. @@ -35,8 +36,6 @@ this.blendMode = BLEND_MODES.NORMAL; - // pull out the vertex and shader uniforms if they are not specified.. - // currently this does not extract structs only default types this.uniformData = uniforms || extractUniformsFromSrc(this.vertexSrc, this.fragmentSrc, 'projectionMatrix|uSampler'); /** @@ -80,7 +79,7 @@ * * @member {number} */ - this.resolution = 1; + this.resolution = settings.RESOLUTION; /** * If enabled is true the filter is applied, if false it will not. @@ -97,8 +96,11 @@ * @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 { // --- // // this.uniforms.filterMatrix = filterManager.calculateSpriteMatrix(tempMatrix, window.panda ); @@ -170,5 +172,3 @@ ].join('\n'); } } - -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 dc7072a..6c7422d 100644 --- a/src/core/renderers/webgl/managers/FilterManager.js +++ b/src/core/renderers/webgl/managers/FilterManager.js @@ -154,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 @@ -176,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; @@ -184,7 +184,7 @@ flop = t; } - filters[i].apply(this, flip, lastState.renderTarget, true); + filters[i].apply(this, flip, lastState.renderTarget, false, currentState); this.freePotRenderTarget(flip); this.freePotRenderTarget(flop); @@ -291,9 +291,13 @@ let textureCount = 1; let currentState; - if (shader.uniforms.data.filterArea) + // 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; @@ -306,9 +310,9 @@ // use this to clamp displaced texture coords so they belong to filterArea // see displacementFilter fragment shader for an example - if (shader.uniforms.data.filterClamp) + if (shader.uniforms.filterClamp) { - currentState = this.filterData.stack[this.filterData.index]; + currentState = currentState || this.filterData.stack[this.filterData.index]; const filterClamp = shader.uniforms.filterClamp; @@ -485,7 +489,7 @@ */ destroy() { - this.shaderCache = []; + this.shaderCache = {}; this.emptyPool(); } diff --git a/src/core/renderers/webgl/utils/RenderTarget.js b/src/core/renderers/webgl/utils/RenderTarget.js index afe51ab..6d2467f 100644 --- a/src/core/renderers/webgl/utils/RenderTarget.js +++ b/src/core/renderers/webgl/utils/RenderTarget.js @@ -124,7 +124,7 @@ * @default PIXI.settings.SCALE_MODE * @see PIXI.SCALE_MODES */ - this.scaleMode = scaleMode || settings.SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; /** * Whether this object is the root element or not 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 36ea5bb..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; @@ -346,8 +346,8 @@ /** * Gets the local bounds of the sprite object. * - * @param {Rectangle} rect - The output rectangle. - * @return {Rectangle} The bounds. + * @param {PIXI.Rectangle} rect - The output rectangle. + * @return {PIXI.Rectangle} The bounds. */ getLocalBounds(rect) { @@ -440,7 +440,7 @@ * * @static * @param {number|string|PIXI.BaseTexture|HTMLCanvasElement|HTMLVideoElement} source Source to create texture from - * @return {PIXI.Texture} 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 e77b39a..90b08c0 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++) @@ -103,11 +102,18 @@ { const gl = this.renderer.gl; - // step 1: first check max textures the GPU can handle. - this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); + if (this.renderer.legacy) + { + this.MAX_TEXTURES = 1; + } + else + { + // step 1: first check max textures the GPU can handle. + this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); - // step 2: check the maximum number of if statements the shader can have too.. - this.MAX_TEXTURES = checkMaxIfStatmentsInShader(this.MAX_TEXTURES, gl); + // step 2: check the maximum number of if statements the shader can have too.. + this.MAX_TEXTURES = checkMaxIfStatmentsInShader(this.MAX_TEXTURES, gl); + } const shader = this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); @@ -130,8 +136,12 @@ .addIndex(this.indexBuffer) .addAttribute(this.vertexBuffers[i], shader.attributes.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) .addAttribute(this.vertexBuffers[i], shader.attributes.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) - .addAttribute(this.vertexBuffers[i], shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4) - .addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + .addAttribute(this.vertexBuffers[i], shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); + + if (shader.attributes.aTextureId) + { + this.vaos[i].addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + } /* eslint-enable max-len */ } @@ -378,8 +388,12 @@ .addIndex(this.indexBuffer) .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) - .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4) - .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); + + if (this.shader.attributes.aTextureId) + { + this.vaos[this.vertexCount].addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + } /* eslint-enable max-len */ } diff --git a/src/core/text/Text.js b/src/core/text/Text.js index 013625a..cec8c32 100644 --- a/src/core/text/Text.js +++ b/src/core/text/Text.js @@ -6,6 +6,7 @@ import { TEXT_GRADIENT } from '../const'; import settings from '../settings'; import TextStyle from './TextStyle'; +import trimCanvas from '../utils/trimCanvas'; const defaultDestroyOptions = { texture: true, @@ -57,7 +58,7 @@ /** * The canvas 2d context that everything is drawn with - * @member {HTMLCanvasElement} + * @member {CanvasRenderingContext2D} */ this.context = this.canvas.getContext('2d'); @@ -189,10 +190,12 @@ if (style.dropShadow) { + this.context.shadowBlur = style.dropShadowBlur; + this.context.globalAlpha = style.dropShadowAlpha; + if (style.dropShadowBlur > 0) { this.context.shadowColor = style.dropShadowColor; - this.context.shadowBlur = style.dropShadowBlur; } else { @@ -237,6 +240,10 @@ } } + // reset the shadow blur and alpha that was set by the drop shadow, for the regular text + this.context.shadowBlur = 0; + this.context.globalAlpha = 1; + // set canvas text styles this.context.fillStyle = this._generateFillStyle(style, lines); @@ -326,6 +333,15 @@ */ updateTexture() { + if (this._style.trim) + { + const trimmed = trimCanvas(this.canvas); + + this.canvas.width = trimmed.width; + this.canvas.height = trimmed.height; + this.context.putImageData(trimmed.data, 0, 0); + } + const texture = this._texture; const style = this._style; @@ -475,6 +491,19 @@ } /** + * Gets the local bounds of the text object. + * + * @param {Rectangle} rect - The output rectangle. + * @return {Rectangle} The bounds. + */ + getLocalBounds(rect) + { + this.updateText(true); + + return super.getLocalBounds.call(this, rect); + } + + /** * calculates the bounds of the Text as a rectangle. The bounds calculation takes the worldTransform into account. */ _calculateBounds() @@ -525,6 +554,29 @@ const width = this.canvas.width / this.resolution; const height = this.canvas.height / this.resolution; + // make a copy of the style settings, so we can manipulate them later + const fill = style.fill.slice(); + const fillGradientStops = style.fillGradientStops.slice(); + + // wanting to evenly distribute the fills. So an array of 4 colours should give fills of 0.25, 0.5 and 0.75 + if (!fillGradientStops.length) + { + const lengthPlus1 = fill.length + 1; + + for (let i = 1; i < lengthPlus1; ++i) + { + fillGradientStops.push(i / lengthPlus1); + } + } + + // stop the bleeding of the last gradient on the line above to the top gradient of the this line + // by hard defining the first gradient colour at point 0, and last gradient colour at point 1 + fill.unshift(style.fill[0]); + fillGradientStops.unshift(0); + + fill.push(style.fill[style.fill.length - 1]); + fillGradientStops.push(1); + if (style.fillGradientType === TEXT_GRADIENT.LINEAR_VERTICAL) { // start the gradient at the top center of the canvas, and end at the bottom middle of the canvas @@ -532,15 +584,22 @@ // we need to repeat the gradient so that each individual line of text has the same vertical gradient effect // ['#FF0000', '#00FF00', '#0000FF'] over 2 lines would create stops at 0.125, 0.25, 0.375, 0.625, 0.75, 0.875 - totalIterations = (style.fill.length + 1) * lines.length; + totalIterations = (fill.length + 1) * lines.length; currentIteration = 0; for (let i = 0; i < lines.length; i++) { currentIteration += 1; - for (let j = 0; j < style.fill.length; j++) + for (let j = 0; j < fill.length; j++) { - stop = (currentIteration / totalIterations); - gradient.addColorStop(stop, style.fill[j]); + if (fillGradientStops[j]) + { + stop = (fillGradientStops[j] / lines.length) + (i / lines.length); + } + else + { + stop = currentIteration / totalIterations; + } + gradient.addColorStop(stop, fill[j]); currentIteration++; } } @@ -552,13 +611,20 @@ // can just evenly space out the gradients in this case, as multiple lines makes no difference // to an even left to right gradient - totalIterations = style.fill.length + 1; + totalIterations = fill.length + 1; currentIteration = 1; - for (let i = 0; i < style.fill.length; i++) + for (let i = 0; i < fill.length; i++) { - stop = currentIteration / totalIterations; - gradient.addColorStop(stop, style.fill[i]); + if (fillGradientStops[i]) + { + stop = fillGradientStops[i]; + } + else + { + stop = currentIteration / totalIterations; + } + gradient.addColorStop(stop, fill[i]); currentIteration++; } } @@ -569,7 +635,7 @@ /** * Destroys this text object. * Note* Unlike a Sprite, a Text object will automatically destroy its baseTexture and texture as - * the majorety of the time the texture will not be shared with any other Sprites. + * the majority of the time the texture will not be shared with any other Sprites. * * @param {object|boolean} [options] - Options parameter. A boolean will act as if all options * have been set to that value @@ -680,7 +746,7 @@ set text(text) // eslint-disable-line require-jsdoc { - text = String(text || ' '); + text = String(text === '' || text === null || text === undefined ? ' ' : text); if (this._text === text) { diff --git a/src/core/text/TextStyle.js b/src/core/text/TextStyle.js index 3b25528..890950b 100644 --- a/src/core/text/TextStyle.js +++ b/src/core/text/TextStyle.js @@ -8,12 +8,14 @@ align: 'left', breakWords: false, dropShadow: false, + dropShadowAlpha: 1, dropShadowAngle: Math.PI / 6, dropShadowBlur: 0, dropShadowColor: '#000000', dropShadowDistance: 5, fill: 'black', fillGradientType: TEXT_GRADIENT.LINEAR_VERTICAL, + fillGradientStops: [], fontFamily: 'Arial', fontSize: 26, fontStyle: 'normal', @@ -27,6 +29,7 @@ stroke: 'black', strokeThickness: 0, textBaseline: 'alphabetic', + trim: false, wordWrap: false, wordWrapWidth: 100, }; @@ -47,6 +50,7 @@ * @param {boolean} [style.breakWords=false] - Indicates if lines can be wrapped within words, it * needs wordWrap to be set to true * @param {boolean} [style.dropShadow=false] - Set a drop shadow for the text + * @param {number} [style.dropShadowAlpha=1] - Set alpha for the drop shadow * @param {number} [style.dropShadowAngle=Math.PI/6] - Set a angle of the drop shadow * @param {number} [style.dropShadowBlur=0] - Set a shadow blur radius * @param {string} [style.dropShadowColor='#000000'] - A fill style to be used on the dropshadow e.g 'red', '#00FF00' @@ -55,8 +59,10 @@ * fillstyle that will be used on the text e.g 'red', '#00FF00'. Can be an array to create a gradient * eg ['#000000','#FFFFFF'] * {@link https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/fillStyle|MDN} - * @param {number} [style.fillGradientType=PIXI.TEXT_GRADIENT.LINEAR_VERTICAL] - If fills styles are - * supplied, this can change the type/direction of the gradient. See {@link PIXI.TEXT_GRADIENT} for possible values + * @param {number} [style.fillGradientType=PIXI.TEXT_GRADIENT.LINEAR_VERTICAL] - If fill is an array of colours + * to create a gradient, this can change the type/direction of the gradient. See {@link PIXI.TEXT_GRADIENT} + * @param {number[]} [style.fillGradientStops] - If fill is an array of colours to create a gradient, this array can set + * the stop points (numbers between 0 and 1) for the color, overriding the default behaviour of evenly spacing them. * @param {string|string[]} [style.fontFamily='Arial'] - The font family * @param {number|string} [style.fontSize=26] - The font size (as a number it converts to px, but as a string, * equivalents are '26px','20pt','160%' or '1.6em') @@ -76,6 +82,7 @@ * e.g 'blue', '#FCFF00' * @param {number} [style.strokeThickness=0] - A number that represents the thickness of the stroke. * Default is 0 (no stroke) + * @param {boolean} [style.trim=false] - Trim transparent borders * @param {string} [style.textBaseline='alphabetic'] - The baseline of the text that is rendered. * @param {boolean} [style.wordWrap=false] - Indicates if word wrap should be used * @param {number} [style.wordWrapWidth=100] - The width at which text will wrap, it needs wordWrap to be set to true @@ -152,6 +159,19 @@ } } + get dropShadowAlpha() + { + return this._dropShadowAlpha; + } + set dropShadowAlpha(dropShadowAlpha) + { + if (this._dropShadowAlpha !== dropShadowAlpha) + { + this._dropShadowAlpha = dropShadowAlpha; + this.styleID++; + } + } + get dropShadowAngle() { return this._dropShadowAngle; @@ -232,6 +252,19 @@ } } + get fillGradientStops() + { + return this._fillGradientStops; + } + set fillGradientStops(fillGradientStops) + { + if (!areArraysEqual(this._fillGradientStops,fillGradientStops)) + { + this._fillGradientStops = fillGradientStops; + this.styleID++; + } + } + get fontFamily() { return this._fontFamily; @@ -402,6 +435,19 @@ } } + get trim() + { + return this._trim; + } + set trim(trim) + { + if (this._trim !== trim) + { + this._trim = trim; + this.styleID++; + } + } + get wordWrap() { return this._wordWrap; @@ -475,3 +521,34 @@ return color; } } + +/** + * Utility function to convert hexadecimal colors to strings, and simply return the color if it's a string. + * This version can also convert array of colors + * + * @param {Array} array1 First array to compare + * @param {Array} array2 Second array to compare + * @return {boolean} Do the arrays contain the same values in the same order + */ +function areArraysEqual(array1, array2) +{ + if (!Array.isArray(array1) || !Array.isArray(array2)) + { + return false; + } + + if (array1.length !== array2.length) + { + return false; + } + + for (let i = 0; i < array1.length; ++i) + { + if (array1[i] !== array2[i]) + { + return false; + } + } + + return true; +} diff --git a/src/core/textures/BaseRenderTexture.js b/src/core/textures/BaseRenderTexture.js index 5d50146..aa727f0 100644 --- a/src/core/textures/BaseRenderTexture.js +++ b/src/core/textures/BaseRenderTexture.js @@ -1,8 +1,6 @@ import BaseTexture from './BaseTexture'; import settings from '../settings'; -const { RESOLUTION, SCALE_MODE } = settings; - /** * A BaseRenderTexture is a special texture that allows any Pixi display object to be rendered to it. * @@ -54,7 +52,7 @@ { super(null, scaleMode); - this.resolution = resolution || RESOLUTION; + this.resolution = resolution || settings.RESOLUTION; this.width = width; this.height = height; @@ -62,7 +60,7 @@ this.realWidth = this.width * this.resolution; this.realHeight = this.height * this.resolution; - this.scaleMode = scaleMode || SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; this.hasLoaded = true; /** diff --git a/src/core/textures/BaseTexture.js b/src/core/textures/BaseTexture.js index 676dca3..aeccf39 100644 --- a/src/core/textures/BaseTexture.js +++ b/src/core/textures/BaseTexture.js @@ -77,7 +77,7 @@ * @default PIXI.settings.SCALE_MODE * @see PIXI.SCALE_MODES */ - this.scaleMode = scaleMode || settings.SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; /** * Set to true once the base texture has successfully loaded. @@ -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/RenderTexture.js b/src/core/textures/RenderTexture.js index 4c5034f..b3c6297 100644 --- a/src/core/textures/RenderTexture.js +++ b/src/core/textures/RenderTexture.js @@ -54,8 +54,8 @@ /* eslint-disable prefer-rest-params, no-console */ const width = arguments[1]; const height = arguments[2]; - const scaleMode = arguments[3] || 0; - const resolution = arguments[4] || 1; + const scaleMode = arguments[3]; + const resolution = arguments[4]; // we have an old render texture.. console.warn(`Please use RenderTexture.create(${width}, ${height}) instead of the ctor directly.`); 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 ca31456..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 @@ -459,7 +497,9 @@ if (frame.x + frame.width > this.baseTexture.width || frame.y + frame.height > this.baseTexture.height) { - throw new Error(`Texture Error: frame does not fit inside the base Texture dimensions ${this}`); + throw new Error('Texture Error: frame does not fit inside the base Texture dimensions: ' + + `X: ${frame.x} + ${frame.width} > ${this.baseTexture.width} ` + + `Y: ${frame.y} + ${frame.height} > ${this.baseTexture.height}`); } // this.valid = frame && frame.width && frame.height && this.baseTexture.source && this.baseTexture.hasLoaded; @@ -520,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. @@ -528,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 2a7497b..99e585a 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)); } /** @@ -360,3 +362,43 @@ * @private */ export const BaseTextureCache = {}; + +/** + * 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/core/utils/trimCanvas.js b/src/core/utils/trimCanvas.js new file mode 100644 index 0000000..5af6de0 --- /dev/null +++ b/src/core/utils/trimCanvas.js @@ -0,0 +1,83 @@ +/** + * Trim transparent borders from a canvas + * + * @memberof PIXI + * @function trimCanvas + * @private + * @param {HTMLCanvasElement} canvas - the canvas to trim + * @returns {object} Trim data + */ +export default function trimCanvas(canvas) +{ + // https://gist.github.com/remy/784508 + + let width = canvas.width; + let height = canvas.height; + + const context = canvas.getContext('2d'); + const imageData = context.getImageData(0, 0, width, height); + const pixels = imageData.data; + const len = pixels.length; + + const bound = { + top: null, + left: null, + right: null, + bottom: null, + }; + let i; + let x; + let y; + + for (i = 0; i < len; i += 4) + { + if (pixels[i + 3] !== 0) + { + x = (i / 4) % width; + y = ~~((i / 4) / width); + + if (bound.top === null) + { + bound.top = y; + } + + if (bound.left === null) + { + bound.left = x; + } + else if (x < bound.left) + { + bound.left = x; + } + + if (bound.right === null) + { + bound.right = x + 1; + } + else if (bound.right < x) + { + bound.right = x + 1; + } + + if (bound.bottom === null) + { + bound.bottom = y; + } + else if (bound.bottom < y) + { + bound.bottom = y; + } + } + } + + width = bound.right - bound.left; + height = bound.bottom - bound.top + 1; + + const data = context.getImageData(bound.left, bound.top, width, height); + + return { + height, + width, + data, + }; +} diff --git a/src/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/extras/TilingSprite.js b/src/extras/TilingSprite.js index f90d248..fe82451 100644 --- a/src/extras/TilingSprite.js +++ b/src/extras/TilingSprite.js @@ -220,20 +220,33 @@ transform.tx * resolution, transform.ty * resolution); - // TODO - this should be rolled into the setTransform above.. - context.scale(this.tileScale.x / baseTextureResolution, this.tileScale.y / baseTextureResolution); - - context.translate(modX + (this.anchor.x * -this._width), - modY + (this.anchor.y * -this._height)); - renderer.setBlendMode(this.blendMode); // fill the pattern! context.fillStyle = this._canvasPattern; - context.fillRect(-modX, - -modY, - this._width / this.tileScale.x * baseTextureResolution, - this._height / this.tileScale.y * baseTextureResolution); + + // TODO - this should be rolled into the setTransform above.. + context.scale(this.tileScale.x / baseTextureResolution, this.tileScale.y / baseTextureResolution); + + const anchorX = this.anchor.x * -this._width; + const anchorY = this.anchor.y * -this._height; + + if (this.uvRespectAnchor) + { + context.translate(modX, modY); + + context.fillRect(-modX + anchorX, -modY + anchorY, + this._width / this.tileScale.x * baseTextureResolution, + this._height / this.tileScale.y * baseTextureResolution); + } + else + { + context.translate(modX + anchorX, modY + anchorY); + + context.fillRect(-modX, -modY, + this._width / this.tileScale.x * baseTextureResolution, + this._height / this.tileScale.y * baseTextureResolution); + } } /** diff --git a/src/extras/cacheAsBitmap.js b/src/extras/cacheAsBitmap.js index 18043a7..5dd5640 100644 --- a/src/extras/cacheAsBitmap.js +++ b/src/extras/cacheAsBitmap.js @@ -41,6 +41,9 @@ * provide a performance benefit for complex static displayObjects. * To remove simply set this property to 'false' * + * IMPORTANT GOTCHA - make sure that all your textures are preloaded BEFORE setting this property to true + * as it will take a snapshot of what is currently there. If the textures have not loaded then they will not appear. + * * @member {boolean} * @memberof PIXI.DisplayObject# */ diff --git a/src/filters/blur/BlurFilter.js b/src/filters/blur/BlurFilter.js index 340fc58..1575db9 100644 --- a/src/filters/blur/BlurFilter.js +++ b/src/filters/blur/BlurFilter.js @@ -24,10 +24,9 @@ this.blurXFilter = new BlurXFilter(strength, quality, resolution, kernelSize); this.blurYFilter = new BlurYFilter(strength, quality, resolution, kernelSize); - this.resolution = 1; this.padding = 0; - this.resolution = resolution || 1; + this.resolution = resolution || core.settings.RESOLUTION; this.quality = quality || 4; this.blur = strength || 8; } diff --git a/src/filters/blur/BlurXFilter.js b/src/filters/blur/BlurXFilter.js index def99c3..0f48889 100644 --- a/src/filters/blur/BlurXFilter.js +++ b/src/filters/blur/BlurXFilter.js @@ -31,7 +31,7 @@ fragSrc ); - this.resolution = resolution || 1; + this.resolution = resolution || core.settings.RESOLUTION; this._quality = 0; diff --git a/src/filters/blur/BlurYFilter.js b/src/filters/blur/BlurYFilter.js index 638e0f0..6d489c7 100644 --- a/src/filters/blur/BlurYFilter.js +++ b/src/filters/blur/BlurYFilter.js @@ -31,7 +31,7 @@ fragSrc ); - this.resolution = resolution || 1; + this.resolution = resolution || core.settings.RESOLUTION; this._quality = 0; diff --git a/src/filters/colormatrix/ColorMatrixFilter.js b/src/filters/colormatrix/ColorMatrixFilter.js index 8509505..679f8ec 100644 --- a/src/filters/colormatrix/ColorMatrixFilter.js +++ b/src/filters/colormatrix/ColorMatrixFilter.js @@ -75,28 +75,28 @@ out[1] = (a[0] * b[1]) + (a[1] * b[6]) + (a[2] * b[11]) + (a[3] * b[16]); out[2] = (a[0] * b[2]) + (a[1] * b[7]) + (a[2] * b[12]) + (a[3] * b[17]); out[3] = (a[0] * b[3]) + (a[1] * b[8]) + (a[2] * b[13]) + (a[3] * b[18]); - out[4] = (a[0] * b[4]) + (a[1] * b[9]) + (a[2] * b[14]) + (a[3] * b[19]); + out[4] = (a[0] * b[4]) + (a[1] * b[9]) + (a[2] * b[14]) + (a[3] * b[19]) + a[4]; // Green Channel out[5] = (a[5] * b[0]) + (a[6] * b[5]) + (a[7] * b[10]) + (a[8] * b[15]); out[6] = (a[5] * b[1]) + (a[6] * b[6]) + (a[7] * b[11]) + (a[8] * b[16]); out[7] = (a[5] * b[2]) + (a[6] * b[7]) + (a[7] * b[12]) + (a[8] * b[17]); out[8] = (a[5] * b[3]) + (a[6] * b[8]) + (a[7] * b[13]) + (a[8] * b[18]); - out[9] = (a[5] * b[4]) + (a[6] * b[9]) + (a[7] * b[14]) + (a[8] * b[19]); + out[9] = (a[5] * b[4]) + (a[6] * b[9]) + (a[7] * b[14]) + (a[8] * b[19]) + a[9]; // Blue Channel out[10] = (a[10] * b[0]) + (a[11] * b[5]) + (a[12] * b[10]) + (a[13] * b[15]); out[11] = (a[10] * b[1]) + (a[11] * b[6]) + (a[12] * b[11]) + (a[13] * b[16]); out[12] = (a[10] * b[2]) + (a[11] * b[7]) + (a[12] * b[12]) + (a[13] * b[17]); out[13] = (a[10] * b[3]) + (a[11] * b[8]) + (a[12] * b[13]) + (a[13] * b[18]); - out[14] = (a[10] * b[4]) + (a[11] * b[9]) + (a[12] * b[14]) + (a[13] * b[19]); + out[14] = (a[10] * b[4]) + (a[11] * b[9]) + (a[12] * b[14]) + (a[13] * b[19]) + a[14]; // Alpha Channel out[15] = (a[15] * b[0]) + (a[16] * b[5]) + (a[17] * b[10]) + (a[18] * b[15]); out[16] = (a[15] * b[1]) + (a[16] * b[6]) + (a[17] * b[11]) + (a[18] * b[16]); out[17] = (a[15] * b[2]) + (a[16] * b[7]) + (a[17] * b[12]) + (a[18] * b[17]); out[18] = (a[15] * b[3]) + (a[16] * b[8]) + (a[17] * b[13]) + (a[18] * b[18]); - out[19] = (a[15] * b[4]) + (a[16] * b[9]) + (a[17] * b[14]) + (a[18] * b[19]); + out[19] = (a[15] * b[4]) + (a[16] * b[9]) + (a[17] * b[14]) + (a[18] * b[19]) + a[19]; return out; } diff --git a/src/filters/colormatrix/colorMatrix.frag b/src/filters/colormatrix/colorMatrix.frag index 3aaf2b3..c73c0e9 100644 --- a/src/filters/colormatrix/colorMatrix.frag +++ b/src/filters/colormatrix/colorMatrix.frag @@ -4,32 +4,38 @@ void main(void) { - vec4 c = texture2D(uSampler, vTextureCoord); + // Un-premultiply alpha before applying the color matrix. See issue #3539. + if (c.a > 0.0) { + c.rgb /= c.a; + } + vec4 result; + result.r = (m[0] * c.r); + result.r += (m[1] * c.g); + result.r += (m[2] * c.b); + result.r += (m[3] * c.a); + result.r += m[4]; - gl_FragColor.r = (m[0] * c.r); - gl_FragColor.r += (m[1] * c.g); - gl_FragColor.r += (m[2] * c.b); - gl_FragColor.r += (m[3] * c.a); - gl_FragColor.r += m[4] * c.a; + result.g = (m[5] * c.r); + result.g += (m[6] * c.g); + result.g += (m[7] * c.b); + result.g += (m[8] * c.a); + result.g += m[9]; - gl_FragColor.g = (m[5] * c.r); - gl_FragColor.g += (m[6] * c.g); - gl_FragColor.g += (m[7] * c.b); - gl_FragColor.g += (m[8] * c.a); - gl_FragColor.g += m[9] * c.a; + result.b = (m[10] * c.r); + result.b += (m[11] * c.g); + result.b += (m[12] * c.b); + result.b += (m[13] * c.a); + result.b += m[14]; - gl_FragColor.b = (m[10] * c.r); - gl_FragColor.b += (m[11] * c.g); - gl_FragColor.b += (m[12] * c.b); - gl_FragColor.b += (m[13] * c.a); - gl_FragColor.b += m[14] * c.a; + result.a = (m[15] * c.r); + result.a += (m[16] * c.g); + result.a += (m[17] * c.b); + result.a += (m[18] * c.a); + result.a += m[19]; - gl_FragColor.a = (m[15] * c.r); - gl_FragColor.a += (m[16] * c.g); - gl_FragColor.a += (m[17] * c.b); - gl_FragColor.a += (m[18] * c.a); - gl_FragColor.a += m[19] * c.a; + // Premultiply alpha again. + result.rgb *= result.a; -// gl_FragColor = vec4(m[0]); + gl_FragColor = result; } diff --git a/src/filters/displacement/DisplacementFilter.js b/src/filters/displacement/DisplacementFilter.js index 112de0b..d5355c3 100644 --- a/src/filters/displacement/DisplacementFilter.js +++ b/src/filters/displacement/DisplacementFilter.js @@ -36,7 +36,7 @@ this.maskMatrix = maskMatrix; this.uniforms.mapSampler = sprite.texture; - this.uniforms.filterMatrix = maskMatrix.toArray(true); + this.uniforms.filterMatrix = maskMatrix; this.uniforms.scale = { x: 1, y: 1 }; if (scale === null || scale === undefined) diff --git a/src/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/InteractionData.js b/src/interaction/InteractionData.js index 02d53a3..818da35 100644 --- a/src/interaction/InteractionData.js +++ b/src/interaction/InteractionData.js @@ -21,9 +21,9 @@ this.global = new core.Point(); /** - * The target Sprite that was interacted with + * The target DisplayObject that was interacted with * - * @member {PIXI.Sprite} + * @member {PIXI.DisplayObject} */ this.target = null; @@ -33,6 +33,13 @@ * @member {Event} */ this.originalEvent = null; + + /** + * Unique identifier for this interaction + * + * @member {number} + */ + this.identifier = null; } /** diff --git a/src/interaction/InteractionEvent.js b/src/interaction/InteractionEvent.js index 75b2966..dc554f9 100644 --- a/src/interaction/InteractionEvent.js +++ b/src/interaction/InteractionEvent.js @@ -58,7 +58,7 @@ } /** - * Prevents event from reaching any objects other than the current object. + * Resets the event. * * @private */ diff --git a/src/interaction/InteractionManager.js b/src/interaction/InteractionManager.js index 7d51134..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/src/prepare/BasePrepare.js b/src/prepare/BasePrepare.js index a2005f5..09eb5e5 100644 --- a/src/prepare/BasePrepare.js +++ b/src/prepare/BasePrepare.js @@ -20,7 +20,7 @@ * * @abstract * @class - * @memberof PIXI + * @memberof PIXI.prepare */ export default class BasePrepare { @@ -107,9 +107,9 @@ /** * Upload all the textures and graphics to the GPU. * - * @param {Function|PIXI.DisplayObject|PIXI.Container} item - Either - * the container or display object to search for items to upload or - * the callback function, if items have been added using `prepare.add`. + * @param {Function|PIXI.DisplayObject|PIXI.Container|PIXI.BaseTexture|PIXI.Texture|PIXI.Graphics|PIXI.Text} item - + * Either the container or display object to search for items to upload, the items to upload themselves, + * or the callback function, if items have been added using `prepare.add`. * @param {Function} [done] - Optional callback when all queued uploads have completed */ upload(item, done) @@ -236,7 +236,8 @@ /** * Manually add an item to the uploading queue. * - * @param {PIXI.DisplayObject|PIXI.Container|*} item - Object to add to the queue + * @param {PIXI.DisplayObject|PIXI.Container|PIXI.BaseTexture|PIXI.Texture|PIXI.Graphics|PIXI.Text|*} item - Object to + * add to the queue * @return {PIXI.CanvasPrepare} Instance of plugin for chaining. */ add(item) diff --git a/src/prepare/canvas/CanvasPrepare.js b/src/prepare/canvas/CanvasPrepare.js index 4b99d22..0224bb5 100644 --- a/src/prepare/canvas/CanvasPrepare.js +++ b/src/prepare/canvas/CanvasPrepare.js @@ -12,7 +12,8 @@ * An instance of this class is automatically created by default, and can be found at renderer.plugins.prepare * * @class - * @memberof PIXI + * @extends PIXI.prepare.BasePrepare + * @memberof PIXI.prepare */ export default class CanvasPrepare extends BasePrepare { diff --git a/src/prepare/webgl/WebGLPrepare.js b/src/prepare/webgl/WebGLPrepare.js index 1ef7467..eb7023f 100644 --- a/src/prepare/webgl/WebGLPrepare.js +++ b/src/prepare/webgl/WebGLPrepare.js @@ -7,7 +7,8 @@ * An instance of this class is automatically created by default, and can be found at renderer.plugins.prepare * * @class - * @memberof PIXI + * @extends PIXI.prepare.BasePrepare + * @memberof PIXI.prepare */ export default class WebGLPrepare extends BasePrepare { diff --git a/test/core/Circle.js b/test/core/Circle.js index 183f98f..f621992 100644 --- a/test/core/Circle.js +++ b/test/core/Circle.js @@ -53,6 +53,10 @@ expect(circ1.contains(10, 16)).to.be.false; expect(circ1.contains(11, 15)).to.be.false; expect(circ1.contains(0, 0)).to.be.false; + + const circ2 = new PIXI.Circle(10, 10, 0); + + expect(circ2.contains(10, 10)).to.be.false; }); it('should return framing rectangle', function () diff --git a/test/core/Container.js b/test/core/Container.js index 8a10b40..cefde54 100644 --- a/test/core/Container.js +++ b/test/core/Container.js @@ -1,5 +1,39 @@ 'use strict'; +function testAddChild(fn) +{ + return function () + { + fn(function (container, obj) + { + container.addChild(obj); + }); + fn(function (container, obj) + { + container.addChildAt(obj); + }); + }; +} + +function testRemoveChild(fn) +{ + return function () + { + 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 +104,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 +279,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/Ellipse.js b/test/core/Ellipse.js index f2e8234..3f47442 100644 --- a/test/core/Ellipse.js +++ b/test/core/Ellipse.js @@ -56,6 +56,10 @@ expect(ellipse1.contains(10, 16)).to.be.false; expect(ellipse1.contains(11, 15)).to.be.false; expect(ellipse1.contains(0, 0)).to.be.false; + + const ellipse2 = new PIXI.Ellipse(10, 10, 0, 0); + + expect(ellipse2.contains(10, 10)).to.be.false; }); it('should return framing rectangle', function () diff --git a/test/core/Graphics.js b/test/core/Graphics.js index 2a64946..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 () @@ -127,6 +130,16 @@ expect(graphics.containsPoint(point)).to.be.false; }); + + it('should return false when no fill', function () + { + const point = new PIXI.Point(1, 1); + const graphics = new PIXI.Graphics(); + + graphics.drawRect(0, 0, 10, 10); + + expect(graphics.containsPoint(point)).to.be.false; + }); }); describe('arc', function () @@ -208,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/Matrix.js b/test/core/Matrix.js index 9d2694b..76bd57c 100644 --- a/test/core/Matrix.js +++ b/test/core/Matrix.js @@ -107,10 +107,60 @@ expect(m1.ty).to.equal(m2.ty); }); + it('should prepend matrix', function () + { + const m1 = new PIXI.Matrix(); + const m2 = new PIXI.Matrix(); + + m2.set(2, 3, 4, 5, 100, 200); + m1.prepend(m2); + + expect(m1.a).to.equal(m2.a); + expect(m1.b).to.equal(m2.b); + expect(m1.c).to.equal(m2.c); + expect(m1.d).to.equal(m2.d); + expect(m1.tx).to.equal(m2.tx); + expect(m1.ty).to.equal(m2.ty); + + const m3 = new PIXI.Matrix(); + const m4 = new PIXI.Matrix(); + + m3.prepend(m4); + + expect(m3.a).to.equal(m4.a); + expect(m3.b).to.equal(m4.b); + expect(m3.c).to.equal(m4.c); + expect(m3.d).to.equal(m4.d); + expect(m3.tx).to.equal(m4.tx); + expect(m3.ty).to.equal(m4.ty); + }); + it('should get IDENTITY and TEMP_MATRIX', function () { expect(PIXI.Matrix.IDENTITY instanceof PIXI.Matrix).to.be.true; expect(PIXI.Matrix.TEMP_MATRIX instanceof PIXI.Matrix).to.be.true; }); -}); + it('should reset matrix to default when identity() is called', function () + { + const matrix = new PIXI.Matrix(); + + matrix.set(2, 3, 4, 5, 100, 200); + + expect(matrix.a).to.equal(2); + expect(matrix.b).to.equal(3); + expect(matrix.c).to.equal(4); + expect(matrix.d).to.equal(5); + expect(matrix.tx).to.equal(100); + expect(matrix.ty).to.equal(200); + + matrix.identity(); + + expect(matrix.a).to.equal(1); + expect(matrix.b).to.equal(0); + expect(matrix.c).to.equal(0); + expect(matrix.d).to.equal(1); + expect(matrix.tx).to.equal(0); + expect(matrix.ty).to.equal(0); + }); +}); 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/Text.js b/test/core/Text.js index b8ce561..5799ad4 100644 --- a/test/core/Text.js +++ b/test/core/Text.js @@ -112,4 +112,42 @@ expect(text.width).to.equal(300); }); }); + + describe('text', function () + { + it('should convert numbers into strings', function () + { + const text = new PIXI.Text(2); + + expect(text.text).to.equal('2'); + }); + + it('should not change 0 to \'\'', function () + { + const text = new PIXI.Text(0); + + expect(text.text).to.equal('0'); + }); + + it('should prevent setting null', function () + { + const text = new PIXI.Text(null); + + expect(text.text).to.equal(' '); + }); + + it('should prevent setting undefined', function () + { + const text = new PIXI.Text(); + + expect(text.text).to.equal(' '); + }); + + it('should prevent setting \'\'', function () + { + const text = new PIXI.Text(''); + + expect(text.text).to.equal(' '); + }); + }); }); diff --git a/test/core/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 1271fef..71ade15 100644 --- a/package.json +++ b/package.json @@ -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/scripts/release.js b/scripts/release.js index c4c8e36..156620c 100644 --- a/scripts/release.js +++ b/scripts/release.js @@ -12,7 +12,8 @@ 'dist/**/*', 'lib/**/*', 'src/**/*', - 'scripts/**/*', + 'scripts/*', + 'scripts/renders/*', 'test/**/*', '*.json', '*.md', diff --git a/scripts/renders/client.js b/scripts/renders/client.js index 16c9565..6faee34 100755 --- a/scripts/renders/client.js +++ b/scripts/renders/client.js @@ -1,6 +1,6 @@ 'use strict'; -require('../../bin/pixi'); +require('../../dist/pixi'); PIXI.utils.skipHello(); const fs = require('fs'); 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 bc730db..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. @@ -33,8 +33,11 @@ * need to call toDataUrl on the webgl context * @param {number} [options.resolution=1] - The resolution / device pixel ratio of the renderer, retina would be 2 * @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 @@ -43,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. */ @@ -73,7 +99,7 @@ */ stop() { - this.ticker.stop(); + this._ticker.stop(); } /** @@ -81,7 +107,7 @@ */ start() { - this.ticker.start(); + this._ticker.start(); } /** @@ -95,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 index 81e076b..78b17e6 100644 --- a/src/core/Shader.js +++ b/src/core/Shader.js @@ -1,9 +1,7 @@ import { GLShader } from 'pixi-gl-core'; import settings from './settings'; -const { PRECISION } = settings; - -function checkPrecision(src) +function checkPrecision(src, def) { if (src instanceof Array) { @@ -11,14 +9,14 @@ { const copy = src.slice(0); - copy.unshift(`precision ${PRECISION} float;`); + copy.unshift(`precision ${def} float;`); return copy; } } else if (src.substring(0, 9) !== 'precision') { - return `precision ${PRECISION} float;\n${src}`; + return `precision ${def} float;\n${src}`; } return src; @@ -42,6 +40,7 @@ */ constructor(gl, vertexSrc, fragmentSrc) { - super(gl, checkPrecision(vertexSrc), checkPrecision(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 3425b41..f9235e4 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -181,7 +181,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 b3792ef..17fde8d 100644 --- a/src/core/display/Container.js +++ b/src/core/display/Container.js @@ -73,13 +73,14 @@ } child.parent = this; - - // ensure a transform will be recalculated.. - this.transform._parentID = -1; - this._boundsID++; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.push(child); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(this.children.length - 1); child.emit('added', this); @@ -108,9 +109,14 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.splice(index, 0, child); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(index); child.emit('added', this); @@ -174,6 +180,7 @@ removeItems(this.children, currentIndex, 1); // remove from old position this.children.splice(index, 0, child); // add at new position + this.onChildrenChange(index); } @@ -220,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! @@ -244,9 +252,14 @@ { const child = this.getChildAt(index); + // ensure child transform will be recalculated.. child.parent = null; + child.transform._parentID = -1; removeItems(this.children, index, 1); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(index); child.emit('removed', this); @@ -275,8 +288,14 @@ for (let i = 0; i < removed.length; ++i) { removed[i].parent = null; + if (removed[i].transform) + { + removed[i].transform._parentID = -1; + } } + this._boundsID++; + this.onChildrenChange(beginIndex); for (let i = 0; i < removed.length; ++i) diff --git a/src/core/display/DisplayObject.js b/src/core/display/DisplayObject.js index 3affe36..4e6c77d 100644 --- a/src/core/display/DisplayObject.js +++ b/src/core/display/DisplayObject.js @@ -109,7 +109,7 @@ /** * The original, cached mask of the object * - * @member {PIXI.Rectangle} + * @member {PIXI.Graphics|PIXI.Sprite} * @private */ this._mask = null; diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index 95f2af4..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 ); @@ -1065,10 +1061,15 @@ canvasRenderer = new CanvasRenderer(); } - tempMatrix.tx = -bounds.x; - tempMatrix.ty = -bounds.y; + this.transform.updateLocalTransform(); + this.transform.localTransform.copy(tempMatrix); - canvasRenderer.render(this, canvasBuffer, false, tempMatrix); + tempMatrix.invert(); + + tempMatrix.tx -= bounds.x; + tempMatrix.ty -= bounds.y; + + canvasRenderer.render(this, canvasBuffer, true, tempMatrix); const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode); 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 e67680f..867dde2 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/buildCircle.js b/src/core/graphics/webgl/utils/buildCircle.js index 0400d13..abf5d02 100644 --- a/src/core/graphics/webgl/utils/buildCircle.js +++ b/src/core/graphics/webgl/utils/buildCircle.js @@ -33,6 +33,11 @@ height = circleData.height; } + if (width === 0 || height === 0) + { + return; + } + const totalSegs = Math.floor(30 * Math.sqrt(circleData.radius)) || Math.floor(15 * Math.sqrt(circleData.width + circleData.height)); diff --git a/src/core/graphics/webgl/utils/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 90155f9..f03fe15 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -30,6 +30,7 @@ export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; export { default as CanvasGraphicsRenderer } from './graphics/canvas/CanvasGraphicsRenderer'; +export { default as Spritesheet } from './textures/Spritesheet'; export { default as Texture } from './textures/Texture'; export { default as BaseTexture } from './textures/BaseTexture'; export { default as RenderTexture } from './textures/RenderTexture'; diff --git a/src/core/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 ccce680..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); } /** @@ -162,6 +162,9 @@ if (transform) { transform.copy(tempWt); + + // lets not forget to flag the parent transform as dirty... + this._tempDisplayObjectParent.transform._worldID = -1; } else { @@ -169,6 +172,7 @@ } displayObject.parent = this._tempDisplayObjectParent; + displayObject.updateTransform(); displayObject.parent = cacheParent; // displayObject.hitArea = //TODO add a temp hit area @@ -279,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/canvas/utils/CanvasRenderTarget.js b/src/core/renderers/canvas/utils/CanvasRenderTarget.js index 3de0aef..b409891 100644 --- a/src/core/renderers/canvas/utils/CanvasRenderTarget.js +++ b/src/core/renderers/canvas/utils/CanvasRenderTarget.js @@ -1,5 +1,4 @@ import settings from '../../../settings'; -const { RESOLUTION } = settings; /** * Creates a Canvas element of the given size. @@ -30,7 +29,7 @@ */ this.context = this.canvas.getContext('2d'); - this.resolution = resolution || RESOLUTION; + this.resolution = resolution || settings.RESOLUTION; this.resize(width, height); } 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 ce0e1c4..788ed6c 100644 --- a/src/core/renderers/webgl/WebGLRenderer.js +++ b/src/core/renderers/webgl/WebGLRenderer.js @@ -30,8 +30,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 @@ -49,10 +49,19 @@ * enable this if you need to call toDataUrl on the webgl context. * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when * rendering, stopping pixel interpolation. + * @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; + + if (this.legacy) + { + glCore.VertexArrayObject.FORCE_NATIVE = true; + } /** * The type of this renderer as a standardised const @@ -229,7 +238,7 @@ this.emit('context', gl); // setup the width/height properties and gl viewport - this.resize(this.width, this.height); + this.resize(this.screen.width, this.screen.height); } /** @@ -323,16 +332,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) { @@ -376,6 +385,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 @@ -440,9 +469,10 @@ * Changes the current shader to the one given in parameter * * @param {PIXI.Shader} shader - the new shader + * @param {boolean} [autoProject=true] - Whether automatically set the projection matrix * @return {PIXI.WebGLRenderer} Returns itself. */ - bindShader(shader) + bindShader(shader, autoProject) { // TODO cache if (this._activeShader !== shader) @@ -450,8 +480,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 bbab3b3..944f41d 100644 --- a/src/core/renderers/webgl/filters/Filter.js +++ b/src/core/renderers/webgl/filters/Filter.js @@ -1,6 +1,7 @@ import extractUniformsFromSrc from './extractUniformsFromSrc'; import { uid } from '../../../utils'; import { BLEND_MODES } from '../../../const'; +import settings from '../../../settings'; const SOURCE_KEY_MAP = {}; @@ -10,7 +11,7 @@ * @memberof PIXI * @extends PIXI.Shader */ -class Filter +export default class Filter { /** * @param {string} [vertexSrc] - The source of the vertex shader. @@ -35,8 +36,6 @@ this.blendMode = BLEND_MODES.NORMAL; - // pull out the vertex and shader uniforms if they are not specified.. - // currently this does not extract structs only default types this.uniformData = uniforms || extractUniformsFromSrc(this.vertexSrc, this.fragmentSrc, 'projectionMatrix|uSampler'); /** @@ -80,7 +79,7 @@ * * @member {number} */ - this.resolution = 1; + this.resolution = settings.RESOLUTION; /** * If enabled is true the filter is applied, if false it will not. @@ -97,8 +96,11 @@ * @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 { // --- // // this.uniforms.filterMatrix = filterManager.calculateSpriteMatrix(tempMatrix, window.panda ); @@ -170,5 +172,3 @@ ].join('\n'); } } - -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 dc7072a..6c7422d 100644 --- a/src/core/renderers/webgl/managers/FilterManager.js +++ b/src/core/renderers/webgl/managers/FilterManager.js @@ -154,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 @@ -176,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; @@ -184,7 +184,7 @@ flop = t; } - filters[i].apply(this, flip, lastState.renderTarget, true); + filters[i].apply(this, flip, lastState.renderTarget, false, currentState); this.freePotRenderTarget(flip); this.freePotRenderTarget(flop); @@ -291,9 +291,13 @@ let textureCount = 1; let currentState; - if (shader.uniforms.data.filterArea) + // 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; @@ -306,9 +310,9 @@ // use this to clamp displaced texture coords so they belong to filterArea // see displacementFilter fragment shader for an example - if (shader.uniforms.data.filterClamp) + if (shader.uniforms.filterClamp) { - currentState = this.filterData.stack[this.filterData.index]; + currentState = currentState || this.filterData.stack[this.filterData.index]; const filterClamp = shader.uniforms.filterClamp; @@ -485,7 +489,7 @@ */ destroy() { - this.shaderCache = []; + this.shaderCache = {}; this.emptyPool(); } diff --git a/src/core/renderers/webgl/utils/RenderTarget.js b/src/core/renderers/webgl/utils/RenderTarget.js index afe51ab..6d2467f 100644 --- a/src/core/renderers/webgl/utils/RenderTarget.js +++ b/src/core/renderers/webgl/utils/RenderTarget.js @@ -124,7 +124,7 @@ * @default PIXI.settings.SCALE_MODE * @see PIXI.SCALE_MODES */ - this.scaleMode = scaleMode || settings.SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; /** * Whether this object is the root element or not 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 36ea5bb..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; @@ -346,8 +346,8 @@ /** * Gets the local bounds of the sprite object. * - * @param {Rectangle} rect - The output rectangle. - * @return {Rectangle} The bounds. + * @param {PIXI.Rectangle} rect - The output rectangle. + * @return {PIXI.Rectangle} The bounds. */ getLocalBounds(rect) { @@ -440,7 +440,7 @@ * * @static * @param {number|string|PIXI.BaseTexture|HTMLCanvasElement|HTMLVideoElement} source Source to create texture from - * @return {PIXI.Texture} 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 e77b39a..90b08c0 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++) @@ -103,11 +102,18 @@ { const gl = this.renderer.gl; - // step 1: first check max textures the GPU can handle. - this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); + if (this.renderer.legacy) + { + this.MAX_TEXTURES = 1; + } + else + { + // step 1: first check max textures the GPU can handle. + this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); - // step 2: check the maximum number of if statements the shader can have too.. - this.MAX_TEXTURES = checkMaxIfStatmentsInShader(this.MAX_TEXTURES, gl); + // step 2: check the maximum number of if statements the shader can have too.. + this.MAX_TEXTURES = checkMaxIfStatmentsInShader(this.MAX_TEXTURES, gl); + } const shader = this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); @@ -130,8 +136,12 @@ .addIndex(this.indexBuffer) .addAttribute(this.vertexBuffers[i], shader.attributes.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) .addAttribute(this.vertexBuffers[i], shader.attributes.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) - .addAttribute(this.vertexBuffers[i], shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4) - .addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + .addAttribute(this.vertexBuffers[i], shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); + + if (shader.attributes.aTextureId) + { + this.vaos[i].addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + } /* eslint-enable max-len */ } @@ -378,8 +388,12 @@ .addIndex(this.indexBuffer) .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) - .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4) - .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); + + if (this.shader.attributes.aTextureId) + { + this.vaos[this.vertexCount].addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + } /* eslint-enable max-len */ } diff --git a/src/core/text/Text.js b/src/core/text/Text.js index 013625a..cec8c32 100644 --- a/src/core/text/Text.js +++ b/src/core/text/Text.js @@ -6,6 +6,7 @@ import { TEXT_GRADIENT } from '../const'; import settings from '../settings'; import TextStyle from './TextStyle'; +import trimCanvas from '../utils/trimCanvas'; const defaultDestroyOptions = { texture: true, @@ -57,7 +58,7 @@ /** * The canvas 2d context that everything is drawn with - * @member {HTMLCanvasElement} + * @member {CanvasRenderingContext2D} */ this.context = this.canvas.getContext('2d'); @@ -189,10 +190,12 @@ if (style.dropShadow) { + this.context.shadowBlur = style.dropShadowBlur; + this.context.globalAlpha = style.dropShadowAlpha; + if (style.dropShadowBlur > 0) { this.context.shadowColor = style.dropShadowColor; - this.context.shadowBlur = style.dropShadowBlur; } else { @@ -237,6 +240,10 @@ } } + // reset the shadow blur and alpha that was set by the drop shadow, for the regular text + this.context.shadowBlur = 0; + this.context.globalAlpha = 1; + // set canvas text styles this.context.fillStyle = this._generateFillStyle(style, lines); @@ -326,6 +333,15 @@ */ updateTexture() { + if (this._style.trim) + { + const trimmed = trimCanvas(this.canvas); + + this.canvas.width = trimmed.width; + this.canvas.height = trimmed.height; + this.context.putImageData(trimmed.data, 0, 0); + } + const texture = this._texture; const style = this._style; @@ -475,6 +491,19 @@ } /** + * Gets the local bounds of the text object. + * + * @param {Rectangle} rect - The output rectangle. + * @return {Rectangle} The bounds. + */ + getLocalBounds(rect) + { + this.updateText(true); + + return super.getLocalBounds.call(this, rect); + } + + /** * calculates the bounds of the Text as a rectangle. The bounds calculation takes the worldTransform into account. */ _calculateBounds() @@ -525,6 +554,29 @@ const width = this.canvas.width / this.resolution; const height = this.canvas.height / this.resolution; + // make a copy of the style settings, so we can manipulate them later + const fill = style.fill.slice(); + const fillGradientStops = style.fillGradientStops.slice(); + + // wanting to evenly distribute the fills. So an array of 4 colours should give fills of 0.25, 0.5 and 0.75 + if (!fillGradientStops.length) + { + const lengthPlus1 = fill.length + 1; + + for (let i = 1; i < lengthPlus1; ++i) + { + fillGradientStops.push(i / lengthPlus1); + } + } + + // stop the bleeding of the last gradient on the line above to the top gradient of the this line + // by hard defining the first gradient colour at point 0, and last gradient colour at point 1 + fill.unshift(style.fill[0]); + fillGradientStops.unshift(0); + + fill.push(style.fill[style.fill.length - 1]); + fillGradientStops.push(1); + if (style.fillGradientType === TEXT_GRADIENT.LINEAR_VERTICAL) { // start the gradient at the top center of the canvas, and end at the bottom middle of the canvas @@ -532,15 +584,22 @@ // we need to repeat the gradient so that each individual line of text has the same vertical gradient effect // ['#FF0000', '#00FF00', '#0000FF'] over 2 lines would create stops at 0.125, 0.25, 0.375, 0.625, 0.75, 0.875 - totalIterations = (style.fill.length + 1) * lines.length; + totalIterations = (fill.length + 1) * lines.length; currentIteration = 0; for (let i = 0; i < lines.length; i++) { currentIteration += 1; - for (let j = 0; j < style.fill.length; j++) + for (let j = 0; j < fill.length; j++) { - stop = (currentIteration / totalIterations); - gradient.addColorStop(stop, style.fill[j]); + if (fillGradientStops[j]) + { + stop = (fillGradientStops[j] / lines.length) + (i / lines.length); + } + else + { + stop = currentIteration / totalIterations; + } + gradient.addColorStop(stop, fill[j]); currentIteration++; } } @@ -552,13 +611,20 @@ // can just evenly space out the gradients in this case, as multiple lines makes no difference // to an even left to right gradient - totalIterations = style.fill.length + 1; + totalIterations = fill.length + 1; currentIteration = 1; - for (let i = 0; i < style.fill.length; i++) + for (let i = 0; i < fill.length; i++) { - stop = currentIteration / totalIterations; - gradient.addColorStop(stop, style.fill[i]); + if (fillGradientStops[i]) + { + stop = fillGradientStops[i]; + } + else + { + stop = currentIteration / totalIterations; + } + gradient.addColorStop(stop, fill[i]); currentIteration++; } } @@ -569,7 +635,7 @@ /** * Destroys this text object. * Note* Unlike a Sprite, a Text object will automatically destroy its baseTexture and texture as - * the majorety of the time the texture will not be shared with any other Sprites. + * the majority of the time the texture will not be shared with any other Sprites. * * @param {object|boolean} [options] - Options parameter. A boolean will act as if all options * have been set to that value @@ -680,7 +746,7 @@ set text(text) // eslint-disable-line require-jsdoc { - text = String(text || ' '); + text = String(text === '' || text === null || text === undefined ? ' ' : text); if (this._text === text) { diff --git a/src/core/text/TextStyle.js b/src/core/text/TextStyle.js index 3b25528..890950b 100644 --- a/src/core/text/TextStyle.js +++ b/src/core/text/TextStyle.js @@ -8,12 +8,14 @@ align: 'left', breakWords: false, dropShadow: false, + dropShadowAlpha: 1, dropShadowAngle: Math.PI / 6, dropShadowBlur: 0, dropShadowColor: '#000000', dropShadowDistance: 5, fill: 'black', fillGradientType: TEXT_GRADIENT.LINEAR_VERTICAL, + fillGradientStops: [], fontFamily: 'Arial', fontSize: 26, fontStyle: 'normal', @@ -27,6 +29,7 @@ stroke: 'black', strokeThickness: 0, textBaseline: 'alphabetic', + trim: false, wordWrap: false, wordWrapWidth: 100, }; @@ -47,6 +50,7 @@ * @param {boolean} [style.breakWords=false] - Indicates if lines can be wrapped within words, it * needs wordWrap to be set to true * @param {boolean} [style.dropShadow=false] - Set a drop shadow for the text + * @param {number} [style.dropShadowAlpha=1] - Set alpha for the drop shadow * @param {number} [style.dropShadowAngle=Math.PI/6] - Set a angle of the drop shadow * @param {number} [style.dropShadowBlur=0] - Set a shadow blur radius * @param {string} [style.dropShadowColor='#000000'] - A fill style to be used on the dropshadow e.g 'red', '#00FF00' @@ -55,8 +59,10 @@ * fillstyle that will be used on the text e.g 'red', '#00FF00'. Can be an array to create a gradient * eg ['#000000','#FFFFFF'] * {@link https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/fillStyle|MDN} - * @param {number} [style.fillGradientType=PIXI.TEXT_GRADIENT.LINEAR_VERTICAL] - If fills styles are - * supplied, this can change the type/direction of the gradient. See {@link PIXI.TEXT_GRADIENT} for possible values + * @param {number} [style.fillGradientType=PIXI.TEXT_GRADIENT.LINEAR_VERTICAL] - If fill is an array of colours + * to create a gradient, this can change the type/direction of the gradient. See {@link PIXI.TEXT_GRADIENT} + * @param {number[]} [style.fillGradientStops] - If fill is an array of colours to create a gradient, this array can set + * the stop points (numbers between 0 and 1) for the color, overriding the default behaviour of evenly spacing them. * @param {string|string[]} [style.fontFamily='Arial'] - The font family * @param {number|string} [style.fontSize=26] - The font size (as a number it converts to px, but as a string, * equivalents are '26px','20pt','160%' or '1.6em') @@ -76,6 +82,7 @@ * e.g 'blue', '#FCFF00' * @param {number} [style.strokeThickness=0] - A number that represents the thickness of the stroke. * Default is 0 (no stroke) + * @param {boolean} [style.trim=false] - Trim transparent borders * @param {string} [style.textBaseline='alphabetic'] - The baseline of the text that is rendered. * @param {boolean} [style.wordWrap=false] - Indicates if word wrap should be used * @param {number} [style.wordWrapWidth=100] - The width at which text will wrap, it needs wordWrap to be set to true @@ -152,6 +159,19 @@ } } + get dropShadowAlpha() + { + return this._dropShadowAlpha; + } + set dropShadowAlpha(dropShadowAlpha) + { + if (this._dropShadowAlpha !== dropShadowAlpha) + { + this._dropShadowAlpha = dropShadowAlpha; + this.styleID++; + } + } + get dropShadowAngle() { return this._dropShadowAngle; @@ -232,6 +252,19 @@ } } + get fillGradientStops() + { + return this._fillGradientStops; + } + set fillGradientStops(fillGradientStops) + { + if (!areArraysEqual(this._fillGradientStops,fillGradientStops)) + { + this._fillGradientStops = fillGradientStops; + this.styleID++; + } + } + get fontFamily() { return this._fontFamily; @@ -402,6 +435,19 @@ } } + get trim() + { + return this._trim; + } + set trim(trim) + { + if (this._trim !== trim) + { + this._trim = trim; + this.styleID++; + } + } + get wordWrap() { return this._wordWrap; @@ -475,3 +521,34 @@ return color; } } + +/** + * Utility function to convert hexadecimal colors to strings, and simply return the color if it's a string. + * This version can also convert array of colors + * + * @param {Array} array1 First array to compare + * @param {Array} array2 Second array to compare + * @return {boolean} Do the arrays contain the same values in the same order + */ +function areArraysEqual(array1, array2) +{ + if (!Array.isArray(array1) || !Array.isArray(array2)) + { + return false; + } + + if (array1.length !== array2.length) + { + return false; + } + + for (let i = 0; i < array1.length; ++i) + { + if (array1[i] !== array2[i]) + { + return false; + } + } + + return true; +} diff --git a/src/core/textures/BaseRenderTexture.js b/src/core/textures/BaseRenderTexture.js index 5d50146..aa727f0 100644 --- a/src/core/textures/BaseRenderTexture.js +++ b/src/core/textures/BaseRenderTexture.js @@ -1,8 +1,6 @@ import BaseTexture from './BaseTexture'; import settings from '../settings'; -const { RESOLUTION, SCALE_MODE } = settings; - /** * A BaseRenderTexture is a special texture that allows any Pixi display object to be rendered to it. * @@ -54,7 +52,7 @@ { super(null, scaleMode); - this.resolution = resolution || RESOLUTION; + this.resolution = resolution || settings.RESOLUTION; this.width = width; this.height = height; @@ -62,7 +60,7 @@ this.realWidth = this.width * this.resolution; this.realHeight = this.height * this.resolution; - this.scaleMode = scaleMode || SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; this.hasLoaded = true; /** diff --git a/src/core/textures/BaseTexture.js b/src/core/textures/BaseTexture.js index 676dca3..aeccf39 100644 --- a/src/core/textures/BaseTexture.js +++ b/src/core/textures/BaseTexture.js @@ -77,7 +77,7 @@ * @default PIXI.settings.SCALE_MODE * @see PIXI.SCALE_MODES */ - this.scaleMode = scaleMode || settings.SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; /** * Set to true once the base texture has successfully loaded. @@ -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/RenderTexture.js b/src/core/textures/RenderTexture.js index 4c5034f..b3c6297 100644 --- a/src/core/textures/RenderTexture.js +++ b/src/core/textures/RenderTexture.js @@ -54,8 +54,8 @@ /* eslint-disable prefer-rest-params, no-console */ const width = arguments[1]; const height = arguments[2]; - const scaleMode = arguments[3] || 0; - const resolution = arguments[4] || 1; + const scaleMode = arguments[3]; + const resolution = arguments[4]; // we have an old render texture.. console.warn(`Please use RenderTexture.create(${width}, ${height}) instead of the ctor directly.`); 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 ca31456..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 @@ -459,7 +497,9 @@ if (frame.x + frame.width > this.baseTexture.width || frame.y + frame.height > this.baseTexture.height) { - throw new Error(`Texture Error: frame does not fit inside the base Texture dimensions ${this}`); + throw new Error('Texture Error: frame does not fit inside the base Texture dimensions: ' + + `X: ${frame.x} + ${frame.width} > ${this.baseTexture.width} ` + + `Y: ${frame.y} + ${frame.height} > ${this.baseTexture.height}`); } // this.valid = frame && frame.width && frame.height && this.baseTexture.source && this.baseTexture.hasLoaded; @@ -520,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. @@ -528,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 2a7497b..99e585a 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)); } /** @@ -360,3 +362,43 @@ * @private */ export const BaseTextureCache = {}; + +/** + * 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/core/utils/trimCanvas.js b/src/core/utils/trimCanvas.js new file mode 100644 index 0000000..5af6de0 --- /dev/null +++ b/src/core/utils/trimCanvas.js @@ -0,0 +1,83 @@ +/** + * Trim transparent borders from a canvas + * + * @memberof PIXI + * @function trimCanvas + * @private + * @param {HTMLCanvasElement} canvas - the canvas to trim + * @returns {object} Trim data + */ +export default function trimCanvas(canvas) +{ + // https://gist.github.com/remy/784508 + + let width = canvas.width; + let height = canvas.height; + + const context = canvas.getContext('2d'); + const imageData = context.getImageData(0, 0, width, height); + const pixels = imageData.data; + const len = pixels.length; + + const bound = { + top: null, + left: null, + right: null, + bottom: null, + }; + let i; + let x; + let y; + + for (i = 0; i < len; i += 4) + { + if (pixels[i + 3] !== 0) + { + x = (i / 4) % width; + y = ~~((i / 4) / width); + + if (bound.top === null) + { + bound.top = y; + } + + if (bound.left === null) + { + bound.left = x; + } + else if (x < bound.left) + { + bound.left = x; + } + + if (bound.right === null) + { + bound.right = x + 1; + } + else if (bound.right < x) + { + bound.right = x + 1; + } + + if (bound.bottom === null) + { + bound.bottom = y; + } + else if (bound.bottom < y) + { + bound.bottom = y; + } + } + } + + width = bound.right - bound.left; + height = bound.bottom - bound.top + 1; + + const data = context.getImageData(bound.left, bound.top, width, height); + + return { + height, + width, + data, + }; +} diff --git a/src/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/extras/TilingSprite.js b/src/extras/TilingSprite.js index f90d248..fe82451 100644 --- a/src/extras/TilingSprite.js +++ b/src/extras/TilingSprite.js @@ -220,20 +220,33 @@ transform.tx * resolution, transform.ty * resolution); - // TODO - this should be rolled into the setTransform above.. - context.scale(this.tileScale.x / baseTextureResolution, this.tileScale.y / baseTextureResolution); - - context.translate(modX + (this.anchor.x * -this._width), - modY + (this.anchor.y * -this._height)); - renderer.setBlendMode(this.blendMode); // fill the pattern! context.fillStyle = this._canvasPattern; - context.fillRect(-modX, - -modY, - this._width / this.tileScale.x * baseTextureResolution, - this._height / this.tileScale.y * baseTextureResolution); + + // TODO - this should be rolled into the setTransform above.. + context.scale(this.tileScale.x / baseTextureResolution, this.tileScale.y / baseTextureResolution); + + const anchorX = this.anchor.x * -this._width; + const anchorY = this.anchor.y * -this._height; + + if (this.uvRespectAnchor) + { + context.translate(modX, modY); + + context.fillRect(-modX + anchorX, -modY + anchorY, + this._width / this.tileScale.x * baseTextureResolution, + this._height / this.tileScale.y * baseTextureResolution); + } + else + { + context.translate(modX + anchorX, modY + anchorY); + + context.fillRect(-modX, -modY, + this._width / this.tileScale.x * baseTextureResolution, + this._height / this.tileScale.y * baseTextureResolution); + } } /** diff --git a/src/extras/cacheAsBitmap.js b/src/extras/cacheAsBitmap.js index 18043a7..5dd5640 100644 --- a/src/extras/cacheAsBitmap.js +++ b/src/extras/cacheAsBitmap.js @@ -41,6 +41,9 @@ * provide a performance benefit for complex static displayObjects. * To remove simply set this property to 'false' * + * IMPORTANT GOTCHA - make sure that all your textures are preloaded BEFORE setting this property to true + * as it will take a snapshot of what is currently there. If the textures have not loaded then they will not appear. + * * @member {boolean} * @memberof PIXI.DisplayObject# */ diff --git a/src/filters/blur/BlurFilter.js b/src/filters/blur/BlurFilter.js index 340fc58..1575db9 100644 --- a/src/filters/blur/BlurFilter.js +++ b/src/filters/blur/BlurFilter.js @@ -24,10 +24,9 @@ this.blurXFilter = new BlurXFilter(strength, quality, resolution, kernelSize); this.blurYFilter = new BlurYFilter(strength, quality, resolution, kernelSize); - this.resolution = 1; this.padding = 0; - this.resolution = resolution || 1; + this.resolution = resolution || core.settings.RESOLUTION; this.quality = quality || 4; this.blur = strength || 8; } diff --git a/src/filters/blur/BlurXFilter.js b/src/filters/blur/BlurXFilter.js index def99c3..0f48889 100644 --- a/src/filters/blur/BlurXFilter.js +++ b/src/filters/blur/BlurXFilter.js @@ -31,7 +31,7 @@ fragSrc ); - this.resolution = resolution || 1; + this.resolution = resolution || core.settings.RESOLUTION; this._quality = 0; diff --git a/src/filters/blur/BlurYFilter.js b/src/filters/blur/BlurYFilter.js index 638e0f0..6d489c7 100644 --- a/src/filters/blur/BlurYFilter.js +++ b/src/filters/blur/BlurYFilter.js @@ -31,7 +31,7 @@ fragSrc ); - this.resolution = resolution || 1; + this.resolution = resolution || core.settings.RESOLUTION; this._quality = 0; diff --git a/src/filters/colormatrix/ColorMatrixFilter.js b/src/filters/colormatrix/ColorMatrixFilter.js index 8509505..679f8ec 100644 --- a/src/filters/colormatrix/ColorMatrixFilter.js +++ b/src/filters/colormatrix/ColorMatrixFilter.js @@ -75,28 +75,28 @@ out[1] = (a[0] * b[1]) + (a[1] * b[6]) + (a[2] * b[11]) + (a[3] * b[16]); out[2] = (a[0] * b[2]) + (a[1] * b[7]) + (a[2] * b[12]) + (a[3] * b[17]); out[3] = (a[0] * b[3]) + (a[1] * b[8]) + (a[2] * b[13]) + (a[3] * b[18]); - out[4] = (a[0] * b[4]) + (a[1] * b[9]) + (a[2] * b[14]) + (a[3] * b[19]); + out[4] = (a[0] * b[4]) + (a[1] * b[9]) + (a[2] * b[14]) + (a[3] * b[19]) + a[4]; // Green Channel out[5] = (a[5] * b[0]) + (a[6] * b[5]) + (a[7] * b[10]) + (a[8] * b[15]); out[6] = (a[5] * b[1]) + (a[6] * b[6]) + (a[7] * b[11]) + (a[8] * b[16]); out[7] = (a[5] * b[2]) + (a[6] * b[7]) + (a[7] * b[12]) + (a[8] * b[17]); out[8] = (a[5] * b[3]) + (a[6] * b[8]) + (a[7] * b[13]) + (a[8] * b[18]); - out[9] = (a[5] * b[4]) + (a[6] * b[9]) + (a[7] * b[14]) + (a[8] * b[19]); + out[9] = (a[5] * b[4]) + (a[6] * b[9]) + (a[7] * b[14]) + (a[8] * b[19]) + a[9]; // Blue Channel out[10] = (a[10] * b[0]) + (a[11] * b[5]) + (a[12] * b[10]) + (a[13] * b[15]); out[11] = (a[10] * b[1]) + (a[11] * b[6]) + (a[12] * b[11]) + (a[13] * b[16]); out[12] = (a[10] * b[2]) + (a[11] * b[7]) + (a[12] * b[12]) + (a[13] * b[17]); out[13] = (a[10] * b[3]) + (a[11] * b[8]) + (a[12] * b[13]) + (a[13] * b[18]); - out[14] = (a[10] * b[4]) + (a[11] * b[9]) + (a[12] * b[14]) + (a[13] * b[19]); + out[14] = (a[10] * b[4]) + (a[11] * b[9]) + (a[12] * b[14]) + (a[13] * b[19]) + a[14]; // Alpha Channel out[15] = (a[15] * b[0]) + (a[16] * b[5]) + (a[17] * b[10]) + (a[18] * b[15]); out[16] = (a[15] * b[1]) + (a[16] * b[6]) + (a[17] * b[11]) + (a[18] * b[16]); out[17] = (a[15] * b[2]) + (a[16] * b[7]) + (a[17] * b[12]) + (a[18] * b[17]); out[18] = (a[15] * b[3]) + (a[16] * b[8]) + (a[17] * b[13]) + (a[18] * b[18]); - out[19] = (a[15] * b[4]) + (a[16] * b[9]) + (a[17] * b[14]) + (a[18] * b[19]); + out[19] = (a[15] * b[4]) + (a[16] * b[9]) + (a[17] * b[14]) + (a[18] * b[19]) + a[19]; return out; } diff --git a/src/filters/colormatrix/colorMatrix.frag b/src/filters/colormatrix/colorMatrix.frag index 3aaf2b3..c73c0e9 100644 --- a/src/filters/colormatrix/colorMatrix.frag +++ b/src/filters/colormatrix/colorMatrix.frag @@ -4,32 +4,38 @@ void main(void) { - vec4 c = texture2D(uSampler, vTextureCoord); + // Un-premultiply alpha before applying the color matrix. See issue #3539. + if (c.a > 0.0) { + c.rgb /= c.a; + } + vec4 result; + result.r = (m[0] * c.r); + result.r += (m[1] * c.g); + result.r += (m[2] * c.b); + result.r += (m[3] * c.a); + result.r += m[4]; - gl_FragColor.r = (m[0] * c.r); - gl_FragColor.r += (m[1] * c.g); - gl_FragColor.r += (m[2] * c.b); - gl_FragColor.r += (m[3] * c.a); - gl_FragColor.r += m[4] * c.a; + result.g = (m[5] * c.r); + result.g += (m[6] * c.g); + result.g += (m[7] * c.b); + result.g += (m[8] * c.a); + result.g += m[9]; - gl_FragColor.g = (m[5] * c.r); - gl_FragColor.g += (m[6] * c.g); - gl_FragColor.g += (m[7] * c.b); - gl_FragColor.g += (m[8] * c.a); - gl_FragColor.g += m[9] * c.a; + result.b = (m[10] * c.r); + result.b += (m[11] * c.g); + result.b += (m[12] * c.b); + result.b += (m[13] * c.a); + result.b += m[14]; - gl_FragColor.b = (m[10] * c.r); - gl_FragColor.b += (m[11] * c.g); - gl_FragColor.b += (m[12] * c.b); - gl_FragColor.b += (m[13] * c.a); - gl_FragColor.b += m[14] * c.a; + result.a = (m[15] * c.r); + result.a += (m[16] * c.g); + result.a += (m[17] * c.b); + result.a += (m[18] * c.a); + result.a += m[19]; - gl_FragColor.a = (m[15] * c.r); - gl_FragColor.a += (m[16] * c.g); - gl_FragColor.a += (m[17] * c.b); - gl_FragColor.a += (m[18] * c.a); - gl_FragColor.a += m[19] * c.a; + // Premultiply alpha again. + result.rgb *= result.a; -// gl_FragColor = vec4(m[0]); + gl_FragColor = result; } diff --git a/src/filters/displacement/DisplacementFilter.js b/src/filters/displacement/DisplacementFilter.js index 112de0b..d5355c3 100644 --- a/src/filters/displacement/DisplacementFilter.js +++ b/src/filters/displacement/DisplacementFilter.js @@ -36,7 +36,7 @@ this.maskMatrix = maskMatrix; this.uniforms.mapSampler = sprite.texture; - this.uniforms.filterMatrix = maskMatrix.toArray(true); + this.uniforms.filterMatrix = maskMatrix; this.uniforms.scale = { x: 1, y: 1 }; if (scale === null || scale === undefined) diff --git a/src/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/InteractionData.js b/src/interaction/InteractionData.js index 02d53a3..818da35 100644 --- a/src/interaction/InteractionData.js +++ b/src/interaction/InteractionData.js @@ -21,9 +21,9 @@ this.global = new core.Point(); /** - * The target Sprite that was interacted with + * The target DisplayObject that was interacted with * - * @member {PIXI.Sprite} + * @member {PIXI.DisplayObject} */ this.target = null; @@ -33,6 +33,13 @@ * @member {Event} */ this.originalEvent = null; + + /** + * Unique identifier for this interaction + * + * @member {number} + */ + this.identifier = null; } /** diff --git a/src/interaction/InteractionEvent.js b/src/interaction/InteractionEvent.js index 75b2966..dc554f9 100644 --- a/src/interaction/InteractionEvent.js +++ b/src/interaction/InteractionEvent.js @@ -58,7 +58,7 @@ } /** - * Prevents event from reaching any objects other than the current object. + * Resets the event. * * @private */ diff --git a/src/interaction/InteractionManager.js b/src/interaction/InteractionManager.js index 7d51134..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/src/prepare/BasePrepare.js b/src/prepare/BasePrepare.js index a2005f5..09eb5e5 100644 --- a/src/prepare/BasePrepare.js +++ b/src/prepare/BasePrepare.js @@ -20,7 +20,7 @@ * * @abstract * @class - * @memberof PIXI + * @memberof PIXI.prepare */ export default class BasePrepare { @@ -107,9 +107,9 @@ /** * Upload all the textures and graphics to the GPU. * - * @param {Function|PIXI.DisplayObject|PIXI.Container} item - Either - * the container or display object to search for items to upload or - * the callback function, if items have been added using `prepare.add`. + * @param {Function|PIXI.DisplayObject|PIXI.Container|PIXI.BaseTexture|PIXI.Texture|PIXI.Graphics|PIXI.Text} item - + * Either the container or display object to search for items to upload, the items to upload themselves, + * or the callback function, if items have been added using `prepare.add`. * @param {Function} [done] - Optional callback when all queued uploads have completed */ upload(item, done) @@ -236,7 +236,8 @@ /** * Manually add an item to the uploading queue. * - * @param {PIXI.DisplayObject|PIXI.Container|*} item - Object to add to the queue + * @param {PIXI.DisplayObject|PIXI.Container|PIXI.BaseTexture|PIXI.Texture|PIXI.Graphics|PIXI.Text|*} item - Object to + * add to the queue * @return {PIXI.CanvasPrepare} Instance of plugin for chaining. */ add(item) diff --git a/src/prepare/canvas/CanvasPrepare.js b/src/prepare/canvas/CanvasPrepare.js index 4b99d22..0224bb5 100644 --- a/src/prepare/canvas/CanvasPrepare.js +++ b/src/prepare/canvas/CanvasPrepare.js @@ -12,7 +12,8 @@ * An instance of this class is automatically created by default, and can be found at renderer.plugins.prepare * * @class - * @memberof PIXI + * @extends PIXI.prepare.BasePrepare + * @memberof PIXI.prepare */ export default class CanvasPrepare extends BasePrepare { diff --git a/src/prepare/webgl/WebGLPrepare.js b/src/prepare/webgl/WebGLPrepare.js index 1ef7467..eb7023f 100644 --- a/src/prepare/webgl/WebGLPrepare.js +++ b/src/prepare/webgl/WebGLPrepare.js @@ -7,7 +7,8 @@ * An instance of this class is automatically created by default, and can be found at renderer.plugins.prepare * * @class - * @memberof PIXI + * @extends PIXI.prepare.BasePrepare + * @memberof PIXI.prepare */ export default class WebGLPrepare extends BasePrepare { diff --git a/test/core/Circle.js b/test/core/Circle.js index 183f98f..f621992 100644 --- a/test/core/Circle.js +++ b/test/core/Circle.js @@ -53,6 +53,10 @@ expect(circ1.contains(10, 16)).to.be.false; expect(circ1.contains(11, 15)).to.be.false; expect(circ1.contains(0, 0)).to.be.false; + + const circ2 = new PIXI.Circle(10, 10, 0); + + expect(circ2.contains(10, 10)).to.be.false; }); it('should return framing rectangle', function () diff --git a/test/core/Container.js b/test/core/Container.js index 8a10b40..cefde54 100644 --- a/test/core/Container.js +++ b/test/core/Container.js @@ -1,5 +1,39 @@ 'use strict'; +function testAddChild(fn) +{ + return function () + { + fn(function (container, obj) + { + container.addChild(obj); + }); + fn(function (container, obj) + { + container.addChildAt(obj); + }); + }; +} + +function testRemoveChild(fn) +{ + return function () + { + 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 +104,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 +279,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/Ellipse.js b/test/core/Ellipse.js index f2e8234..3f47442 100644 --- a/test/core/Ellipse.js +++ b/test/core/Ellipse.js @@ -56,6 +56,10 @@ expect(ellipse1.contains(10, 16)).to.be.false; expect(ellipse1.contains(11, 15)).to.be.false; expect(ellipse1.contains(0, 0)).to.be.false; + + const ellipse2 = new PIXI.Ellipse(10, 10, 0, 0); + + expect(ellipse2.contains(10, 10)).to.be.false; }); it('should return framing rectangle', function () diff --git a/test/core/Graphics.js b/test/core/Graphics.js index 2a64946..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 () @@ -127,6 +130,16 @@ expect(graphics.containsPoint(point)).to.be.false; }); + + it('should return false when no fill', function () + { + const point = new PIXI.Point(1, 1); + const graphics = new PIXI.Graphics(); + + graphics.drawRect(0, 0, 10, 10); + + expect(graphics.containsPoint(point)).to.be.false; + }); }); describe('arc', function () @@ -208,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/Matrix.js b/test/core/Matrix.js index 9d2694b..76bd57c 100644 --- a/test/core/Matrix.js +++ b/test/core/Matrix.js @@ -107,10 +107,60 @@ expect(m1.ty).to.equal(m2.ty); }); + it('should prepend matrix', function () + { + const m1 = new PIXI.Matrix(); + const m2 = new PIXI.Matrix(); + + m2.set(2, 3, 4, 5, 100, 200); + m1.prepend(m2); + + expect(m1.a).to.equal(m2.a); + expect(m1.b).to.equal(m2.b); + expect(m1.c).to.equal(m2.c); + expect(m1.d).to.equal(m2.d); + expect(m1.tx).to.equal(m2.tx); + expect(m1.ty).to.equal(m2.ty); + + const m3 = new PIXI.Matrix(); + const m4 = new PIXI.Matrix(); + + m3.prepend(m4); + + expect(m3.a).to.equal(m4.a); + expect(m3.b).to.equal(m4.b); + expect(m3.c).to.equal(m4.c); + expect(m3.d).to.equal(m4.d); + expect(m3.tx).to.equal(m4.tx); + expect(m3.ty).to.equal(m4.ty); + }); + it('should get IDENTITY and TEMP_MATRIX', function () { expect(PIXI.Matrix.IDENTITY instanceof PIXI.Matrix).to.be.true; expect(PIXI.Matrix.TEMP_MATRIX instanceof PIXI.Matrix).to.be.true; }); -}); + it('should reset matrix to default when identity() is called', function () + { + const matrix = new PIXI.Matrix(); + + matrix.set(2, 3, 4, 5, 100, 200); + + expect(matrix.a).to.equal(2); + expect(matrix.b).to.equal(3); + expect(matrix.c).to.equal(4); + expect(matrix.d).to.equal(5); + expect(matrix.tx).to.equal(100); + expect(matrix.ty).to.equal(200); + + matrix.identity(); + + expect(matrix.a).to.equal(1); + expect(matrix.b).to.equal(0); + expect(matrix.c).to.equal(0); + expect(matrix.d).to.equal(1); + expect(matrix.tx).to.equal(0); + expect(matrix.ty).to.equal(0); + }); +}); 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/Text.js b/test/core/Text.js index b8ce561..5799ad4 100644 --- a/test/core/Text.js +++ b/test/core/Text.js @@ -112,4 +112,42 @@ expect(text.width).to.equal(300); }); }); + + describe('text', function () + { + it('should convert numbers into strings', function () + { + const text = new PIXI.Text(2); + + expect(text.text).to.equal('2'); + }); + + it('should not change 0 to \'\'', function () + { + const text = new PIXI.Text(0); + + expect(text.text).to.equal('0'); + }); + + it('should prevent setting null', function () + { + const text = new PIXI.Text(null); + + expect(text.text).to.equal(' '); + }); + + it('should prevent setting undefined', function () + { + const text = new PIXI.Text(); + + expect(text.text).to.equal(' '); + }); + + it('should prevent setting \'\'', function () + { + const text = new PIXI.Text(''); + + expect(text.text).to.equal(' '); + }); + }); }); diff --git a/test/core/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/TilingSprite.js b/test/core/TilingSprite.js index 2e449a7..7694792 100644 --- a/test/core/TilingSprite.js +++ b/test/core/TilingSprite.js @@ -24,4 +24,25 @@ expect(bounds.height).to.equal(600); }); }); + + it('checks if tilingSprite contains a point', function () + { + const texture = new PIXI.Texture(new PIXI.BaseTexture()); + const tilingSprite = new PIXI.extras.TilingSprite(texture, 200, 300); + + expect(tilingSprite.containsPoint(new PIXI.Point(1, 1))).to.equal(true); + expect(tilingSprite.containsPoint(new PIXI.Point(300, 400))).to.equal(false); + }); + + it('gets and sets height and width correctly', function () + { + const texture = new PIXI.Texture(new PIXI.BaseTexture()); + const tilingSprite = new PIXI.extras.TilingSprite(texture, 200, 300); + + tilingSprite.width = 400; + tilingSprite.height = 600; + + expect(tilingSprite.width).to.equal(400); + expect(tilingSprite.height).to.equal(600); + }); }); diff --git a/package.json b/package.json index 1271fef..71ade15 100644 --- a/package.json +++ b/package.json @@ -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/scripts/release.js b/scripts/release.js index c4c8e36..156620c 100644 --- a/scripts/release.js +++ b/scripts/release.js @@ -12,7 +12,8 @@ 'dist/**/*', 'lib/**/*', 'src/**/*', - 'scripts/**/*', + 'scripts/*', + 'scripts/renders/*', 'test/**/*', '*.json', '*.md', diff --git a/scripts/renders/client.js b/scripts/renders/client.js index 16c9565..6faee34 100755 --- a/scripts/renders/client.js +++ b/scripts/renders/client.js @@ -1,6 +1,6 @@ 'use strict'; -require('../../bin/pixi'); +require('../../dist/pixi'); PIXI.utils.skipHello(); const fs = require('fs'); 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 bc730db..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. @@ -33,8 +33,11 @@ * need to call toDataUrl on the webgl context * @param {number} [options.resolution=1] - The resolution / device pixel ratio of the renderer, retina would be 2 * @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 @@ -43,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. */ @@ -73,7 +99,7 @@ */ stop() { - this.ticker.stop(); + this._ticker.stop(); } /** @@ -81,7 +107,7 @@ */ start() { - this.ticker.start(); + this._ticker.start(); } /** @@ -95,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 index 81e076b..78b17e6 100644 --- a/src/core/Shader.js +++ b/src/core/Shader.js @@ -1,9 +1,7 @@ import { GLShader } from 'pixi-gl-core'; import settings from './settings'; -const { PRECISION } = settings; - -function checkPrecision(src) +function checkPrecision(src, def) { if (src instanceof Array) { @@ -11,14 +9,14 @@ { const copy = src.slice(0); - copy.unshift(`precision ${PRECISION} float;`); + copy.unshift(`precision ${def} float;`); return copy; } } else if (src.substring(0, 9) !== 'precision') { - return `precision ${PRECISION} float;\n${src}`; + return `precision ${def} float;\n${src}`; } return src; @@ -42,6 +40,7 @@ */ constructor(gl, vertexSrc, fragmentSrc) { - super(gl, checkPrecision(vertexSrc), checkPrecision(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 3425b41..f9235e4 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -181,7 +181,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 b3792ef..17fde8d 100644 --- a/src/core/display/Container.js +++ b/src/core/display/Container.js @@ -73,13 +73,14 @@ } child.parent = this; - - // ensure a transform will be recalculated.. - this.transform._parentID = -1; - this._boundsID++; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.push(child); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(this.children.length - 1); child.emit('added', this); @@ -108,9 +109,14 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.splice(index, 0, child); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(index); child.emit('added', this); @@ -174,6 +180,7 @@ removeItems(this.children, currentIndex, 1); // remove from old position this.children.splice(index, 0, child); // add at new position + this.onChildrenChange(index); } @@ -220,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! @@ -244,9 +252,14 @@ { const child = this.getChildAt(index); + // ensure child transform will be recalculated.. child.parent = null; + child.transform._parentID = -1; removeItems(this.children, index, 1); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(index); child.emit('removed', this); @@ -275,8 +288,14 @@ for (let i = 0; i < removed.length; ++i) { removed[i].parent = null; + if (removed[i].transform) + { + removed[i].transform._parentID = -1; + } } + this._boundsID++; + this.onChildrenChange(beginIndex); for (let i = 0; i < removed.length; ++i) diff --git a/src/core/display/DisplayObject.js b/src/core/display/DisplayObject.js index 3affe36..4e6c77d 100644 --- a/src/core/display/DisplayObject.js +++ b/src/core/display/DisplayObject.js @@ -109,7 +109,7 @@ /** * The original, cached mask of the object * - * @member {PIXI.Rectangle} + * @member {PIXI.Graphics|PIXI.Sprite} * @private */ this._mask = null; diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index 95f2af4..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 ); @@ -1065,10 +1061,15 @@ canvasRenderer = new CanvasRenderer(); } - tempMatrix.tx = -bounds.x; - tempMatrix.ty = -bounds.y; + this.transform.updateLocalTransform(); + this.transform.localTransform.copy(tempMatrix); - canvasRenderer.render(this, canvasBuffer, false, tempMatrix); + tempMatrix.invert(); + + tempMatrix.tx -= bounds.x; + tempMatrix.ty -= bounds.y; + + canvasRenderer.render(this, canvasBuffer, true, tempMatrix); const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode); 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 e67680f..867dde2 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/buildCircle.js b/src/core/graphics/webgl/utils/buildCircle.js index 0400d13..abf5d02 100644 --- a/src/core/graphics/webgl/utils/buildCircle.js +++ b/src/core/graphics/webgl/utils/buildCircle.js @@ -33,6 +33,11 @@ height = circleData.height; } + if (width === 0 || height === 0) + { + return; + } + const totalSegs = Math.floor(30 * Math.sqrt(circleData.radius)) || Math.floor(15 * Math.sqrt(circleData.width + circleData.height)); diff --git a/src/core/graphics/webgl/utils/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 90155f9..f03fe15 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -30,6 +30,7 @@ export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; export { default as CanvasGraphicsRenderer } from './graphics/canvas/CanvasGraphicsRenderer'; +export { default as Spritesheet } from './textures/Spritesheet'; export { default as Texture } from './textures/Texture'; export { default as BaseTexture } from './textures/BaseTexture'; export { default as RenderTexture } from './textures/RenderTexture'; diff --git a/src/core/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 ccce680..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); } /** @@ -162,6 +162,9 @@ if (transform) { transform.copy(tempWt); + + // lets not forget to flag the parent transform as dirty... + this._tempDisplayObjectParent.transform._worldID = -1; } else { @@ -169,6 +172,7 @@ } displayObject.parent = this._tempDisplayObjectParent; + displayObject.updateTransform(); displayObject.parent = cacheParent; // displayObject.hitArea = //TODO add a temp hit area @@ -279,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/canvas/utils/CanvasRenderTarget.js b/src/core/renderers/canvas/utils/CanvasRenderTarget.js index 3de0aef..b409891 100644 --- a/src/core/renderers/canvas/utils/CanvasRenderTarget.js +++ b/src/core/renderers/canvas/utils/CanvasRenderTarget.js @@ -1,5 +1,4 @@ import settings from '../../../settings'; -const { RESOLUTION } = settings; /** * Creates a Canvas element of the given size. @@ -30,7 +29,7 @@ */ this.context = this.canvas.getContext('2d'); - this.resolution = resolution || RESOLUTION; + this.resolution = resolution || settings.RESOLUTION; this.resize(width, height); } 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 ce0e1c4..788ed6c 100644 --- a/src/core/renderers/webgl/WebGLRenderer.js +++ b/src/core/renderers/webgl/WebGLRenderer.js @@ -30,8 +30,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 @@ -49,10 +49,19 @@ * enable this if you need to call toDataUrl on the webgl context. * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when * rendering, stopping pixel interpolation. + * @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; + + if (this.legacy) + { + glCore.VertexArrayObject.FORCE_NATIVE = true; + } /** * The type of this renderer as a standardised const @@ -229,7 +238,7 @@ this.emit('context', gl); // setup the width/height properties and gl viewport - this.resize(this.width, this.height); + this.resize(this.screen.width, this.screen.height); } /** @@ -323,16 +332,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) { @@ -376,6 +385,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 @@ -440,9 +469,10 @@ * Changes the current shader to the one given in parameter * * @param {PIXI.Shader} shader - the new shader + * @param {boolean} [autoProject=true] - Whether automatically set the projection matrix * @return {PIXI.WebGLRenderer} Returns itself. */ - bindShader(shader) + bindShader(shader, autoProject) { // TODO cache if (this._activeShader !== shader) @@ -450,8 +480,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 bbab3b3..944f41d 100644 --- a/src/core/renderers/webgl/filters/Filter.js +++ b/src/core/renderers/webgl/filters/Filter.js @@ -1,6 +1,7 @@ import extractUniformsFromSrc from './extractUniformsFromSrc'; import { uid } from '../../../utils'; import { BLEND_MODES } from '../../../const'; +import settings from '../../../settings'; const SOURCE_KEY_MAP = {}; @@ -10,7 +11,7 @@ * @memberof PIXI * @extends PIXI.Shader */ -class Filter +export default class Filter { /** * @param {string} [vertexSrc] - The source of the vertex shader. @@ -35,8 +36,6 @@ this.blendMode = BLEND_MODES.NORMAL; - // pull out the vertex and shader uniforms if they are not specified.. - // currently this does not extract structs only default types this.uniformData = uniforms || extractUniformsFromSrc(this.vertexSrc, this.fragmentSrc, 'projectionMatrix|uSampler'); /** @@ -80,7 +79,7 @@ * * @member {number} */ - this.resolution = 1; + this.resolution = settings.RESOLUTION; /** * If enabled is true the filter is applied, if false it will not. @@ -97,8 +96,11 @@ * @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 { // --- // // this.uniforms.filterMatrix = filterManager.calculateSpriteMatrix(tempMatrix, window.panda ); @@ -170,5 +172,3 @@ ].join('\n'); } } - -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 dc7072a..6c7422d 100644 --- a/src/core/renderers/webgl/managers/FilterManager.js +++ b/src/core/renderers/webgl/managers/FilterManager.js @@ -154,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 @@ -176,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; @@ -184,7 +184,7 @@ flop = t; } - filters[i].apply(this, flip, lastState.renderTarget, true); + filters[i].apply(this, flip, lastState.renderTarget, false, currentState); this.freePotRenderTarget(flip); this.freePotRenderTarget(flop); @@ -291,9 +291,13 @@ let textureCount = 1; let currentState; - if (shader.uniforms.data.filterArea) + // 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; @@ -306,9 +310,9 @@ // use this to clamp displaced texture coords so they belong to filterArea // see displacementFilter fragment shader for an example - if (shader.uniforms.data.filterClamp) + if (shader.uniforms.filterClamp) { - currentState = this.filterData.stack[this.filterData.index]; + currentState = currentState || this.filterData.stack[this.filterData.index]; const filterClamp = shader.uniforms.filterClamp; @@ -485,7 +489,7 @@ */ destroy() { - this.shaderCache = []; + this.shaderCache = {}; this.emptyPool(); } diff --git a/src/core/renderers/webgl/utils/RenderTarget.js b/src/core/renderers/webgl/utils/RenderTarget.js index afe51ab..6d2467f 100644 --- a/src/core/renderers/webgl/utils/RenderTarget.js +++ b/src/core/renderers/webgl/utils/RenderTarget.js @@ -124,7 +124,7 @@ * @default PIXI.settings.SCALE_MODE * @see PIXI.SCALE_MODES */ - this.scaleMode = scaleMode || settings.SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; /** * Whether this object is the root element or not 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 36ea5bb..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; @@ -346,8 +346,8 @@ /** * Gets the local bounds of the sprite object. * - * @param {Rectangle} rect - The output rectangle. - * @return {Rectangle} The bounds. + * @param {PIXI.Rectangle} rect - The output rectangle. + * @return {PIXI.Rectangle} The bounds. */ getLocalBounds(rect) { @@ -440,7 +440,7 @@ * * @static * @param {number|string|PIXI.BaseTexture|HTMLCanvasElement|HTMLVideoElement} source Source to create texture from - * @return {PIXI.Texture} 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 e77b39a..90b08c0 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++) @@ -103,11 +102,18 @@ { const gl = this.renderer.gl; - // step 1: first check max textures the GPU can handle. - this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); + if (this.renderer.legacy) + { + this.MAX_TEXTURES = 1; + } + else + { + // step 1: first check max textures the GPU can handle. + this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); - // step 2: check the maximum number of if statements the shader can have too.. - this.MAX_TEXTURES = checkMaxIfStatmentsInShader(this.MAX_TEXTURES, gl); + // step 2: check the maximum number of if statements the shader can have too.. + this.MAX_TEXTURES = checkMaxIfStatmentsInShader(this.MAX_TEXTURES, gl); + } const shader = this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); @@ -130,8 +136,12 @@ .addIndex(this.indexBuffer) .addAttribute(this.vertexBuffers[i], shader.attributes.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) .addAttribute(this.vertexBuffers[i], shader.attributes.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) - .addAttribute(this.vertexBuffers[i], shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4) - .addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + .addAttribute(this.vertexBuffers[i], shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); + + if (shader.attributes.aTextureId) + { + this.vaos[i].addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + } /* eslint-enable max-len */ } @@ -378,8 +388,12 @@ .addIndex(this.indexBuffer) .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) - .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4) - .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); + + if (this.shader.attributes.aTextureId) + { + this.vaos[this.vertexCount].addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + } /* eslint-enable max-len */ } diff --git a/src/core/text/Text.js b/src/core/text/Text.js index 013625a..cec8c32 100644 --- a/src/core/text/Text.js +++ b/src/core/text/Text.js @@ -6,6 +6,7 @@ import { TEXT_GRADIENT } from '../const'; import settings from '../settings'; import TextStyle from './TextStyle'; +import trimCanvas from '../utils/trimCanvas'; const defaultDestroyOptions = { texture: true, @@ -57,7 +58,7 @@ /** * The canvas 2d context that everything is drawn with - * @member {HTMLCanvasElement} + * @member {CanvasRenderingContext2D} */ this.context = this.canvas.getContext('2d'); @@ -189,10 +190,12 @@ if (style.dropShadow) { + this.context.shadowBlur = style.dropShadowBlur; + this.context.globalAlpha = style.dropShadowAlpha; + if (style.dropShadowBlur > 0) { this.context.shadowColor = style.dropShadowColor; - this.context.shadowBlur = style.dropShadowBlur; } else { @@ -237,6 +240,10 @@ } } + // reset the shadow blur and alpha that was set by the drop shadow, for the regular text + this.context.shadowBlur = 0; + this.context.globalAlpha = 1; + // set canvas text styles this.context.fillStyle = this._generateFillStyle(style, lines); @@ -326,6 +333,15 @@ */ updateTexture() { + if (this._style.trim) + { + const trimmed = trimCanvas(this.canvas); + + this.canvas.width = trimmed.width; + this.canvas.height = trimmed.height; + this.context.putImageData(trimmed.data, 0, 0); + } + const texture = this._texture; const style = this._style; @@ -475,6 +491,19 @@ } /** + * Gets the local bounds of the text object. + * + * @param {Rectangle} rect - The output rectangle. + * @return {Rectangle} The bounds. + */ + getLocalBounds(rect) + { + this.updateText(true); + + return super.getLocalBounds.call(this, rect); + } + + /** * calculates the bounds of the Text as a rectangle. The bounds calculation takes the worldTransform into account. */ _calculateBounds() @@ -525,6 +554,29 @@ const width = this.canvas.width / this.resolution; const height = this.canvas.height / this.resolution; + // make a copy of the style settings, so we can manipulate them later + const fill = style.fill.slice(); + const fillGradientStops = style.fillGradientStops.slice(); + + // wanting to evenly distribute the fills. So an array of 4 colours should give fills of 0.25, 0.5 and 0.75 + if (!fillGradientStops.length) + { + const lengthPlus1 = fill.length + 1; + + for (let i = 1; i < lengthPlus1; ++i) + { + fillGradientStops.push(i / lengthPlus1); + } + } + + // stop the bleeding of the last gradient on the line above to the top gradient of the this line + // by hard defining the first gradient colour at point 0, and last gradient colour at point 1 + fill.unshift(style.fill[0]); + fillGradientStops.unshift(0); + + fill.push(style.fill[style.fill.length - 1]); + fillGradientStops.push(1); + if (style.fillGradientType === TEXT_GRADIENT.LINEAR_VERTICAL) { // start the gradient at the top center of the canvas, and end at the bottom middle of the canvas @@ -532,15 +584,22 @@ // we need to repeat the gradient so that each individual line of text has the same vertical gradient effect // ['#FF0000', '#00FF00', '#0000FF'] over 2 lines would create stops at 0.125, 0.25, 0.375, 0.625, 0.75, 0.875 - totalIterations = (style.fill.length + 1) * lines.length; + totalIterations = (fill.length + 1) * lines.length; currentIteration = 0; for (let i = 0; i < lines.length; i++) { currentIteration += 1; - for (let j = 0; j < style.fill.length; j++) + for (let j = 0; j < fill.length; j++) { - stop = (currentIteration / totalIterations); - gradient.addColorStop(stop, style.fill[j]); + if (fillGradientStops[j]) + { + stop = (fillGradientStops[j] / lines.length) + (i / lines.length); + } + else + { + stop = currentIteration / totalIterations; + } + gradient.addColorStop(stop, fill[j]); currentIteration++; } } @@ -552,13 +611,20 @@ // can just evenly space out the gradients in this case, as multiple lines makes no difference // to an even left to right gradient - totalIterations = style.fill.length + 1; + totalIterations = fill.length + 1; currentIteration = 1; - for (let i = 0; i < style.fill.length; i++) + for (let i = 0; i < fill.length; i++) { - stop = currentIteration / totalIterations; - gradient.addColorStop(stop, style.fill[i]); + if (fillGradientStops[i]) + { + stop = fillGradientStops[i]; + } + else + { + stop = currentIteration / totalIterations; + } + gradient.addColorStop(stop, fill[i]); currentIteration++; } } @@ -569,7 +635,7 @@ /** * Destroys this text object. * Note* Unlike a Sprite, a Text object will automatically destroy its baseTexture and texture as - * the majorety of the time the texture will not be shared with any other Sprites. + * the majority of the time the texture will not be shared with any other Sprites. * * @param {object|boolean} [options] - Options parameter. A boolean will act as if all options * have been set to that value @@ -680,7 +746,7 @@ set text(text) // eslint-disable-line require-jsdoc { - text = String(text || ' '); + text = String(text === '' || text === null || text === undefined ? ' ' : text); if (this._text === text) { diff --git a/src/core/text/TextStyle.js b/src/core/text/TextStyle.js index 3b25528..890950b 100644 --- a/src/core/text/TextStyle.js +++ b/src/core/text/TextStyle.js @@ -8,12 +8,14 @@ align: 'left', breakWords: false, dropShadow: false, + dropShadowAlpha: 1, dropShadowAngle: Math.PI / 6, dropShadowBlur: 0, dropShadowColor: '#000000', dropShadowDistance: 5, fill: 'black', fillGradientType: TEXT_GRADIENT.LINEAR_VERTICAL, + fillGradientStops: [], fontFamily: 'Arial', fontSize: 26, fontStyle: 'normal', @@ -27,6 +29,7 @@ stroke: 'black', strokeThickness: 0, textBaseline: 'alphabetic', + trim: false, wordWrap: false, wordWrapWidth: 100, }; @@ -47,6 +50,7 @@ * @param {boolean} [style.breakWords=false] - Indicates if lines can be wrapped within words, it * needs wordWrap to be set to true * @param {boolean} [style.dropShadow=false] - Set a drop shadow for the text + * @param {number} [style.dropShadowAlpha=1] - Set alpha for the drop shadow * @param {number} [style.dropShadowAngle=Math.PI/6] - Set a angle of the drop shadow * @param {number} [style.dropShadowBlur=0] - Set a shadow blur radius * @param {string} [style.dropShadowColor='#000000'] - A fill style to be used on the dropshadow e.g 'red', '#00FF00' @@ -55,8 +59,10 @@ * fillstyle that will be used on the text e.g 'red', '#00FF00'. Can be an array to create a gradient * eg ['#000000','#FFFFFF'] * {@link https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/fillStyle|MDN} - * @param {number} [style.fillGradientType=PIXI.TEXT_GRADIENT.LINEAR_VERTICAL] - If fills styles are - * supplied, this can change the type/direction of the gradient. See {@link PIXI.TEXT_GRADIENT} for possible values + * @param {number} [style.fillGradientType=PIXI.TEXT_GRADIENT.LINEAR_VERTICAL] - If fill is an array of colours + * to create a gradient, this can change the type/direction of the gradient. See {@link PIXI.TEXT_GRADIENT} + * @param {number[]} [style.fillGradientStops] - If fill is an array of colours to create a gradient, this array can set + * the stop points (numbers between 0 and 1) for the color, overriding the default behaviour of evenly spacing them. * @param {string|string[]} [style.fontFamily='Arial'] - The font family * @param {number|string} [style.fontSize=26] - The font size (as a number it converts to px, but as a string, * equivalents are '26px','20pt','160%' or '1.6em') @@ -76,6 +82,7 @@ * e.g 'blue', '#FCFF00' * @param {number} [style.strokeThickness=0] - A number that represents the thickness of the stroke. * Default is 0 (no stroke) + * @param {boolean} [style.trim=false] - Trim transparent borders * @param {string} [style.textBaseline='alphabetic'] - The baseline of the text that is rendered. * @param {boolean} [style.wordWrap=false] - Indicates if word wrap should be used * @param {number} [style.wordWrapWidth=100] - The width at which text will wrap, it needs wordWrap to be set to true @@ -152,6 +159,19 @@ } } + get dropShadowAlpha() + { + return this._dropShadowAlpha; + } + set dropShadowAlpha(dropShadowAlpha) + { + if (this._dropShadowAlpha !== dropShadowAlpha) + { + this._dropShadowAlpha = dropShadowAlpha; + this.styleID++; + } + } + get dropShadowAngle() { return this._dropShadowAngle; @@ -232,6 +252,19 @@ } } + get fillGradientStops() + { + return this._fillGradientStops; + } + set fillGradientStops(fillGradientStops) + { + if (!areArraysEqual(this._fillGradientStops,fillGradientStops)) + { + this._fillGradientStops = fillGradientStops; + this.styleID++; + } + } + get fontFamily() { return this._fontFamily; @@ -402,6 +435,19 @@ } } + get trim() + { + return this._trim; + } + set trim(trim) + { + if (this._trim !== trim) + { + this._trim = trim; + this.styleID++; + } + } + get wordWrap() { return this._wordWrap; @@ -475,3 +521,34 @@ return color; } } + +/** + * Utility function to convert hexadecimal colors to strings, and simply return the color if it's a string. + * This version can also convert array of colors + * + * @param {Array} array1 First array to compare + * @param {Array} array2 Second array to compare + * @return {boolean} Do the arrays contain the same values in the same order + */ +function areArraysEqual(array1, array2) +{ + if (!Array.isArray(array1) || !Array.isArray(array2)) + { + return false; + } + + if (array1.length !== array2.length) + { + return false; + } + + for (let i = 0; i < array1.length; ++i) + { + if (array1[i] !== array2[i]) + { + return false; + } + } + + return true; +} diff --git a/src/core/textures/BaseRenderTexture.js b/src/core/textures/BaseRenderTexture.js index 5d50146..aa727f0 100644 --- a/src/core/textures/BaseRenderTexture.js +++ b/src/core/textures/BaseRenderTexture.js @@ -1,8 +1,6 @@ import BaseTexture from './BaseTexture'; import settings from '../settings'; -const { RESOLUTION, SCALE_MODE } = settings; - /** * A BaseRenderTexture is a special texture that allows any Pixi display object to be rendered to it. * @@ -54,7 +52,7 @@ { super(null, scaleMode); - this.resolution = resolution || RESOLUTION; + this.resolution = resolution || settings.RESOLUTION; this.width = width; this.height = height; @@ -62,7 +60,7 @@ this.realWidth = this.width * this.resolution; this.realHeight = this.height * this.resolution; - this.scaleMode = scaleMode || SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; this.hasLoaded = true; /** diff --git a/src/core/textures/BaseTexture.js b/src/core/textures/BaseTexture.js index 676dca3..aeccf39 100644 --- a/src/core/textures/BaseTexture.js +++ b/src/core/textures/BaseTexture.js @@ -77,7 +77,7 @@ * @default PIXI.settings.SCALE_MODE * @see PIXI.SCALE_MODES */ - this.scaleMode = scaleMode || settings.SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; /** * Set to true once the base texture has successfully loaded. @@ -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/RenderTexture.js b/src/core/textures/RenderTexture.js index 4c5034f..b3c6297 100644 --- a/src/core/textures/RenderTexture.js +++ b/src/core/textures/RenderTexture.js @@ -54,8 +54,8 @@ /* eslint-disable prefer-rest-params, no-console */ const width = arguments[1]; const height = arguments[2]; - const scaleMode = arguments[3] || 0; - const resolution = arguments[4] || 1; + const scaleMode = arguments[3]; + const resolution = arguments[4]; // we have an old render texture.. console.warn(`Please use RenderTexture.create(${width}, ${height}) instead of the ctor directly.`); 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 ca31456..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 @@ -459,7 +497,9 @@ if (frame.x + frame.width > this.baseTexture.width || frame.y + frame.height > this.baseTexture.height) { - throw new Error(`Texture Error: frame does not fit inside the base Texture dimensions ${this}`); + throw new Error('Texture Error: frame does not fit inside the base Texture dimensions: ' + + `X: ${frame.x} + ${frame.width} > ${this.baseTexture.width} ` + + `Y: ${frame.y} + ${frame.height} > ${this.baseTexture.height}`); } // this.valid = frame && frame.width && frame.height && this.baseTexture.source && this.baseTexture.hasLoaded; @@ -520,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. @@ -528,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 2a7497b..99e585a 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)); } /** @@ -360,3 +362,43 @@ * @private */ export const BaseTextureCache = {}; + +/** + * 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/core/utils/trimCanvas.js b/src/core/utils/trimCanvas.js new file mode 100644 index 0000000..5af6de0 --- /dev/null +++ b/src/core/utils/trimCanvas.js @@ -0,0 +1,83 @@ +/** + * Trim transparent borders from a canvas + * + * @memberof PIXI + * @function trimCanvas + * @private + * @param {HTMLCanvasElement} canvas - the canvas to trim + * @returns {object} Trim data + */ +export default function trimCanvas(canvas) +{ + // https://gist.github.com/remy/784508 + + let width = canvas.width; + let height = canvas.height; + + const context = canvas.getContext('2d'); + const imageData = context.getImageData(0, 0, width, height); + const pixels = imageData.data; + const len = pixels.length; + + const bound = { + top: null, + left: null, + right: null, + bottom: null, + }; + let i; + let x; + let y; + + for (i = 0; i < len; i += 4) + { + if (pixels[i + 3] !== 0) + { + x = (i / 4) % width; + y = ~~((i / 4) / width); + + if (bound.top === null) + { + bound.top = y; + } + + if (bound.left === null) + { + bound.left = x; + } + else if (x < bound.left) + { + bound.left = x; + } + + if (bound.right === null) + { + bound.right = x + 1; + } + else if (bound.right < x) + { + bound.right = x + 1; + } + + if (bound.bottom === null) + { + bound.bottom = y; + } + else if (bound.bottom < y) + { + bound.bottom = y; + } + } + } + + width = bound.right - bound.left; + height = bound.bottom - bound.top + 1; + + const data = context.getImageData(bound.left, bound.top, width, height); + + return { + height, + width, + data, + }; +} diff --git a/src/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/extras/TilingSprite.js b/src/extras/TilingSprite.js index f90d248..fe82451 100644 --- a/src/extras/TilingSprite.js +++ b/src/extras/TilingSprite.js @@ -220,20 +220,33 @@ transform.tx * resolution, transform.ty * resolution); - // TODO - this should be rolled into the setTransform above.. - context.scale(this.tileScale.x / baseTextureResolution, this.tileScale.y / baseTextureResolution); - - context.translate(modX + (this.anchor.x * -this._width), - modY + (this.anchor.y * -this._height)); - renderer.setBlendMode(this.blendMode); // fill the pattern! context.fillStyle = this._canvasPattern; - context.fillRect(-modX, - -modY, - this._width / this.tileScale.x * baseTextureResolution, - this._height / this.tileScale.y * baseTextureResolution); + + // TODO - this should be rolled into the setTransform above.. + context.scale(this.tileScale.x / baseTextureResolution, this.tileScale.y / baseTextureResolution); + + const anchorX = this.anchor.x * -this._width; + const anchorY = this.anchor.y * -this._height; + + if (this.uvRespectAnchor) + { + context.translate(modX, modY); + + context.fillRect(-modX + anchorX, -modY + anchorY, + this._width / this.tileScale.x * baseTextureResolution, + this._height / this.tileScale.y * baseTextureResolution); + } + else + { + context.translate(modX + anchorX, modY + anchorY); + + context.fillRect(-modX, -modY, + this._width / this.tileScale.x * baseTextureResolution, + this._height / this.tileScale.y * baseTextureResolution); + } } /** diff --git a/src/extras/cacheAsBitmap.js b/src/extras/cacheAsBitmap.js index 18043a7..5dd5640 100644 --- a/src/extras/cacheAsBitmap.js +++ b/src/extras/cacheAsBitmap.js @@ -41,6 +41,9 @@ * provide a performance benefit for complex static displayObjects. * To remove simply set this property to 'false' * + * IMPORTANT GOTCHA - make sure that all your textures are preloaded BEFORE setting this property to true + * as it will take a snapshot of what is currently there. If the textures have not loaded then they will not appear. + * * @member {boolean} * @memberof PIXI.DisplayObject# */ diff --git a/src/filters/blur/BlurFilter.js b/src/filters/blur/BlurFilter.js index 340fc58..1575db9 100644 --- a/src/filters/blur/BlurFilter.js +++ b/src/filters/blur/BlurFilter.js @@ -24,10 +24,9 @@ this.blurXFilter = new BlurXFilter(strength, quality, resolution, kernelSize); this.blurYFilter = new BlurYFilter(strength, quality, resolution, kernelSize); - this.resolution = 1; this.padding = 0; - this.resolution = resolution || 1; + this.resolution = resolution || core.settings.RESOLUTION; this.quality = quality || 4; this.blur = strength || 8; } diff --git a/src/filters/blur/BlurXFilter.js b/src/filters/blur/BlurXFilter.js index def99c3..0f48889 100644 --- a/src/filters/blur/BlurXFilter.js +++ b/src/filters/blur/BlurXFilter.js @@ -31,7 +31,7 @@ fragSrc ); - this.resolution = resolution || 1; + this.resolution = resolution || core.settings.RESOLUTION; this._quality = 0; diff --git a/src/filters/blur/BlurYFilter.js b/src/filters/blur/BlurYFilter.js index 638e0f0..6d489c7 100644 --- a/src/filters/blur/BlurYFilter.js +++ b/src/filters/blur/BlurYFilter.js @@ -31,7 +31,7 @@ fragSrc ); - this.resolution = resolution || 1; + this.resolution = resolution || core.settings.RESOLUTION; this._quality = 0; diff --git a/src/filters/colormatrix/ColorMatrixFilter.js b/src/filters/colormatrix/ColorMatrixFilter.js index 8509505..679f8ec 100644 --- a/src/filters/colormatrix/ColorMatrixFilter.js +++ b/src/filters/colormatrix/ColorMatrixFilter.js @@ -75,28 +75,28 @@ out[1] = (a[0] * b[1]) + (a[1] * b[6]) + (a[2] * b[11]) + (a[3] * b[16]); out[2] = (a[0] * b[2]) + (a[1] * b[7]) + (a[2] * b[12]) + (a[3] * b[17]); out[3] = (a[0] * b[3]) + (a[1] * b[8]) + (a[2] * b[13]) + (a[3] * b[18]); - out[4] = (a[0] * b[4]) + (a[1] * b[9]) + (a[2] * b[14]) + (a[3] * b[19]); + out[4] = (a[0] * b[4]) + (a[1] * b[9]) + (a[2] * b[14]) + (a[3] * b[19]) + a[4]; // Green Channel out[5] = (a[5] * b[0]) + (a[6] * b[5]) + (a[7] * b[10]) + (a[8] * b[15]); out[6] = (a[5] * b[1]) + (a[6] * b[6]) + (a[7] * b[11]) + (a[8] * b[16]); out[7] = (a[5] * b[2]) + (a[6] * b[7]) + (a[7] * b[12]) + (a[8] * b[17]); out[8] = (a[5] * b[3]) + (a[6] * b[8]) + (a[7] * b[13]) + (a[8] * b[18]); - out[9] = (a[5] * b[4]) + (a[6] * b[9]) + (a[7] * b[14]) + (a[8] * b[19]); + out[9] = (a[5] * b[4]) + (a[6] * b[9]) + (a[7] * b[14]) + (a[8] * b[19]) + a[9]; // Blue Channel out[10] = (a[10] * b[0]) + (a[11] * b[5]) + (a[12] * b[10]) + (a[13] * b[15]); out[11] = (a[10] * b[1]) + (a[11] * b[6]) + (a[12] * b[11]) + (a[13] * b[16]); out[12] = (a[10] * b[2]) + (a[11] * b[7]) + (a[12] * b[12]) + (a[13] * b[17]); out[13] = (a[10] * b[3]) + (a[11] * b[8]) + (a[12] * b[13]) + (a[13] * b[18]); - out[14] = (a[10] * b[4]) + (a[11] * b[9]) + (a[12] * b[14]) + (a[13] * b[19]); + out[14] = (a[10] * b[4]) + (a[11] * b[9]) + (a[12] * b[14]) + (a[13] * b[19]) + a[14]; // Alpha Channel out[15] = (a[15] * b[0]) + (a[16] * b[5]) + (a[17] * b[10]) + (a[18] * b[15]); out[16] = (a[15] * b[1]) + (a[16] * b[6]) + (a[17] * b[11]) + (a[18] * b[16]); out[17] = (a[15] * b[2]) + (a[16] * b[7]) + (a[17] * b[12]) + (a[18] * b[17]); out[18] = (a[15] * b[3]) + (a[16] * b[8]) + (a[17] * b[13]) + (a[18] * b[18]); - out[19] = (a[15] * b[4]) + (a[16] * b[9]) + (a[17] * b[14]) + (a[18] * b[19]); + out[19] = (a[15] * b[4]) + (a[16] * b[9]) + (a[17] * b[14]) + (a[18] * b[19]) + a[19]; return out; } diff --git a/src/filters/colormatrix/colorMatrix.frag b/src/filters/colormatrix/colorMatrix.frag index 3aaf2b3..c73c0e9 100644 --- a/src/filters/colormatrix/colorMatrix.frag +++ b/src/filters/colormatrix/colorMatrix.frag @@ -4,32 +4,38 @@ void main(void) { - vec4 c = texture2D(uSampler, vTextureCoord); + // Un-premultiply alpha before applying the color matrix. See issue #3539. + if (c.a > 0.0) { + c.rgb /= c.a; + } + vec4 result; + result.r = (m[0] * c.r); + result.r += (m[1] * c.g); + result.r += (m[2] * c.b); + result.r += (m[3] * c.a); + result.r += m[4]; - gl_FragColor.r = (m[0] * c.r); - gl_FragColor.r += (m[1] * c.g); - gl_FragColor.r += (m[2] * c.b); - gl_FragColor.r += (m[3] * c.a); - gl_FragColor.r += m[4] * c.a; + result.g = (m[5] * c.r); + result.g += (m[6] * c.g); + result.g += (m[7] * c.b); + result.g += (m[8] * c.a); + result.g += m[9]; - gl_FragColor.g = (m[5] * c.r); - gl_FragColor.g += (m[6] * c.g); - gl_FragColor.g += (m[7] * c.b); - gl_FragColor.g += (m[8] * c.a); - gl_FragColor.g += m[9] * c.a; + result.b = (m[10] * c.r); + result.b += (m[11] * c.g); + result.b += (m[12] * c.b); + result.b += (m[13] * c.a); + result.b += m[14]; - gl_FragColor.b = (m[10] * c.r); - gl_FragColor.b += (m[11] * c.g); - gl_FragColor.b += (m[12] * c.b); - gl_FragColor.b += (m[13] * c.a); - gl_FragColor.b += m[14] * c.a; + result.a = (m[15] * c.r); + result.a += (m[16] * c.g); + result.a += (m[17] * c.b); + result.a += (m[18] * c.a); + result.a += m[19]; - gl_FragColor.a = (m[15] * c.r); - gl_FragColor.a += (m[16] * c.g); - gl_FragColor.a += (m[17] * c.b); - gl_FragColor.a += (m[18] * c.a); - gl_FragColor.a += m[19] * c.a; + // Premultiply alpha again. + result.rgb *= result.a; -// gl_FragColor = vec4(m[0]); + gl_FragColor = result; } diff --git a/src/filters/displacement/DisplacementFilter.js b/src/filters/displacement/DisplacementFilter.js index 112de0b..d5355c3 100644 --- a/src/filters/displacement/DisplacementFilter.js +++ b/src/filters/displacement/DisplacementFilter.js @@ -36,7 +36,7 @@ this.maskMatrix = maskMatrix; this.uniforms.mapSampler = sprite.texture; - this.uniforms.filterMatrix = maskMatrix.toArray(true); + this.uniforms.filterMatrix = maskMatrix; this.uniforms.scale = { x: 1, y: 1 }; if (scale === null || scale === undefined) diff --git a/src/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/InteractionData.js b/src/interaction/InteractionData.js index 02d53a3..818da35 100644 --- a/src/interaction/InteractionData.js +++ b/src/interaction/InteractionData.js @@ -21,9 +21,9 @@ this.global = new core.Point(); /** - * The target Sprite that was interacted with + * The target DisplayObject that was interacted with * - * @member {PIXI.Sprite} + * @member {PIXI.DisplayObject} */ this.target = null; @@ -33,6 +33,13 @@ * @member {Event} */ this.originalEvent = null; + + /** + * Unique identifier for this interaction + * + * @member {number} + */ + this.identifier = null; } /** diff --git a/src/interaction/InteractionEvent.js b/src/interaction/InteractionEvent.js index 75b2966..dc554f9 100644 --- a/src/interaction/InteractionEvent.js +++ b/src/interaction/InteractionEvent.js @@ -58,7 +58,7 @@ } /** - * Prevents event from reaching any objects other than the current object. + * Resets the event. * * @private */ diff --git a/src/interaction/InteractionManager.js b/src/interaction/InteractionManager.js index 7d51134..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/src/prepare/BasePrepare.js b/src/prepare/BasePrepare.js index a2005f5..09eb5e5 100644 --- a/src/prepare/BasePrepare.js +++ b/src/prepare/BasePrepare.js @@ -20,7 +20,7 @@ * * @abstract * @class - * @memberof PIXI + * @memberof PIXI.prepare */ export default class BasePrepare { @@ -107,9 +107,9 @@ /** * Upload all the textures and graphics to the GPU. * - * @param {Function|PIXI.DisplayObject|PIXI.Container} item - Either - * the container or display object to search for items to upload or - * the callback function, if items have been added using `prepare.add`. + * @param {Function|PIXI.DisplayObject|PIXI.Container|PIXI.BaseTexture|PIXI.Texture|PIXI.Graphics|PIXI.Text} item - + * Either the container or display object to search for items to upload, the items to upload themselves, + * or the callback function, if items have been added using `prepare.add`. * @param {Function} [done] - Optional callback when all queued uploads have completed */ upload(item, done) @@ -236,7 +236,8 @@ /** * Manually add an item to the uploading queue. * - * @param {PIXI.DisplayObject|PIXI.Container|*} item - Object to add to the queue + * @param {PIXI.DisplayObject|PIXI.Container|PIXI.BaseTexture|PIXI.Texture|PIXI.Graphics|PIXI.Text|*} item - Object to + * add to the queue * @return {PIXI.CanvasPrepare} Instance of plugin for chaining. */ add(item) diff --git a/src/prepare/canvas/CanvasPrepare.js b/src/prepare/canvas/CanvasPrepare.js index 4b99d22..0224bb5 100644 --- a/src/prepare/canvas/CanvasPrepare.js +++ b/src/prepare/canvas/CanvasPrepare.js @@ -12,7 +12,8 @@ * An instance of this class is automatically created by default, and can be found at renderer.plugins.prepare * * @class - * @memberof PIXI + * @extends PIXI.prepare.BasePrepare + * @memberof PIXI.prepare */ export default class CanvasPrepare extends BasePrepare { diff --git a/src/prepare/webgl/WebGLPrepare.js b/src/prepare/webgl/WebGLPrepare.js index 1ef7467..eb7023f 100644 --- a/src/prepare/webgl/WebGLPrepare.js +++ b/src/prepare/webgl/WebGLPrepare.js @@ -7,7 +7,8 @@ * An instance of this class is automatically created by default, and can be found at renderer.plugins.prepare * * @class - * @memberof PIXI + * @extends PIXI.prepare.BasePrepare + * @memberof PIXI.prepare */ export default class WebGLPrepare extends BasePrepare { diff --git a/test/core/Circle.js b/test/core/Circle.js index 183f98f..f621992 100644 --- a/test/core/Circle.js +++ b/test/core/Circle.js @@ -53,6 +53,10 @@ expect(circ1.contains(10, 16)).to.be.false; expect(circ1.contains(11, 15)).to.be.false; expect(circ1.contains(0, 0)).to.be.false; + + const circ2 = new PIXI.Circle(10, 10, 0); + + expect(circ2.contains(10, 10)).to.be.false; }); it('should return framing rectangle', function () diff --git a/test/core/Container.js b/test/core/Container.js index 8a10b40..cefde54 100644 --- a/test/core/Container.js +++ b/test/core/Container.js @@ -1,5 +1,39 @@ 'use strict'; +function testAddChild(fn) +{ + return function () + { + fn(function (container, obj) + { + container.addChild(obj); + }); + fn(function (container, obj) + { + container.addChildAt(obj); + }); + }; +} + +function testRemoveChild(fn) +{ + return function () + { + 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 +104,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 +279,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/Ellipse.js b/test/core/Ellipse.js index f2e8234..3f47442 100644 --- a/test/core/Ellipse.js +++ b/test/core/Ellipse.js @@ -56,6 +56,10 @@ expect(ellipse1.contains(10, 16)).to.be.false; expect(ellipse1.contains(11, 15)).to.be.false; expect(ellipse1.contains(0, 0)).to.be.false; + + const ellipse2 = new PIXI.Ellipse(10, 10, 0, 0); + + expect(ellipse2.contains(10, 10)).to.be.false; }); it('should return framing rectangle', function () diff --git a/test/core/Graphics.js b/test/core/Graphics.js index 2a64946..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 () @@ -127,6 +130,16 @@ expect(graphics.containsPoint(point)).to.be.false; }); + + it('should return false when no fill', function () + { + const point = new PIXI.Point(1, 1); + const graphics = new PIXI.Graphics(); + + graphics.drawRect(0, 0, 10, 10); + + expect(graphics.containsPoint(point)).to.be.false; + }); }); describe('arc', function () @@ -208,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/Matrix.js b/test/core/Matrix.js index 9d2694b..76bd57c 100644 --- a/test/core/Matrix.js +++ b/test/core/Matrix.js @@ -107,10 +107,60 @@ expect(m1.ty).to.equal(m2.ty); }); + it('should prepend matrix', function () + { + const m1 = new PIXI.Matrix(); + const m2 = new PIXI.Matrix(); + + m2.set(2, 3, 4, 5, 100, 200); + m1.prepend(m2); + + expect(m1.a).to.equal(m2.a); + expect(m1.b).to.equal(m2.b); + expect(m1.c).to.equal(m2.c); + expect(m1.d).to.equal(m2.d); + expect(m1.tx).to.equal(m2.tx); + expect(m1.ty).to.equal(m2.ty); + + const m3 = new PIXI.Matrix(); + const m4 = new PIXI.Matrix(); + + m3.prepend(m4); + + expect(m3.a).to.equal(m4.a); + expect(m3.b).to.equal(m4.b); + expect(m3.c).to.equal(m4.c); + expect(m3.d).to.equal(m4.d); + expect(m3.tx).to.equal(m4.tx); + expect(m3.ty).to.equal(m4.ty); + }); + it('should get IDENTITY and TEMP_MATRIX', function () { expect(PIXI.Matrix.IDENTITY instanceof PIXI.Matrix).to.be.true; expect(PIXI.Matrix.TEMP_MATRIX instanceof PIXI.Matrix).to.be.true; }); -}); + it('should reset matrix to default when identity() is called', function () + { + const matrix = new PIXI.Matrix(); + + matrix.set(2, 3, 4, 5, 100, 200); + + expect(matrix.a).to.equal(2); + expect(matrix.b).to.equal(3); + expect(matrix.c).to.equal(4); + expect(matrix.d).to.equal(5); + expect(matrix.tx).to.equal(100); + expect(matrix.ty).to.equal(200); + + matrix.identity(); + + expect(matrix.a).to.equal(1); + expect(matrix.b).to.equal(0); + expect(matrix.c).to.equal(0); + expect(matrix.d).to.equal(1); + expect(matrix.tx).to.equal(0); + expect(matrix.ty).to.equal(0); + }); +}); 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/Text.js b/test/core/Text.js index b8ce561..5799ad4 100644 --- a/test/core/Text.js +++ b/test/core/Text.js @@ -112,4 +112,42 @@ expect(text.width).to.equal(300); }); }); + + describe('text', function () + { + it('should convert numbers into strings', function () + { + const text = new PIXI.Text(2); + + expect(text.text).to.equal('2'); + }); + + it('should not change 0 to \'\'', function () + { + const text = new PIXI.Text(0); + + expect(text.text).to.equal('0'); + }); + + it('should prevent setting null', function () + { + const text = new PIXI.Text(null); + + expect(text.text).to.equal(' '); + }); + + it('should prevent setting undefined', function () + { + const text = new PIXI.Text(); + + expect(text.text).to.equal(' '); + }); + + it('should prevent setting \'\'', function () + { + const text = new PIXI.Text(''); + + expect(text.text).to.equal(' '); + }); + }); }); diff --git a/test/core/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/TilingSprite.js b/test/core/TilingSprite.js index 2e449a7..7694792 100644 --- a/test/core/TilingSprite.js +++ b/test/core/TilingSprite.js @@ -24,4 +24,25 @@ expect(bounds.height).to.equal(600); }); }); + + it('checks if tilingSprite contains a point', function () + { + const texture = new PIXI.Texture(new PIXI.BaseTexture()); + const tilingSprite = new PIXI.extras.TilingSprite(texture, 200, 300); + + expect(tilingSprite.containsPoint(new PIXI.Point(1, 1))).to.equal(true); + expect(tilingSprite.containsPoint(new PIXI.Point(300, 400))).to.equal(false); + }); + + it('gets and sets height and width correctly', function () + { + const texture = new PIXI.Texture(new PIXI.BaseTexture()); + const tilingSprite = new PIXI.extras.TilingSprite(texture, 200, 300); + + tilingSprite.width = 400; + tilingSprite.height = 600; + + expect(tilingSprite.width).to.equal(400); + expect(tilingSprite.height).to.equal(600); + }); }); diff --git a/test/core/WebGLRenderer.js b/test/core/WebGLRenderer.js new file mode 100644 index 0000000..3d6cf98 --- /dev/null +++ b/test/core/WebGLRenderer.js @@ -0,0 +1,21 @@ +'use strict'; + +const withGL = require('../withGL'); + +describe('PIXI.WebGLRenderer', function () +{ + it('setting option legacy should disable VAOs and SPRITE_MAX_TEXTURES', withGL(function () + { + const renderer = new PIXI.WebGLRenderer(1, 1, { legacy: true }); + + 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 1271fef..71ade15 100644 --- a/package.json +++ b/package.json @@ -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/scripts/release.js b/scripts/release.js index c4c8e36..156620c 100644 --- a/scripts/release.js +++ b/scripts/release.js @@ -12,7 +12,8 @@ 'dist/**/*', 'lib/**/*', 'src/**/*', - 'scripts/**/*', + 'scripts/*', + 'scripts/renders/*', 'test/**/*', '*.json', '*.md', diff --git a/scripts/renders/client.js b/scripts/renders/client.js index 16c9565..6faee34 100755 --- a/scripts/renders/client.js +++ b/scripts/renders/client.js @@ -1,6 +1,6 @@ 'use strict'; -require('../../bin/pixi'); +require('../../dist/pixi'); PIXI.utils.skipHello(); const fs = require('fs'); 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 bc730db..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. @@ -33,8 +33,11 @@ * need to call toDataUrl on the webgl context * @param {number} [options.resolution=1] - The resolution / device pixel ratio of the renderer, retina would be 2 * @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 @@ -43,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. */ @@ -73,7 +99,7 @@ */ stop() { - this.ticker.stop(); + this._ticker.stop(); } /** @@ -81,7 +107,7 @@ */ start() { - this.ticker.start(); + this._ticker.start(); } /** @@ -95,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 index 81e076b..78b17e6 100644 --- a/src/core/Shader.js +++ b/src/core/Shader.js @@ -1,9 +1,7 @@ import { GLShader } from 'pixi-gl-core'; import settings from './settings'; -const { PRECISION } = settings; - -function checkPrecision(src) +function checkPrecision(src, def) { if (src instanceof Array) { @@ -11,14 +9,14 @@ { const copy = src.slice(0); - copy.unshift(`precision ${PRECISION} float;`); + copy.unshift(`precision ${def} float;`); return copy; } } else if (src.substring(0, 9) !== 'precision') { - return `precision ${PRECISION} float;\n${src}`; + return `precision ${def} float;\n${src}`; } return src; @@ -42,6 +40,7 @@ */ constructor(gl, vertexSrc, fragmentSrc) { - super(gl, checkPrecision(vertexSrc), checkPrecision(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 3425b41..f9235e4 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -181,7 +181,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 b3792ef..17fde8d 100644 --- a/src/core/display/Container.js +++ b/src/core/display/Container.js @@ -73,13 +73,14 @@ } child.parent = this; - - // ensure a transform will be recalculated.. - this.transform._parentID = -1; - this._boundsID++; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.push(child); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(this.children.length - 1); child.emit('added', this); @@ -108,9 +109,14 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.splice(index, 0, child); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(index); child.emit('added', this); @@ -174,6 +180,7 @@ removeItems(this.children, currentIndex, 1); // remove from old position this.children.splice(index, 0, child); // add at new position + this.onChildrenChange(index); } @@ -220,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! @@ -244,9 +252,14 @@ { const child = this.getChildAt(index); + // ensure child transform will be recalculated.. child.parent = null; + child.transform._parentID = -1; removeItems(this.children, index, 1); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(index); child.emit('removed', this); @@ -275,8 +288,14 @@ for (let i = 0; i < removed.length; ++i) { removed[i].parent = null; + if (removed[i].transform) + { + removed[i].transform._parentID = -1; + } } + this._boundsID++; + this.onChildrenChange(beginIndex); for (let i = 0; i < removed.length; ++i) diff --git a/src/core/display/DisplayObject.js b/src/core/display/DisplayObject.js index 3affe36..4e6c77d 100644 --- a/src/core/display/DisplayObject.js +++ b/src/core/display/DisplayObject.js @@ -109,7 +109,7 @@ /** * The original, cached mask of the object * - * @member {PIXI.Rectangle} + * @member {PIXI.Graphics|PIXI.Sprite} * @private */ this._mask = null; diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index 95f2af4..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 ); @@ -1065,10 +1061,15 @@ canvasRenderer = new CanvasRenderer(); } - tempMatrix.tx = -bounds.x; - tempMatrix.ty = -bounds.y; + this.transform.updateLocalTransform(); + this.transform.localTransform.copy(tempMatrix); - canvasRenderer.render(this, canvasBuffer, false, tempMatrix); + tempMatrix.invert(); + + tempMatrix.tx -= bounds.x; + tempMatrix.ty -= bounds.y; + + canvasRenderer.render(this, canvasBuffer, true, tempMatrix); const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode); 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 e67680f..867dde2 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/buildCircle.js b/src/core/graphics/webgl/utils/buildCircle.js index 0400d13..abf5d02 100644 --- a/src/core/graphics/webgl/utils/buildCircle.js +++ b/src/core/graphics/webgl/utils/buildCircle.js @@ -33,6 +33,11 @@ height = circleData.height; } + if (width === 0 || height === 0) + { + return; + } + const totalSegs = Math.floor(30 * Math.sqrt(circleData.radius)) || Math.floor(15 * Math.sqrt(circleData.width + circleData.height)); diff --git a/src/core/graphics/webgl/utils/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 90155f9..f03fe15 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -30,6 +30,7 @@ export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; export { default as CanvasGraphicsRenderer } from './graphics/canvas/CanvasGraphicsRenderer'; +export { default as Spritesheet } from './textures/Spritesheet'; export { default as Texture } from './textures/Texture'; export { default as BaseTexture } from './textures/BaseTexture'; export { default as RenderTexture } from './textures/RenderTexture'; diff --git a/src/core/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 ccce680..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); } /** @@ -162,6 +162,9 @@ if (transform) { transform.copy(tempWt); + + // lets not forget to flag the parent transform as dirty... + this._tempDisplayObjectParent.transform._worldID = -1; } else { @@ -169,6 +172,7 @@ } displayObject.parent = this._tempDisplayObjectParent; + displayObject.updateTransform(); displayObject.parent = cacheParent; // displayObject.hitArea = //TODO add a temp hit area @@ -279,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/canvas/utils/CanvasRenderTarget.js b/src/core/renderers/canvas/utils/CanvasRenderTarget.js index 3de0aef..b409891 100644 --- a/src/core/renderers/canvas/utils/CanvasRenderTarget.js +++ b/src/core/renderers/canvas/utils/CanvasRenderTarget.js @@ -1,5 +1,4 @@ import settings from '../../../settings'; -const { RESOLUTION } = settings; /** * Creates a Canvas element of the given size. @@ -30,7 +29,7 @@ */ this.context = this.canvas.getContext('2d'); - this.resolution = resolution || RESOLUTION; + this.resolution = resolution || settings.RESOLUTION; this.resize(width, height); } 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 ce0e1c4..788ed6c 100644 --- a/src/core/renderers/webgl/WebGLRenderer.js +++ b/src/core/renderers/webgl/WebGLRenderer.js @@ -30,8 +30,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 @@ -49,10 +49,19 @@ * enable this if you need to call toDataUrl on the webgl context. * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when * rendering, stopping pixel interpolation. + * @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; + + if (this.legacy) + { + glCore.VertexArrayObject.FORCE_NATIVE = true; + } /** * The type of this renderer as a standardised const @@ -229,7 +238,7 @@ this.emit('context', gl); // setup the width/height properties and gl viewport - this.resize(this.width, this.height); + this.resize(this.screen.width, this.screen.height); } /** @@ -323,16 +332,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) { @@ -376,6 +385,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 @@ -440,9 +469,10 @@ * Changes the current shader to the one given in parameter * * @param {PIXI.Shader} shader - the new shader + * @param {boolean} [autoProject=true] - Whether automatically set the projection matrix * @return {PIXI.WebGLRenderer} Returns itself. */ - bindShader(shader) + bindShader(shader, autoProject) { // TODO cache if (this._activeShader !== shader) @@ -450,8 +480,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 bbab3b3..944f41d 100644 --- a/src/core/renderers/webgl/filters/Filter.js +++ b/src/core/renderers/webgl/filters/Filter.js @@ -1,6 +1,7 @@ import extractUniformsFromSrc from './extractUniformsFromSrc'; import { uid } from '../../../utils'; import { BLEND_MODES } from '../../../const'; +import settings from '../../../settings'; const SOURCE_KEY_MAP = {}; @@ -10,7 +11,7 @@ * @memberof PIXI * @extends PIXI.Shader */ -class Filter +export default class Filter { /** * @param {string} [vertexSrc] - The source of the vertex shader. @@ -35,8 +36,6 @@ this.blendMode = BLEND_MODES.NORMAL; - // pull out the vertex and shader uniforms if they are not specified.. - // currently this does not extract structs only default types this.uniformData = uniforms || extractUniformsFromSrc(this.vertexSrc, this.fragmentSrc, 'projectionMatrix|uSampler'); /** @@ -80,7 +79,7 @@ * * @member {number} */ - this.resolution = 1; + this.resolution = settings.RESOLUTION; /** * If enabled is true the filter is applied, if false it will not. @@ -97,8 +96,11 @@ * @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 { // --- // // this.uniforms.filterMatrix = filterManager.calculateSpriteMatrix(tempMatrix, window.panda ); @@ -170,5 +172,3 @@ ].join('\n'); } } - -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 dc7072a..6c7422d 100644 --- a/src/core/renderers/webgl/managers/FilterManager.js +++ b/src/core/renderers/webgl/managers/FilterManager.js @@ -154,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 @@ -176,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; @@ -184,7 +184,7 @@ flop = t; } - filters[i].apply(this, flip, lastState.renderTarget, true); + filters[i].apply(this, flip, lastState.renderTarget, false, currentState); this.freePotRenderTarget(flip); this.freePotRenderTarget(flop); @@ -291,9 +291,13 @@ let textureCount = 1; let currentState; - if (shader.uniforms.data.filterArea) + // 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; @@ -306,9 +310,9 @@ // use this to clamp displaced texture coords so they belong to filterArea // see displacementFilter fragment shader for an example - if (shader.uniforms.data.filterClamp) + if (shader.uniforms.filterClamp) { - currentState = this.filterData.stack[this.filterData.index]; + currentState = currentState || this.filterData.stack[this.filterData.index]; const filterClamp = shader.uniforms.filterClamp; @@ -485,7 +489,7 @@ */ destroy() { - this.shaderCache = []; + this.shaderCache = {}; this.emptyPool(); } diff --git a/src/core/renderers/webgl/utils/RenderTarget.js b/src/core/renderers/webgl/utils/RenderTarget.js index afe51ab..6d2467f 100644 --- a/src/core/renderers/webgl/utils/RenderTarget.js +++ b/src/core/renderers/webgl/utils/RenderTarget.js @@ -124,7 +124,7 @@ * @default PIXI.settings.SCALE_MODE * @see PIXI.SCALE_MODES */ - this.scaleMode = scaleMode || settings.SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; /** * Whether this object is the root element or not 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 36ea5bb..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; @@ -346,8 +346,8 @@ /** * Gets the local bounds of the sprite object. * - * @param {Rectangle} rect - The output rectangle. - * @return {Rectangle} The bounds. + * @param {PIXI.Rectangle} rect - The output rectangle. + * @return {PIXI.Rectangle} The bounds. */ getLocalBounds(rect) { @@ -440,7 +440,7 @@ * * @static * @param {number|string|PIXI.BaseTexture|HTMLCanvasElement|HTMLVideoElement} source Source to create texture from - * @return {PIXI.Texture} 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 e77b39a..90b08c0 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++) @@ -103,11 +102,18 @@ { const gl = this.renderer.gl; - // step 1: first check max textures the GPU can handle. - this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); + if (this.renderer.legacy) + { + this.MAX_TEXTURES = 1; + } + else + { + // step 1: first check max textures the GPU can handle. + this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); - // step 2: check the maximum number of if statements the shader can have too.. - this.MAX_TEXTURES = checkMaxIfStatmentsInShader(this.MAX_TEXTURES, gl); + // step 2: check the maximum number of if statements the shader can have too.. + this.MAX_TEXTURES = checkMaxIfStatmentsInShader(this.MAX_TEXTURES, gl); + } const shader = this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); @@ -130,8 +136,12 @@ .addIndex(this.indexBuffer) .addAttribute(this.vertexBuffers[i], shader.attributes.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) .addAttribute(this.vertexBuffers[i], shader.attributes.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) - .addAttribute(this.vertexBuffers[i], shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4) - .addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + .addAttribute(this.vertexBuffers[i], shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); + + if (shader.attributes.aTextureId) + { + this.vaos[i].addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + } /* eslint-enable max-len */ } @@ -378,8 +388,12 @@ .addIndex(this.indexBuffer) .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) - .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4) - .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); + + if (this.shader.attributes.aTextureId) + { + this.vaos[this.vertexCount].addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + } /* eslint-enable max-len */ } diff --git a/src/core/text/Text.js b/src/core/text/Text.js index 013625a..cec8c32 100644 --- a/src/core/text/Text.js +++ b/src/core/text/Text.js @@ -6,6 +6,7 @@ import { TEXT_GRADIENT } from '../const'; import settings from '../settings'; import TextStyle from './TextStyle'; +import trimCanvas from '../utils/trimCanvas'; const defaultDestroyOptions = { texture: true, @@ -57,7 +58,7 @@ /** * The canvas 2d context that everything is drawn with - * @member {HTMLCanvasElement} + * @member {CanvasRenderingContext2D} */ this.context = this.canvas.getContext('2d'); @@ -189,10 +190,12 @@ if (style.dropShadow) { + this.context.shadowBlur = style.dropShadowBlur; + this.context.globalAlpha = style.dropShadowAlpha; + if (style.dropShadowBlur > 0) { this.context.shadowColor = style.dropShadowColor; - this.context.shadowBlur = style.dropShadowBlur; } else { @@ -237,6 +240,10 @@ } } + // reset the shadow blur and alpha that was set by the drop shadow, for the regular text + this.context.shadowBlur = 0; + this.context.globalAlpha = 1; + // set canvas text styles this.context.fillStyle = this._generateFillStyle(style, lines); @@ -326,6 +333,15 @@ */ updateTexture() { + if (this._style.trim) + { + const trimmed = trimCanvas(this.canvas); + + this.canvas.width = trimmed.width; + this.canvas.height = trimmed.height; + this.context.putImageData(trimmed.data, 0, 0); + } + const texture = this._texture; const style = this._style; @@ -475,6 +491,19 @@ } /** + * Gets the local bounds of the text object. + * + * @param {Rectangle} rect - The output rectangle. + * @return {Rectangle} The bounds. + */ + getLocalBounds(rect) + { + this.updateText(true); + + return super.getLocalBounds.call(this, rect); + } + + /** * calculates the bounds of the Text as a rectangle. The bounds calculation takes the worldTransform into account. */ _calculateBounds() @@ -525,6 +554,29 @@ const width = this.canvas.width / this.resolution; const height = this.canvas.height / this.resolution; + // make a copy of the style settings, so we can manipulate them later + const fill = style.fill.slice(); + const fillGradientStops = style.fillGradientStops.slice(); + + // wanting to evenly distribute the fills. So an array of 4 colours should give fills of 0.25, 0.5 and 0.75 + if (!fillGradientStops.length) + { + const lengthPlus1 = fill.length + 1; + + for (let i = 1; i < lengthPlus1; ++i) + { + fillGradientStops.push(i / lengthPlus1); + } + } + + // stop the bleeding of the last gradient on the line above to the top gradient of the this line + // by hard defining the first gradient colour at point 0, and last gradient colour at point 1 + fill.unshift(style.fill[0]); + fillGradientStops.unshift(0); + + fill.push(style.fill[style.fill.length - 1]); + fillGradientStops.push(1); + if (style.fillGradientType === TEXT_GRADIENT.LINEAR_VERTICAL) { // start the gradient at the top center of the canvas, and end at the bottom middle of the canvas @@ -532,15 +584,22 @@ // we need to repeat the gradient so that each individual line of text has the same vertical gradient effect // ['#FF0000', '#00FF00', '#0000FF'] over 2 lines would create stops at 0.125, 0.25, 0.375, 0.625, 0.75, 0.875 - totalIterations = (style.fill.length + 1) * lines.length; + totalIterations = (fill.length + 1) * lines.length; currentIteration = 0; for (let i = 0; i < lines.length; i++) { currentIteration += 1; - for (let j = 0; j < style.fill.length; j++) + for (let j = 0; j < fill.length; j++) { - stop = (currentIteration / totalIterations); - gradient.addColorStop(stop, style.fill[j]); + if (fillGradientStops[j]) + { + stop = (fillGradientStops[j] / lines.length) + (i / lines.length); + } + else + { + stop = currentIteration / totalIterations; + } + gradient.addColorStop(stop, fill[j]); currentIteration++; } } @@ -552,13 +611,20 @@ // can just evenly space out the gradients in this case, as multiple lines makes no difference // to an even left to right gradient - totalIterations = style.fill.length + 1; + totalIterations = fill.length + 1; currentIteration = 1; - for (let i = 0; i < style.fill.length; i++) + for (let i = 0; i < fill.length; i++) { - stop = currentIteration / totalIterations; - gradient.addColorStop(stop, style.fill[i]); + if (fillGradientStops[i]) + { + stop = fillGradientStops[i]; + } + else + { + stop = currentIteration / totalIterations; + } + gradient.addColorStop(stop, fill[i]); currentIteration++; } } @@ -569,7 +635,7 @@ /** * Destroys this text object. * Note* Unlike a Sprite, a Text object will automatically destroy its baseTexture and texture as - * the majorety of the time the texture will not be shared with any other Sprites. + * the majority of the time the texture will not be shared with any other Sprites. * * @param {object|boolean} [options] - Options parameter. A boolean will act as if all options * have been set to that value @@ -680,7 +746,7 @@ set text(text) // eslint-disable-line require-jsdoc { - text = String(text || ' '); + text = String(text === '' || text === null || text === undefined ? ' ' : text); if (this._text === text) { diff --git a/src/core/text/TextStyle.js b/src/core/text/TextStyle.js index 3b25528..890950b 100644 --- a/src/core/text/TextStyle.js +++ b/src/core/text/TextStyle.js @@ -8,12 +8,14 @@ align: 'left', breakWords: false, dropShadow: false, + dropShadowAlpha: 1, dropShadowAngle: Math.PI / 6, dropShadowBlur: 0, dropShadowColor: '#000000', dropShadowDistance: 5, fill: 'black', fillGradientType: TEXT_GRADIENT.LINEAR_VERTICAL, + fillGradientStops: [], fontFamily: 'Arial', fontSize: 26, fontStyle: 'normal', @@ -27,6 +29,7 @@ stroke: 'black', strokeThickness: 0, textBaseline: 'alphabetic', + trim: false, wordWrap: false, wordWrapWidth: 100, }; @@ -47,6 +50,7 @@ * @param {boolean} [style.breakWords=false] - Indicates if lines can be wrapped within words, it * needs wordWrap to be set to true * @param {boolean} [style.dropShadow=false] - Set a drop shadow for the text + * @param {number} [style.dropShadowAlpha=1] - Set alpha for the drop shadow * @param {number} [style.dropShadowAngle=Math.PI/6] - Set a angle of the drop shadow * @param {number} [style.dropShadowBlur=0] - Set a shadow blur radius * @param {string} [style.dropShadowColor='#000000'] - A fill style to be used on the dropshadow e.g 'red', '#00FF00' @@ -55,8 +59,10 @@ * fillstyle that will be used on the text e.g 'red', '#00FF00'. Can be an array to create a gradient * eg ['#000000','#FFFFFF'] * {@link https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/fillStyle|MDN} - * @param {number} [style.fillGradientType=PIXI.TEXT_GRADIENT.LINEAR_VERTICAL] - If fills styles are - * supplied, this can change the type/direction of the gradient. See {@link PIXI.TEXT_GRADIENT} for possible values + * @param {number} [style.fillGradientType=PIXI.TEXT_GRADIENT.LINEAR_VERTICAL] - If fill is an array of colours + * to create a gradient, this can change the type/direction of the gradient. See {@link PIXI.TEXT_GRADIENT} + * @param {number[]} [style.fillGradientStops] - If fill is an array of colours to create a gradient, this array can set + * the stop points (numbers between 0 and 1) for the color, overriding the default behaviour of evenly spacing them. * @param {string|string[]} [style.fontFamily='Arial'] - The font family * @param {number|string} [style.fontSize=26] - The font size (as a number it converts to px, but as a string, * equivalents are '26px','20pt','160%' or '1.6em') @@ -76,6 +82,7 @@ * e.g 'blue', '#FCFF00' * @param {number} [style.strokeThickness=0] - A number that represents the thickness of the stroke. * Default is 0 (no stroke) + * @param {boolean} [style.trim=false] - Trim transparent borders * @param {string} [style.textBaseline='alphabetic'] - The baseline of the text that is rendered. * @param {boolean} [style.wordWrap=false] - Indicates if word wrap should be used * @param {number} [style.wordWrapWidth=100] - The width at which text will wrap, it needs wordWrap to be set to true @@ -152,6 +159,19 @@ } } + get dropShadowAlpha() + { + return this._dropShadowAlpha; + } + set dropShadowAlpha(dropShadowAlpha) + { + if (this._dropShadowAlpha !== dropShadowAlpha) + { + this._dropShadowAlpha = dropShadowAlpha; + this.styleID++; + } + } + get dropShadowAngle() { return this._dropShadowAngle; @@ -232,6 +252,19 @@ } } + get fillGradientStops() + { + return this._fillGradientStops; + } + set fillGradientStops(fillGradientStops) + { + if (!areArraysEqual(this._fillGradientStops,fillGradientStops)) + { + this._fillGradientStops = fillGradientStops; + this.styleID++; + } + } + get fontFamily() { return this._fontFamily; @@ -402,6 +435,19 @@ } } + get trim() + { + return this._trim; + } + set trim(trim) + { + if (this._trim !== trim) + { + this._trim = trim; + this.styleID++; + } + } + get wordWrap() { return this._wordWrap; @@ -475,3 +521,34 @@ return color; } } + +/** + * Utility function to convert hexadecimal colors to strings, and simply return the color if it's a string. + * This version can also convert array of colors + * + * @param {Array} array1 First array to compare + * @param {Array} array2 Second array to compare + * @return {boolean} Do the arrays contain the same values in the same order + */ +function areArraysEqual(array1, array2) +{ + if (!Array.isArray(array1) || !Array.isArray(array2)) + { + return false; + } + + if (array1.length !== array2.length) + { + return false; + } + + for (let i = 0; i < array1.length; ++i) + { + if (array1[i] !== array2[i]) + { + return false; + } + } + + return true; +} diff --git a/src/core/textures/BaseRenderTexture.js b/src/core/textures/BaseRenderTexture.js index 5d50146..aa727f0 100644 --- a/src/core/textures/BaseRenderTexture.js +++ b/src/core/textures/BaseRenderTexture.js @@ -1,8 +1,6 @@ import BaseTexture from './BaseTexture'; import settings from '../settings'; -const { RESOLUTION, SCALE_MODE } = settings; - /** * A BaseRenderTexture is a special texture that allows any Pixi display object to be rendered to it. * @@ -54,7 +52,7 @@ { super(null, scaleMode); - this.resolution = resolution || RESOLUTION; + this.resolution = resolution || settings.RESOLUTION; this.width = width; this.height = height; @@ -62,7 +60,7 @@ this.realWidth = this.width * this.resolution; this.realHeight = this.height * this.resolution; - this.scaleMode = scaleMode || SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; this.hasLoaded = true; /** diff --git a/src/core/textures/BaseTexture.js b/src/core/textures/BaseTexture.js index 676dca3..aeccf39 100644 --- a/src/core/textures/BaseTexture.js +++ b/src/core/textures/BaseTexture.js @@ -77,7 +77,7 @@ * @default PIXI.settings.SCALE_MODE * @see PIXI.SCALE_MODES */ - this.scaleMode = scaleMode || settings.SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; /** * Set to true once the base texture has successfully loaded. @@ -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/RenderTexture.js b/src/core/textures/RenderTexture.js index 4c5034f..b3c6297 100644 --- a/src/core/textures/RenderTexture.js +++ b/src/core/textures/RenderTexture.js @@ -54,8 +54,8 @@ /* eslint-disable prefer-rest-params, no-console */ const width = arguments[1]; const height = arguments[2]; - const scaleMode = arguments[3] || 0; - const resolution = arguments[4] || 1; + const scaleMode = arguments[3]; + const resolution = arguments[4]; // we have an old render texture.. console.warn(`Please use RenderTexture.create(${width}, ${height}) instead of the ctor directly.`); 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 ca31456..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 @@ -459,7 +497,9 @@ if (frame.x + frame.width > this.baseTexture.width || frame.y + frame.height > this.baseTexture.height) { - throw new Error(`Texture Error: frame does not fit inside the base Texture dimensions ${this}`); + throw new Error('Texture Error: frame does not fit inside the base Texture dimensions: ' + + `X: ${frame.x} + ${frame.width} > ${this.baseTexture.width} ` + + `Y: ${frame.y} + ${frame.height} > ${this.baseTexture.height}`); } // this.valid = frame && frame.width && frame.height && this.baseTexture.source && this.baseTexture.hasLoaded; @@ -520,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. @@ -528,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 2a7497b..99e585a 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)); } /** @@ -360,3 +362,43 @@ * @private */ export const BaseTextureCache = {}; + +/** + * 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/core/utils/trimCanvas.js b/src/core/utils/trimCanvas.js new file mode 100644 index 0000000..5af6de0 --- /dev/null +++ b/src/core/utils/trimCanvas.js @@ -0,0 +1,83 @@ +/** + * Trim transparent borders from a canvas + * + * @memberof PIXI + * @function trimCanvas + * @private + * @param {HTMLCanvasElement} canvas - the canvas to trim + * @returns {object} Trim data + */ +export default function trimCanvas(canvas) +{ + // https://gist.github.com/remy/784508 + + let width = canvas.width; + let height = canvas.height; + + const context = canvas.getContext('2d'); + const imageData = context.getImageData(0, 0, width, height); + const pixels = imageData.data; + const len = pixels.length; + + const bound = { + top: null, + left: null, + right: null, + bottom: null, + }; + let i; + let x; + let y; + + for (i = 0; i < len; i += 4) + { + if (pixels[i + 3] !== 0) + { + x = (i / 4) % width; + y = ~~((i / 4) / width); + + if (bound.top === null) + { + bound.top = y; + } + + if (bound.left === null) + { + bound.left = x; + } + else if (x < bound.left) + { + bound.left = x; + } + + if (bound.right === null) + { + bound.right = x + 1; + } + else if (bound.right < x) + { + bound.right = x + 1; + } + + if (bound.bottom === null) + { + bound.bottom = y; + } + else if (bound.bottom < y) + { + bound.bottom = y; + } + } + } + + width = bound.right - bound.left; + height = bound.bottom - bound.top + 1; + + const data = context.getImageData(bound.left, bound.top, width, height); + + return { + height, + width, + data, + }; +} diff --git a/src/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/extras/TilingSprite.js b/src/extras/TilingSprite.js index f90d248..fe82451 100644 --- a/src/extras/TilingSprite.js +++ b/src/extras/TilingSprite.js @@ -220,20 +220,33 @@ transform.tx * resolution, transform.ty * resolution); - // TODO - this should be rolled into the setTransform above.. - context.scale(this.tileScale.x / baseTextureResolution, this.tileScale.y / baseTextureResolution); - - context.translate(modX + (this.anchor.x * -this._width), - modY + (this.anchor.y * -this._height)); - renderer.setBlendMode(this.blendMode); // fill the pattern! context.fillStyle = this._canvasPattern; - context.fillRect(-modX, - -modY, - this._width / this.tileScale.x * baseTextureResolution, - this._height / this.tileScale.y * baseTextureResolution); + + // TODO - this should be rolled into the setTransform above.. + context.scale(this.tileScale.x / baseTextureResolution, this.tileScale.y / baseTextureResolution); + + const anchorX = this.anchor.x * -this._width; + const anchorY = this.anchor.y * -this._height; + + if (this.uvRespectAnchor) + { + context.translate(modX, modY); + + context.fillRect(-modX + anchorX, -modY + anchorY, + this._width / this.tileScale.x * baseTextureResolution, + this._height / this.tileScale.y * baseTextureResolution); + } + else + { + context.translate(modX + anchorX, modY + anchorY); + + context.fillRect(-modX, -modY, + this._width / this.tileScale.x * baseTextureResolution, + this._height / this.tileScale.y * baseTextureResolution); + } } /** diff --git a/src/extras/cacheAsBitmap.js b/src/extras/cacheAsBitmap.js index 18043a7..5dd5640 100644 --- a/src/extras/cacheAsBitmap.js +++ b/src/extras/cacheAsBitmap.js @@ -41,6 +41,9 @@ * provide a performance benefit for complex static displayObjects. * To remove simply set this property to 'false' * + * IMPORTANT GOTCHA - make sure that all your textures are preloaded BEFORE setting this property to true + * as it will take a snapshot of what is currently there. If the textures have not loaded then they will not appear. + * * @member {boolean} * @memberof PIXI.DisplayObject# */ diff --git a/src/filters/blur/BlurFilter.js b/src/filters/blur/BlurFilter.js index 340fc58..1575db9 100644 --- a/src/filters/blur/BlurFilter.js +++ b/src/filters/blur/BlurFilter.js @@ -24,10 +24,9 @@ this.blurXFilter = new BlurXFilter(strength, quality, resolution, kernelSize); this.blurYFilter = new BlurYFilter(strength, quality, resolution, kernelSize); - this.resolution = 1; this.padding = 0; - this.resolution = resolution || 1; + this.resolution = resolution || core.settings.RESOLUTION; this.quality = quality || 4; this.blur = strength || 8; } diff --git a/src/filters/blur/BlurXFilter.js b/src/filters/blur/BlurXFilter.js index def99c3..0f48889 100644 --- a/src/filters/blur/BlurXFilter.js +++ b/src/filters/blur/BlurXFilter.js @@ -31,7 +31,7 @@ fragSrc ); - this.resolution = resolution || 1; + this.resolution = resolution || core.settings.RESOLUTION; this._quality = 0; diff --git a/src/filters/blur/BlurYFilter.js b/src/filters/blur/BlurYFilter.js index 638e0f0..6d489c7 100644 --- a/src/filters/blur/BlurYFilter.js +++ b/src/filters/blur/BlurYFilter.js @@ -31,7 +31,7 @@ fragSrc ); - this.resolution = resolution || 1; + this.resolution = resolution || core.settings.RESOLUTION; this._quality = 0; diff --git a/src/filters/colormatrix/ColorMatrixFilter.js b/src/filters/colormatrix/ColorMatrixFilter.js index 8509505..679f8ec 100644 --- a/src/filters/colormatrix/ColorMatrixFilter.js +++ b/src/filters/colormatrix/ColorMatrixFilter.js @@ -75,28 +75,28 @@ out[1] = (a[0] * b[1]) + (a[1] * b[6]) + (a[2] * b[11]) + (a[3] * b[16]); out[2] = (a[0] * b[2]) + (a[1] * b[7]) + (a[2] * b[12]) + (a[3] * b[17]); out[3] = (a[0] * b[3]) + (a[1] * b[8]) + (a[2] * b[13]) + (a[3] * b[18]); - out[4] = (a[0] * b[4]) + (a[1] * b[9]) + (a[2] * b[14]) + (a[3] * b[19]); + out[4] = (a[0] * b[4]) + (a[1] * b[9]) + (a[2] * b[14]) + (a[3] * b[19]) + a[4]; // Green Channel out[5] = (a[5] * b[0]) + (a[6] * b[5]) + (a[7] * b[10]) + (a[8] * b[15]); out[6] = (a[5] * b[1]) + (a[6] * b[6]) + (a[7] * b[11]) + (a[8] * b[16]); out[7] = (a[5] * b[2]) + (a[6] * b[7]) + (a[7] * b[12]) + (a[8] * b[17]); out[8] = (a[5] * b[3]) + (a[6] * b[8]) + (a[7] * b[13]) + (a[8] * b[18]); - out[9] = (a[5] * b[4]) + (a[6] * b[9]) + (a[7] * b[14]) + (a[8] * b[19]); + out[9] = (a[5] * b[4]) + (a[6] * b[9]) + (a[7] * b[14]) + (a[8] * b[19]) + a[9]; // Blue Channel out[10] = (a[10] * b[0]) + (a[11] * b[5]) + (a[12] * b[10]) + (a[13] * b[15]); out[11] = (a[10] * b[1]) + (a[11] * b[6]) + (a[12] * b[11]) + (a[13] * b[16]); out[12] = (a[10] * b[2]) + (a[11] * b[7]) + (a[12] * b[12]) + (a[13] * b[17]); out[13] = (a[10] * b[3]) + (a[11] * b[8]) + (a[12] * b[13]) + (a[13] * b[18]); - out[14] = (a[10] * b[4]) + (a[11] * b[9]) + (a[12] * b[14]) + (a[13] * b[19]); + out[14] = (a[10] * b[4]) + (a[11] * b[9]) + (a[12] * b[14]) + (a[13] * b[19]) + a[14]; // Alpha Channel out[15] = (a[15] * b[0]) + (a[16] * b[5]) + (a[17] * b[10]) + (a[18] * b[15]); out[16] = (a[15] * b[1]) + (a[16] * b[6]) + (a[17] * b[11]) + (a[18] * b[16]); out[17] = (a[15] * b[2]) + (a[16] * b[7]) + (a[17] * b[12]) + (a[18] * b[17]); out[18] = (a[15] * b[3]) + (a[16] * b[8]) + (a[17] * b[13]) + (a[18] * b[18]); - out[19] = (a[15] * b[4]) + (a[16] * b[9]) + (a[17] * b[14]) + (a[18] * b[19]); + out[19] = (a[15] * b[4]) + (a[16] * b[9]) + (a[17] * b[14]) + (a[18] * b[19]) + a[19]; return out; } diff --git a/src/filters/colormatrix/colorMatrix.frag b/src/filters/colormatrix/colorMatrix.frag index 3aaf2b3..c73c0e9 100644 --- a/src/filters/colormatrix/colorMatrix.frag +++ b/src/filters/colormatrix/colorMatrix.frag @@ -4,32 +4,38 @@ void main(void) { - vec4 c = texture2D(uSampler, vTextureCoord); + // Un-premultiply alpha before applying the color matrix. See issue #3539. + if (c.a > 0.0) { + c.rgb /= c.a; + } + vec4 result; + result.r = (m[0] * c.r); + result.r += (m[1] * c.g); + result.r += (m[2] * c.b); + result.r += (m[3] * c.a); + result.r += m[4]; - gl_FragColor.r = (m[0] * c.r); - gl_FragColor.r += (m[1] * c.g); - gl_FragColor.r += (m[2] * c.b); - gl_FragColor.r += (m[3] * c.a); - gl_FragColor.r += m[4] * c.a; + result.g = (m[5] * c.r); + result.g += (m[6] * c.g); + result.g += (m[7] * c.b); + result.g += (m[8] * c.a); + result.g += m[9]; - gl_FragColor.g = (m[5] * c.r); - gl_FragColor.g += (m[6] * c.g); - gl_FragColor.g += (m[7] * c.b); - gl_FragColor.g += (m[8] * c.a); - gl_FragColor.g += m[9] * c.a; + result.b = (m[10] * c.r); + result.b += (m[11] * c.g); + result.b += (m[12] * c.b); + result.b += (m[13] * c.a); + result.b += m[14]; - gl_FragColor.b = (m[10] * c.r); - gl_FragColor.b += (m[11] * c.g); - gl_FragColor.b += (m[12] * c.b); - gl_FragColor.b += (m[13] * c.a); - gl_FragColor.b += m[14] * c.a; + result.a = (m[15] * c.r); + result.a += (m[16] * c.g); + result.a += (m[17] * c.b); + result.a += (m[18] * c.a); + result.a += m[19]; - gl_FragColor.a = (m[15] * c.r); - gl_FragColor.a += (m[16] * c.g); - gl_FragColor.a += (m[17] * c.b); - gl_FragColor.a += (m[18] * c.a); - gl_FragColor.a += m[19] * c.a; + // Premultiply alpha again. + result.rgb *= result.a; -// gl_FragColor = vec4(m[0]); + gl_FragColor = result; } diff --git a/src/filters/displacement/DisplacementFilter.js b/src/filters/displacement/DisplacementFilter.js index 112de0b..d5355c3 100644 --- a/src/filters/displacement/DisplacementFilter.js +++ b/src/filters/displacement/DisplacementFilter.js @@ -36,7 +36,7 @@ this.maskMatrix = maskMatrix; this.uniforms.mapSampler = sprite.texture; - this.uniforms.filterMatrix = maskMatrix.toArray(true); + this.uniforms.filterMatrix = maskMatrix; this.uniforms.scale = { x: 1, y: 1 }; if (scale === null || scale === undefined) diff --git a/src/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/InteractionData.js b/src/interaction/InteractionData.js index 02d53a3..818da35 100644 --- a/src/interaction/InteractionData.js +++ b/src/interaction/InteractionData.js @@ -21,9 +21,9 @@ this.global = new core.Point(); /** - * The target Sprite that was interacted with + * The target DisplayObject that was interacted with * - * @member {PIXI.Sprite} + * @member {PIXI.DisplayObject} */ this.target = null; @@ -33,6 +33,13 @@ * @member {Event} */ this.originalEvent = null; + + /** + * Unique identifier for this interaction + * + * @member {number} + */ + this.identifier = null; } /** diff --git a/src/interaction/InteractionEvent.js b/src/interaction/InteractionEvent.js index 75b2966..dc554f9 100644 --- a/src/interaction/InteractionEvent.js +++ b/src/interaction/InteractionEvent.js @@ -58,7 +58,7 @@ } /** - * Prevents event from reaching any objects other than the current object. + * Resets the event. * * @private */ diff --git a/src/interaction/InteractionManager.js b/src/interaction/InteractionManager.js index 7d51134..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/src/prepare/BasePrepare.js b/src/prepare/BasePrepare.js index a2005f5..09eb5e5 100644 --- a/src/prepare/BasePrepare.js +++ b/src/prepare/BasePrepare.js @@ -20,7 +20,7 @@ * * @abstract * @class - * @memberof PIXI + * @memberof PIXI.prepare */ export default class BasePrepare { @@ -107,9 +107,9 @@ /** * Upload all the textures and graphics to the GPU. * - * @param {Function|PIXI.DisplayObject|PIXI.Container} item - Either - * the container or display object to search for items to upload or - * the callback function, if items have been added using `prepare.add`. + * @param {Function|PIXI.DisplayObject|PIXI.Container|PIXI.BaseTexture|PIXI.Texture|PIXI.Graphics|PIXI.Text} item - + * Either the container or display object to search for items to upload, the items to upload themselves, + * or the callback function, if items have been added using `prepare.add`. * @param {Function} [done] - Optional callback when all queued uploads have completed */ upload(item, done) @@ -236,7 +236,8 @@ /** * Manually add an item to the uploading queue. * - * @param {PIXI.DisplayObject|PIXI.Container|*} item - Object to add to the queue + * @param {PIXI.DisplayObject|PIXI.Container|PIXI.BaseTexture|PIXI.Texture|PIXI.Graphics|PIXI.Text|*} item - Object to + * add to the queue * @return {PIXI.CanvasPrepare} Instance of plugin for chaining. */ add(item) diff --git a/src/prepare/canvas/CanvasPrepare.js b/src/prepare/canvas/CanvasPrepare.js index 4b99d22..0224bb5 100644 --- a/src/prepare/canvas/CanvasPrepare.js +++ b/src/prepare/canvas/CanvasPrepare.js @@ -12,7 +12,8 @@ * An instance of this class is automatically created by default, and can be found at renderer.plugins.prepare * * @class - * @memberof PIXI + * @extends PIXI.prepare.BasePrepare + * @memberof PIXI.prepare */ export default class CanvasPrepare extends BasePrepare { diff --git a/src/prepare/webgl/WebGLPrepare.js b/src/prepare/webgl/WebGLPrepare.js index 1ef7467..eb7023f 100644 --- a/src/prepare/webgl/WebGLPrepare.js +++ b/src/prepare/webgl/WebGLPrepare.js @@ -7,7 +7,8 @@ * An instance of this class is automatically created by default, and can be found at renderer.plugins.prepare * * @class - * @memberof PIXI + * @extends PIXI.prepare.BasePrepare + * @memberof PIXI.prepare */ export default class WebGLPrepare extends BasePrepare { diff --git a/test/core/Circle.js b/test/core/Circle.js index 183f98f..f621992 100644 --- a/test/core/Circle.js +++ b/test/core/Circle.js @@ -53,6 +53,10 @@ expect(circ1.contains(10, 16)).to.be.false; expect(circ1.contains(11, 15)).to.be.false; expect(circ1.contains(0, 0)).to.be.false; + + const circ2 = new PIXI.Circle(10, 10, 0); + + expect(circ2.contains(10, 10)).to.be.false; }); it('should return framing rectangle', function () diff --git a/test/core/Container.js b/test/core/Container.js index 8a10b40..cefde54 100644 --- a/test/core/Container.js +++ b/test/core/Container.js @@ -1,5 +1,39 @@ 'use strict'; +function testAddChild(fn) +{ + return function () + { + fn(function (container, obj) + { + container.addChild(obj); + }); + fn(function (container, obj) + { + container.addChildAt(obj); + }); + }; +} + +function testRemoveChild(fn) +{ + return function () + { + 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 +104,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 +279,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/Ellipse.js b/test/core/Ellipse.js index f2e8234..3f47442 100644 --- a/test/core/Ellipse.js +++ b/test/core/Ellipse.js @@ -56,6 +56,10 @@ expect(ellipse1.contains(10, 16)).to.be.false; expect(ellipse1.contains(11, 15)).to.be.false; expect(ellipse1.contains(0, 0)).to.be.false; + + const ellipse2 = new PIXI.Ellipse(10, 10, 0, 0); + + expect(ellipse2.contains(10, 10)).to.be.false; }); it('should return framing rectangle', function () diff --git a/test/core/Graphics.js b/test/core/Graphics.js index 2a64946..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 () @@ -127,6 +130,16 @@ expect(graphics.containsPoint(point)).to.be.false; }); + + it('should return false when no fill', function () + { + const point = new PIXI.Point(1, 1); + const graphics = new PIXI.Graphics(); + + graphics.drawRect(0, 0, 10, 10); + + expect(graphics.containsPoint(point)).to.be.false; + }); }); describe('arc', function () @@ -208,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/Matrix.js b/test/core/Matrix.js index 9d2694b..76bd57c 100644 --- a/test/core/Matrix.js +++ b/test/core/Matrix.js @@ -107,10 +107,60 @@ expect(m1.ty).to.equal(m2.ty); }); + it('should prepend matrix', function () + { + const m1 = new PIXI.Matrix(); + const m2 = new PIXI.Matrix(); + + m2.set(2, 3, 4, 5, 100, 200); + m1.prepend(m2); + + expect(m1.a).to.equal(m2.a); + expect(m1.b).to.equal(m2.b); + expect(m1.c).to.equal(m2.c); + expect(m1.d).to.equal(m2.d); + expect(m1.tx).to.equal(m2.tx); + expect(m1.ty).to.equal(m2.ty); + + const m3 = new PIXI.Matrix(); + const m4 = new PIXI.Matrix(); + + m3.prepend(m4); + + expect(m3.a).to.equal(m4.a); + expect(m3.b).to.equal(m4.b); + expect(m3.c).to.equal(m4.c); + expect(m3.d).to.equal(m4.d); + expect(m3.tx).to.equal(m4.tx); + expect(m3.ty).to.equal(m4.ty); + }); + it('should get IDENTITY and TEMP_MATRIX', function () { expect(PIXI.Matrix.IDENTITY instanceof PIXI.Matrix).to.be.true; expect(PIXI.Matrix.TEMP_MATRIX instanceof PIXI.Matrix).to.be.true; }); -}); + it('should reset matrix to default when identity() is called', function () + { + const matrix = new PIXI.Matrix(); + + matrix.set(2, 3, 4, 5, 100, 200); + + expect(matrix.a).to.equal(2); + expect(matrix.b).to.equal(3); + expect(matrix.c).to.equal(4); + expect(matrix.d).to.equal(5); + expect(matrix.tx).to.equal(100); + expect(matrix.ty).to.equal(200); + + matrix.identity(); + + expect(matrix.a).to.equal(1); + expect(matrix.b).to.equal(0); + expect(matrix.c).to.equal(0); + expect(matrix.d).to.equal(1); + expect(matrix.tx).to.equal(0); + expect(matrix.ty).to.equal(0); + }); +}); 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/Text.js b/test/core/Text.js index b8ce561..5799ad4 100644 --- a/test/core/Text.js +++ b/test/core/Text.js @@ -112,4 +112,42 @@ expect(text.width).to.equal(300); }); }); + + describe('text', function () + { + it('should convert numbers into strings', function () + { + const text = new PIXI.Text(2); + + expect(text.text).to.equal('2'); + }); + + it('should not change 0 to \'\'', function () + { + const text = new PIXI.Text(0); + + expect(text.text).to.equal('0'); + }); + + it('should prevent setting null', function () + { + const text = new PIXI.Text(null); + + expect(text.text).to.equal(' '); + }); + + it('should prevent setting undefined', function () + { + const text = new PIXI.Text(); + + expect(text.text).to.equal(' '); + }); + + it('should prevent setting \'\'', function () + { + const text = new PIXI.Text(''); + + expect(text.text).to.equal(' '); + }); + }); }); diff --git a/test/core/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/TilingSprite.js b/test/core/TilingSprite.js index 2e449a7..7694792 100644 --- a/test/core/TilingSprite.js +++ b/test/core/TilingSprite.js @@ -24,4 +24,25 @@ expect(bounds.height).to.equal(600); }); }); + + it('checks if tilingSprite contains a point', function () + { + const texture = new PIXI.Texture(new PIXI.BaseTexture()); + const tilingSprite = new PIXI.extras.TilingSprite(texture, 200, 300); + + expect(tilingSprite.containsPoint(new PIXI.Point(1, 1))).to.equal(true); + expect(tilingSprite.containsPoint(new PIXI.Point(300, 400))).to.equal(false); + }); + + it('gets and sets height and width correctly', function () + { + const texture = new PIXI.Texture(new PIXI.BaseTexture()); + const tilingSprite = new PIXI.extras.TilingSprite(texture, 200, 300); + + tilingSprite.width = 400; + tilingSprite.height = 600; + + expect(tilingSprite.width).to.equal(400); + expect(tilingSprite.height).to.equal(600); + }); }); diff --git a/test/core/WebGLRenderer.js b/test/core/WebGLRenderer.js new file mode 100644 index 0000000..3d6cf98 --- /dev/null +++ b/test/core/WebGLRenderer.js @@ -0,0 +1,21 @@ +'use strict'; + +const withGL = require('../withGL'); + +describe('PIXI.WebGLRenderer', function () +{ + it('setting option legacy should disable VAOs and SPRITE_MAX_TEXTURES', withGL(function () + { + const renderer = new PIXI.WebGLRenderer(1, 1, { legacy: true }); + + 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 1271fef..71ade15 100644 --- a/package.json +++ b/package.json @@ -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/scripts/release.js b/scripts/release.js index c4c8e36..156620c 100644 --- a/scripts/release.js +++ b/scripts/release.js @@ -12,7 +12,8 @@ 'dist/**/*', 'lib/**/*', 'src/**/*', - 'scripts/**/*', + 'scripts/*', + 'scripts/renders/*', 'test/**/*', '*.json', '*.md', diff --git a/scripts/renders/client.js b/scripts/renders/client.js index 16c9565..6faee34 100755 --- a/scripts/renders/client.js +++ b/scripts/renders/client.js @@ -1,6 +1,6 @@ 'use strict'; -require('../../bin/pixi'); +require('../../dist/pixi'); PIXI.utils.skipHello(); const fs = require('fs'); 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 bc730db..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. @@ -33,8 +33,11 @@ * need to call toDataUrl on the webgl context * @param {number} [options.resolution=1] - The resolution / device pixel ratio of the renderer, retina would be 2 * @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 @@ -43,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. */ @@ -73,7 +99,7 @@ */ stop() { - this.ticker.stop(); + this._ticker.stop(); } /** @@ -81,7 +107,7 @@ */ start() { - this.ticker.start(); + this._ticker.start(); } /** @@ -95,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 index 81e076b..78b17e6 100644 --- a/src/core/Shader.js +++ b/src/core/Shader.js @@ -1,9 +1,7 @@ import { GLShader } from 'pixi-gl-core'; import settings from './settings'; -const { PRECISION } = settings; - -function checkPrecision(src) +function checkPrecision(src, def) { if (src instanceof Array) { @@ -11,14 +9,14 @@ { const copy = src.slice(0); - copy.unshift(`precision ${PRECISION} float;`); + copy.unshift(`precision ${def} float;`); return copy; } } else if (src.substring(0, 9) !== 'precision') { - return `precision ${PRECISION} float;\n${src}`; + return `precision ${def} float;\n${src}`; } return src; @@ -42,6 +40,7 @@ */ constructor(gl, vertexSrc, fragmentSrc) { - super(gl, checkPrecision(vertexSrc), checkPrecision(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 3425b41..f9235e4 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -181,7 +181,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 b3792ef..17fde8d 100644 --- a/src/core/display/Container.js +++ b/src/core/display/Container.js @@ -73,13 +73,14 @@ } child.parent = this; - - // ensure a transform will be recalculated.. - this.transform._parentID = -1; - this._boundsID++; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.push(child); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(this.children.length - 1); child.emit('added', this); @@ -108,9 +109,14 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.splice(index, 0, child); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(index); child.emit('added', this); @@ -174,6 +180,7 @@ removeItems(this.children, currentIndex, 1); // remove from old position this.children.splice(index, 0, child); // add at new position + this.onChildrenChange(index); } @@ -220,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! @@ -244,9 +252,14 @@ { const child = this.getChildAt(index); + // ensure child transform will be recalculated.. child.parent = null; + child.transform._parentID = -1; removeItems(this.children, index, 1); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(index); child.emit('removed', this); @@ -275,8 +288,14 @@ for (let i = 0; i < removed.length; ++i) { removed[i].parent = null; + if (removed[i].transform) + { + removed[i].transform._parentID = -1; + } } + this._boundsID++; + this.onChildrenChange(beginIndex); for (let i = 0; i < removed.length; ++i) diff --git a/src/core/display/DisplayObject.js b/src/core/display/DisplayObject.js index 3affe36..4e6c77d 100644 --- a/src/core/display/DisplayObject.js +++ b/src/core/display/DisplayObject.js @@ -109,7 +109,7 @@ /** * The original, cached mask of the object * - * @member {PIXI.Rectangle} + * @member {PIXI.Graphics|PIXI.Sprite} * @private */ this._mask = null; diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index 95f2af4..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 ); @@ -1065,10 +1061,15 @@ canvasRenderer = new CanvasRenderer(); } - tempMatrix.tx = -bounds.x; - tempMatrix.ty = -bounds.y; + this.transform.updateLocalTransform(); + this.transform.localTransform.copy(tempMatrix); - canvasRenderer.render(this, canvasBuffer, false, tempMatrix); + tempMatrix.invert(); + + tempMatrix.tx -= bounds.x; + tempMatrix.ty -= bounds.y; + + canvasRenderer.render(this, canvasBuffer, true, tempMatrix); const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode); 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 e67680f..867dde2 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/buildCircle.js b/src/core/graphics/webgl/utils/buildCircle.js index 0400d13..abf5d02 100644 --- a/src/core/graphics/webgl/utils/buildCircle.js +++ b/src/core/graphics/webgl/utils/buildCircle.js @@ -33,6 +33,11 @@ height = circleData.height; } + if (width === 0 || height === 0) + { + return; + } + const totalSegs = Math.floor(30 * Math.sqrt(circleData.radius)) || Math.floor(15 * Math.sqrt(circleData.width + circleData.height)); diff --git a/src/core/graphics/webgl/utils/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 90155f9..f03fe15 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -30,6 +30,7 @@ export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; export { default as CanvasGraphicsRenderer } from './graphics/canvas/CanvasGraphicsRenderer'; +export { default as Spritesheet } from './textures/Spritesheet'; export { default as Texture } from './textures/Texture'; export { default as BaseTexture } from './textures/BaseTexture'; export { default as RenderTexture } from './textures/RenderTexture'; diff --git a/src/core/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 ccce680..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); } /** @@ -162,6 +162,9 @@ if (transform) { transform.copy(tempWt); + + // lets not forget to flag the parent transform as dirty... + this._tempDisplayObjectParent.transform._worldID = -1; } else { @@ -169,6 +172,7 @@ } displayObject.parent = this._tempDisplayObjectParent; + displayObject.updateTransform(); displayObject.parent = cacheParent; // displayObject.hitArea = //TODO add a temp hit area @@ -279,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/canvas/utils/CanvasRenderTarget.js b/src/core/renderers/canvas/utils/CanvasRenderTarget.js index 3de0aef..b409891 100644 --- a/src/core/renderers/canvas/utils/CanvasRenderTarget.js +++ b/src/core/renderers/canvas/utils/CanvasRenderTarget.js @@ -1,5 +1,4 @@ import settings from '../../../settings'; -const { RESOLUTION } = settings; /** * Creates a Canvas element of the given size. @@ -30,7 +29,7 @@ */ this.context = this.canvas.getContext('2d'); - this.resolution = resolution || RESOLUTION; + this.resolution = resolution || settings.RESOLUTION; this.resize(width, height); } 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 ce0e1c4..788ed6c 100644 --- a/src/core/renderers/webgl/WebGLRenderer.js +++ b/src/core/renderers/webgl/WebGLRenderer.js @@ -30,8 +30,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 @@ -49,10 +49,19 @@ * enable this if you need to call toDataUrl on the webgl context. * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when * rendering, stopping pixel interpolation. + * @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; + + if (this.legacy) + { + glCore.VertexArrayObject.FORCE_NATIVE = true; + } /** * The type of this renderer as a standardised const @@ -229,7 +238,7 @@ this.emit('context', gl); // setup the width/height properties and gl viewport - this.resize(this.width, this.height); + this.resize(this.screen.width, this.screen.height); } /** @@ -323,16 +332,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) { @@ -376,6 +385,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 @@ -440,9 +469,10 @@ * Changes the current shader to the one given in parameter * * @param {PIXI.Shader} shader - the new shader + * @param {boolean} [autoProject=true] - Whether automatically set the projection matrix * @return {PIXI.WebGLRenderer} Returns itself. */ - bindShader(shader) + bindShader(shader, autoProject) { // TODO cache if (this._activeShader !== shader) @@ -450,8 +480,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 bbab3b3..944f41d 100644 --- a/src/core/renderers/webgl/filters/Filter.js +++ b/src/core/renderers/webgl/filters/Filter.js @@ -1,6 +1,7 @@ import extractUniformsFromSrc from './extractUniformsFromSrc'; import { uid } from '../../../utils'; import { BLEND_MODES } from '../../../const'; +import settings from '../../../settings'; const SOURCE_KEY_MAP = {}; @@ -10,7 +11,7 @@ * @memberof PIXI * @extends PIXI.Shader */ -class Filter +export default class Filter { /** * @param {string} [vertexSrc] - The source of the vertex shader. @@ -35,8 +36,6 @@ this.blendMode = BLEND_MODES.NORMAL; - // pull out the vertex and shader uniforms if they are not specified.. - // currently this does not extract structs only default types this.uniformData = uniforms || extractUniformsFromSrc(this.vertexSrc, this.fragmentSrc, 'projectionMatrix|uSampler'); /** @@ -80,7 +79,7 @@ * * @member {number} */ - this.resolution = 1; + this.resolution = settings.RESOLUTION; /** * If enabled is true the filter is applied, if false it will not. @@ -97,8 +96,11 @@ * @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 { // --- // // this.uniforms.filterMatrix = filterManager.calculateSpriteMatrix(tempMatrix, window.panda ); @@ -170,5 +172,3 @@ ].join('\n'); } } - -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 dc7072a..6c7422d 100644 --- a/src/core/renderers/webgl/managers/FilterManager.js +++ b/src/core/renderers/webgl/managers/FilterManager.js @@ -154,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 @@ -176,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; @@ -184,7 +184,7 @@ flop = t; } - filters[i].apply(this, flip, lastState.renderTarget, true); + filters[i].apply(this, flip, lastState.renderTarget, false, currentState); this.freePotRenderTarget(flip); this.freePotRenderTarget(flop); @@ -291,9 +291,13 @@ let textureCount = 1; let currentState; - if (shader.uniforms.data.filterArea) + // 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; @@ -306,9 +310,9 @@ // use this to clamp displaced texture coords so they belong to filterArea // see displacementFilter fragment shader for an example - if (shader.uniforms.data.filterClamp) + if (shader.uniforms.filterClamp) { - currentState = this.filterData.stack[this.filterData.index]; + currentState = currentState || this.filterData.stack[this.filterData.index]; const filterClamp = shader.uniforms.filterClamp; @@ -485,7 +489,7 @@ */ destroy() { - this.shaderCache = []; + this.shaderCache = {}; this.emptyPool(); } diff --git a/src/core/renderers/webgl/utils/RenderTarget.js b/src/core/renderers/webgl/utils/RenderTarget.js index afe51ab..6d2467f 100644 --- a/src/core/renderers/webgl/utils/RenderTarget.js +++ b/src/core/renderers/webgl/utils/RenderTarget.js @@ -124,7 +124,7 @@ * @default PIXI.settings.SCALE_MODE * @see PIXI.SCALE_MODES */ - this.scaleMode = scaleMode || settings.SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; /** * Whether this object is the root element or not 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 36ea5bb..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; @@ -346,8 +346,8 @@ /** * Gets the local bounds of the sprite object. * - * @param {Rectangle} rect - The output rectangle. - * @return {Rectangle} The bounds. + * @param {PIXI.Rectangle} rect - The output rectangle. + * @return {PIXI.Rectangle} The bounds. */ getLocalBounds(rect) { @@ -440,7 +440,7 @@ * * @static * @param {number|string|PIXI.BaseTexture|HTMLCanvasElement|HTMLVideoElement} source Source to create texture from - * @return {PIXI.Texture} 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 e77b39a..90b08c0 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++) @@ -103,11 +102,18 @@ { const gl = this.renderer.gl; - // step 1: first check max textures the GPU can handle. - this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); + if (this.renderer.legacy) + { + this.MAX_TEXTURES = 1; + } + else + { + // step 1: first check max textures the GPU can handle. + this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); - // step 2: check the maximum number of if statements the shader can have too.. - this.MAX_TEXTURES = checkMaxIfStatmentsInShader(this.MAX_TEXTURES, gl); + // step 2: check the maximum number of if statements the shader can have too.. + this.MAX_TEXTURES = checkMaxIfStatmentsInShader(this.MAX_TEXTURES, gl); + } const shader = this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); @@ -130,8 +136,12 @@ .addIndex(this.indexBuffer) .addAttribute(this.vertexBuffers[i], shader.attributes.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) .addAttribute(this.vertexBuffers[i], shader.attributes.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) - .addAttribute(this.vertexBuffers[i], shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4) - .addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + .addAttribute(this.vertexBuffers[i], shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); + + if (shader.attributes.aTextureId) + { + this.vaos[i].addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + } /* eslint-enable max-len */ } @@ -378,8 +388,12 @@ .addIndex(this.indexBuffer) .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) - .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4) - .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); + + if (this.shader.attributes.aTextureId) + { + this.vaos[this.vertexCount].addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + } /* eslint-enable max-len */ } diff --git a/src/core/text/Text.js b/src/core/text/Text.js index 013625a..cec8c32 100644 --- a/src/core/text/Text.js +++ b/src/core/text/Text.js @@ -6,6 +6,7 @@ import { TEXT_GRADIENT } from '../const'; import settings from '../settings'; import TextStyle from './TextStyle'; +import trimCanvas from '../utils/trimCanvas'; const defaultDestroyOptions = { texture: true, @@ -57,7 +58,7 @@ /** * The canvas 2d context that everything is drawn with - * @member {HTMLCanvasElement} + * @member {CanvasRenderingContext2D} */ this.context = this.canvas.getContext('2d'); @@ -189,10 +190,12 @@ if (style.dropShadow) { + this.context.shadowBlur = style.dropShadowBlur; + this.context.globalAlpha = style.dropShadowAlpha; + if (style.dropShadowBlur > 0) { this.context.shadowColor = style.dropShadowColor; - this.context.shadowBlur = style.dropShadowBlur; } else { @@ -237,6 +240,10 @@ } } + // reset the shadow blur and alpha that was set by the drop shadow, for the regular text + this.context.shadowBlur = 0; + this.context.globalAlpha = 1; + // set canvas text styles this.context.fillStyle = this._generateFillStyle(style, lines); @@ -326,6 +333,15 @@ */ updateTexture() { + if (this._style.trim) + { + const trimmed = trimCanvas(this.canvas); + + this.canvas.width = trimmed.width; + this.canvas.height = trimmed.height; + this.context.putImageData(trimmed.data, 0, 0); + } + const texture = this._texture; const style = this._style; @@ -475,6 +491,19 @@ } /** + * Gets the local bounds of the text object. + * + * @param {Rectangle} rect - The output rectangle. + * @return {Rectangle} The bounds. + */ + getLocalBounds(rect) + { + this.updateText(true); + + return super.getLocalBounds.call(this, rect); + } + + /** * calculates the bounds of the Text as a rectangle. The bounds calculation takes the worldTransform into account. */ _calculateBounds() @@ -525,6 +554,29 @@ const width = this.canvas.width / this.resolution; const height = this.canvas.height / this.resolution; + // make a copy of the style settings, so we can manipulate them later + const fill = style.fill.slice(); + const fillGradientStops = style.fillGradientStops.slice(); + + // wanting to evenly distribute the fills. So an array of 4 colours should give fills of 0.25, 0.5 and 0.75 + if (!fillGradientStops.length) + { + const lengthPlus1 = fill.length + 1; + + for (let i = 1; i < lengthPlus1; ++i) + { + fillGradientStops.push(i / lengthPlus1); + } + } + + // stop the bleeding of the last gradient on the line above to the top gradient of the this line + // by hard defining the first gradient colour at point 0, and last gradient colour at point 1 + fill.unshift(style.fill[0]); + fillGradientStops.unshift(0); + + fill.push(style.fill[style.fill.length - 1]); + fillGradientStops.push(1); + if (style.fillGradientType === TEXT_GRADIENT.LINEAR_VERTICAL) { // start the gradient at the top center of the canvas, and end at the bottom middle of the canvas @@ -532,15 +584,22 @@ // we need to repeat the gradient so that each individual line of text has the same vertical gradient effect // ['#FF0000', '#00FF00', '#0000FF'] over 2 lines would create stops at 0.125, 0.25, 0.375, 0.625, 0.75, 0.875 - totalIterations = (style.fill.length + 1) * lines.length; + totalIterations = (fill.length + 1) * lines.length; currentIteration = 0; for (let i = 0; i < lines.length; i++) { currentIteration += 1; - for (let j = 0; j < style.fill.length; j++) + for (let j = 0; j < fill.length; j++) { - stop = (currentIteration / totalIterations); - gradient.addColorStop(stop, style.fill[j]); + if (fillGradientStops[j]) + { + stop = (fillGradientStops[j] / lines.length) + (i / lines.length); + } + else + { + stop = currentIteration / totalIterations; + } + gradient.addColorStop(stop, fill[j]); currentIteration++; } } @@ -552,13 +611,20 @@ // can just evenly space out the gradients in this case, as multiple lines makes no difference // to an even left to right gradient - totalIterations = style.fill.length + 1; + totalIterations = fill.length + 1; currentIteration = 1; - for (let i = 0; i < style.fill.length; i++) + for (let i = 0; i < fill.length; i++) { - stop = currentIteration / totalIterations; - gradient.addColorStop(stop, style.fill[i]); + if (fillGradientStops[i]) + { + stop = fillGradientStops[i]; + } + else + { + stop = currentIteration / totalIterations; + } + gradient.addColorStop(stop, fill[i]); currentIteration++; } } @@ -569,7 +635,7 @@ /** * Destroys this text object. * Note* Unlike a Sprite, a Text object will automatically destroy its baseTexture and texture as - * the majorety of the time the texture will not be shared with any other Sprites. + * the majority of the time the texture will not be shared with any other Sprites. * * @param {object|boolean} [options] - Options parameter. A boolean will act as if all options * have been set to that value @@ -680,7 +746,7 @@ set text(text) // eslint-disable-line require-jsdoc { - text = String(text || ' '); + text = String(text === '' || text === null || text === undefined ? ' ' : text); if (this._text === text) { diff --git a/src/core/text/TextStyle.js b/src/core/text/TextStyle.js index 3b25528..890950b 100644 --- a/src/core/text/TextStyle.js +++ b/src/core/text/TextStyle.js @@ -8,12 +8,14 @@ align: 'left', breakWords: false, dropShadow: false, + dropShadowAlpha: 1, dropShadowAngle: Math.PI / 6, dropShadowBlur: 0, dropShadowColor: '#000000', dropShadowDistance: 5, fill: 'black', fillGradientType: TEXT_GRADIENT.LINEAR_VERTICAL, + fillGradientStops: [], fontFamily: 'Arial', fontSize: 26, fontStyle: 'normal', @@ -27,6 +29,7 @@ stroke: 'black', strokeThickness: 0, textBaseline: 'alphabetic', + trim: false, wordWrap: false, wordWrapWidth: 100, }; @@ -47,6 +50,7 @@ * @param {boolean} [style.breakWords=false] - Indicates if lines can be wrapped within words, it * needs wordWrap to be set to true * @param {boolean} [style.dropShadow=false] - Set a drop shadow for the text + * @param {number} [style.dropShadowAlpha=1] - Set alpha for the drop shadow * @param {number} [style.dropShadowAngle=Math.PI/6] - Set a angle of the drop shadow * @param {number} [style.dropShadowBlur=0] - Set a shadow blur radius * @param {string} [style.dropShadowColor='#000000'] - A fill style to be used on the dropshadow e.g 'red', '#00FF00' @@ -55,8 +59,10 @@ * fillstyle that will be used on the text e.g 'red', '#00FF00'. Can be an array to create a gradient * eg ['#000000','#FFFFFF'] * {@link https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/fillStyle|MDN} - * @param {number} [style.fillGradientType=PIXI.TEXT_GRADIENT.LINEAR_VERTICAL] - If fills styles are - * supplied, this can change the type/direction of the gradient. See {@link PIXI.TEXT_GRADIENT} for possible values + * @param {number} [style.fillGradientType=PIXI.TEXT_GRADIENT.LINEAR_VERTICAL] - If fill is an array of colours + * to create a gradient, this can change the type/direction of the gradient. See {@link PIXI.TEXT_GRADIENT} + * @param {number[]} [style.fillGradientStops] - If fill is an array of colours to create a gradient, this array can set + * the stop points (numbers between 0 and 1) for the color, overriding the default behaviour of evenly spacing them. * @param {string|string[]} [style.fontFamily='Arial'] - The font family * @param {number|string} [style.fontSize=26] - The font size (as a number it converts to px, but as a string, * equivalents are '26px','20pt','160%' or '1.6em') @@ -76,6 +82,7 @@ * e.g 'blue', '#FCFF00' * @param {number} [style.strokeThickness=0] - A number that represents the thickness of the stroke. * Default is 0 (no stroke) + * @param {boolean} [style.trim=false] - Trim transparent borders * @param {string} [style.textBaseline='alphabetic'] - The baseline of the text that is rendered. * @param {boolean} [style.wordWrap=false] - Indicates if word wrap should be used * @param {number} [style.wordWrapWidth=100] - The width at which text will wrap, it needs wordWrap to be set to true @@ -152,6 +159,19 @@ } } + get dropShadowAlpha() + { + return this._dropShadowAlpha; + } + set dropShadowAlpha(dropShadowAlpha) + { + if (this._dropShadowAlpha !== dropShadowAlpha) + { + this._dropShadowAlpha = dropShadowAlpha; + this.styleID++; + } + } + get dropShadowAngle() { return this._dropShadowAngle; @@ -232,6 +252,19 @@ } } + get fillGradientStops() + { + return this._fillGradientStops; + } + set fillGradientStops(fillGradientStops) + { + if (!areArraysEqual(this._fillGradientStops,fillGradientStops)) + { + this._fillGradientStops = fillGradientStops; + this.styleID++; + } + } + get fontFamily() { return this._fontFamily; @@ -402,6 +435,19 @@ } } + get trim() + { + return this._trim; + } + set trim(trim) + { + if (this._trim !== trim) + { + this._trim = trim; + this.styleID++; + } + } + get wordWrap() { return this._wordWrap; @@ -475,3 +521,34 @@ return color; } } + +/** + * Utility function to convert hexadecimal colors to strings, and simply return the color if it's a string. + * This version can also convert array of colors + * + * @param {Array} array1 First array to compare + * @param {Array} array2 Second array to compare + * @return {boolean} Do the arrays contain the same values in the same order + */ +function areArraysEqual(array1, array2) +{ + if (!Array.isArray(array1) || !Array.isArray(array2)) + { + return false; + } + + if (array1.length !== array2.length) + { + return false; + } + + for (let i = 0; i < array1.length; ++i) + { + if (array1[i] !== array2[i]) + { + return false; + } + } + + return true; +} diff --git a/src/core/textures/BaseRenderTexture.js b/src/core/textures/BaseRenderTexture.js index 5d50146..aa727f0 100644 --- a/src/core/textures/BaseRenderTexture.js +++ b/src/core/textures/BaseRenderTexture.js @@ -1,8 +1,6 @@ import BaseTexture from './BaseTexture'; import settings from '../settings'; -const { RESOLUTION, SCALE_MODE } = settings; - /** * A BaseRenderTexture is a special texture that allows any Pixi display object to be rendered to it. * @@ -54,7 +52,7 @@ { super(null, scaleMode); - this.resolution = resolution || RESOLUTION; + this.resolution = resolution || settings.RESOLUTION; this.width = width; this.height = height; @@ -62,7 +60,7 @@ this.realWidth = this.width * this.resolution; this.realHeight = this.height * this.resolution; - this.scaleMode = scaleMode || SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; this.hasLoaded = true; /** diff --git a/src/core/textures/BaseTexture.js b/src/core/textures/BaseTexture.js index 676dca3..aeccf39 100644 --- a/src/core/textures/BaseTexture.js +++ b/src/core/textures/BaseTexture.js @@ -77,7 +77,7 @@ * @default PIXI.settings.SCALE_MODE * @see PIXI.SCALE_MODES */ - this.scaleMode = scaleMode || settings.SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; /** * Set to true once the base texture has successfully loaded. @@ -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/RenderTexture.js b/src/core/textures/RenderTexture.js index 4c5034f..b3c6297 100644 --- a/src/core/textures/RenderTexture.js +++ b/src/core/textures/RenderTexture.js @@ -54,8 +54,8 @@ /* eslint-disable prefer-rest-params, no-console */ const width = arguments[1]; const height = arguments[2]; - const scaleMode = arguments[3] || 0; - const resolution = arguments[4] || 1; + const scaleMode = arguments[3]; + const resolution = arguments[4]; // we have an old render texture.. console.warn(`Please use RenderTexture.create(${width}, ${height}) instead of the ctor directly.`); 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 ca31456..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 @@ -459,7 +497,9 @@ if (frame.x + frame.width > this.baseTexture.width || frame.y + frame.height > this.baseTexture.height) { - throw new Error(`Texture Error: frame does not fit inside the base Texture dimensions ${this}`); + throw new Error('Texture Error: frame does not fit inside the base Texture dimensions: ' + + `X: ${frame.x} + ${frame.width} > ${this.baseTexture.width} ` + + `Y: ${frame.y} + ${frame.height} > ${this.baseTexture.height}`); } // this.valid = frame && frame.width && frame.height && this.baseTexture.source && this.baseTexture.hasLoaded; @@ -520,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. @@ -528,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 2a7497b..99e585a 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)); } /** @@ -360,3 +362,43 @@ * @private */ export const BaseTextureCache = {}; + +/** + * 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/core/utils/trimCanvas.js b/src/core/utils/trimCanvas.js new file mode 100644 index 0000000..5af6de0 --- /dev/null +++ b/src/core/utils/trimCanvas.js @@ -0,0 +1,83 @@ +/** + * Trim transparent borders from a canvas + * + * @memberof PIXI + * @function trimCanvas + * @private + * @param {HTMLCanvasElement} canvas - the canvas to trim + * @returns {object} Trim data + */ +export default function trimCanvas(canvas) +{ + // https://gist.github.com/remy/784508 + + let width = canvas.width; + let height = canvas.height; + + const context = canvas.getContext('2d'); + const imageData = context.getImageData(0, 0, width, height); + const pixels = imageData.data; + const len = pixels.length; + + const bound = { + top: null, + left: null, + right: null, + bottom: null, + }; + let i; + let x; + let y; + + for (i = 0; i < len; i += 4) + { + if (pixels[i + 3] !== 0) + { + x = (i / 4) % width; + y = ~~((i / 4) / width); + + if (bound.top === null) + { + bound.top = y; + } + + if (bound.left === null) + { + bound.left = x; + } + else if (x < bound.left) + { + bound.left = x; + } + + if (bound.right === null) + { + bound.right = x + 1; + } + else if (bound.right < x) + { + bound.right = x + 1; + } + + if (bound.bottom === null) + { + bound.bottom = y; + } + else if (bound.bottom < y) + { + bound.bottom = y; + } + } + } + + width = bound.right - bound.left; + height = bound.bottom - bound.top + 1; + + const data = context.getImageData(bound.left, bound.top, width, height); + + return { + height, + width, + data, + }; +} diff --git a/src/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/extras/TilingSprite.js b/src/extras/TilingSprite.js index f90d248..fe82451 100644 --- a/src/extras/TilingSprite.js +++ b/src/extras/TilingSprite.js @@ -220,20 +220,33 @@ transform.tx * resolution, transform.ty * resolution); - // TODO - this should be rolled into the setTransform above.. - context.scale(this.tileScale.x / baseTextureResolution, this.tileScale.y / baseTextureResolution); - - context.translate(modX + (this.anchor.x * -this._width), - modY + (this.anchor.y * -this._height)); - renderer.setBlendMode(this.blendMode); // fill the pattern! context.fillStyle = this._canvasPattern; - context.fillRect(-modX, - -modY, - this._width / this.tileScale.x * baseTextureResolution, - this._height / this.tileScale.y * baseTextureResolution); + + // TODO - this should be rolled into the setTransform above.. + context.scale(this.tileScale.x / baseTextureResolution, this.tileScale.y / baseTextureResolution); + + const anchorX = this.anchor.x * -this._width; + const anchorY = this.anchor.y * -this._height; + + if (this.uvRespectAnchor) + { + context.translate(modX, modY); + + context.fillRect(-modX + anchorX, -modY + anchorY, + this._width / this.tileScale.x * baseTextureResolution, + this._height / this.tileScale.y * baseTextureResolution); + } + else + { + context.translate(modX + anchorX, modY + anchorY); + + context.fillRect(-modX, -modY, + this._width / this.tileScale.x * baseTextureResolution, + this._height / this.tileScale.y * baseTextureResolution); + } } /** diff --git a/src/extras/cacheAsBitmap.js b/src/extras/cacheAsBitmap.js index 18043a7..5dd5640 100644 --- a/src/extras/cacheAsBitmap.js +++ b/src/extras/cacheAsBitmap.js @@ -41,6 +41,9 @@ * provide a performance benefit for complex static displayObjects. * To remove simply set this property to 'false' * + * IMPORTANT GOTCHA - make sure that all your textures are preloaded BEFORE setting this property to true + * as it will take a snapshot of what is currently there. If the textures have not loaded then they will not appear. + * * @member {boolean} * @memberof PIXI.DisplayObject# */ diff --git a/src/filters/blur/BlurFilter.js b/src/filters/blur/BlurFilter.js index 340fc58..1575db9 100644 --- a/src/filters/blur/BlurFilter.js +++ b/src/filters/blur/BlurFilter.js @@ -24,10 +24,9 @@ this.blurXFilter = new BlurXFilter(strength, quality, resolution, kernelSize); this.blurYFilter = new BlurYFilter(strength, quality, resolution, kernelSize); - this.resolution = 1; this.padding = 0; - this.resolution = resolution || 1; + this.resolution = resolution || core.settings.RESOLUTION; this.quality = quality || 4; this.blur = strength || 8; } diff --git a/src/filters/blur/BlurXFilter.js b/src/filters/blur/BlurXFilter.js index def99c3..0f48889 100644 --- a/src/filters/blur/BlurXFilter.js +++ b/src/filters/blur/BlurXFilter.js @@ -31,7 +31,7 @@ fragSrc ); - this.resolution = resolution || 1; + this.resolution = resolution || core.settings.RESOLUTION; this._quality = 0; diff --git a/src/filters/blur/BlurYFilter.js b/src/filters/blur/BlurYFilter.js index 638e0f0..6d489c7 100644 --- a/src/filters/blur/BlurYFilter.js +++ b/src/filters/blur/BlurYFilter.js @@ -31,7 +31,7 @@ fragSrc ); - this.resolution = resolution || 1; + this.resolution = resolution || core.settings.RESOLUTION; this._quality = 0; diff --git a/src/filters/colormatrix/ColorMatrixFilter.js b/src/filters/colormatrix/ColorMatrixFilter.js index 8509505..679f8ec 100644 --- a/src/filters/colormatrix/ColorMatrixFilter.js +++ b/src/filters/colormatrix/ColorMatrixFilter.js @@ -75,28 +75,28 @@ out[1] = (a[0] * b[1]) + (a[1] * b[6]) + (a[2] * b[11]) + (a[3] * b[16]); out[2] = (a[0] * b[2]) + (a[1] * b[7]) + (a[2] * b[12]) + (a[3] * b[17]); out[3] = (a[0] * b[3]) + (a[1] * b[8]) + (a[2] * b[13]) + (a[3] * b[18]); - out[4] = (a[0] * b[4]) + (a[1] * b[9]) + (a[2] * b[14]) + (a[3] * b[19]); + out[4] = (a[0] * b[4]) + (a[1] * b[9]) + (a[2] * b[14]) + (a[3] * b[19]) + a[4]; // Green Channel out[5] = (a[5] * b[0]) + (a[6] * b[5]) + (a[7] * b[10]) + (a[8] * b[15]); out[6] = (a[5] * b[1]) + (a[6] * b[6]) + (a[7] * b[11]) + (a[8] * b[16]); out[7] = (a[5] * b[2]) + (a[6] * b[7]) + (a[7] * b[12]) + (a[8] * b[17]); out[8] = (a[5] * b[3]) + (a[6] * b[8]) + (a[7] * b[13]) + (a[8] * b[18]); - out[9] = (a[5] * b[4]) + (a[6] * b[9]) + (a[7] * b[14]) + (a[8] * b[19]); + out[9] = (a[5] * b[4]) + (a[6] * b[9]) + (a[7] * b[14]) + (a[8] * b[19]) + a[9]; // Blue Channel out[10] = (a[10] * b[0]) + (a[11] * b[5]) + (a[12] * b[10]) + (a[13] * b[15]); out[11] = (a[10] * b[1]) + (a[11] * b[6]) + (a[12] * b[11]) + (a[13] * b[16]); out[12] = (a[10] * b[2]) + (a[11] * b[7]) + (a[12] * b[12]) + (a[13] * b[17]); out[13] = (a[10] * b[3]) + (a[11] * b[8]) + (a[12] * b[13]) + (a[13] * b[18]); - out[14] = (a[10] * b[4]) + (a[11] * b[9]) + (a[12] * b[14]) + (a[13] * b[19]); + out[14] = (a[10] * b[4]) + (a[11] * b[9]) + (a[12] * b[14]) + (a[13] * b[19]) + a[14]; // Alpha Channel out[15] = (a[15] * b[0]) + (a[16] * b[5]) + (a[17] * b[10]) + (a[18] * b[15]); out[16] = (a[15] * b[1]) + (a[16] * b[6]) + (a[17] * b[11]) + (a[18] * b[16]); out[17] = (a[15] * b[2]) + (a[16] * b[7]) + (a[17] * b[12]) + (a[18] * b[17]); out[18] = (a[15] * b[3]) + (a[16] * b[8]) + (a[17] * b[13]) + (a[18] * b[18]); - out[19] = (a[15] * b[4]) + (a[16] * b[9]) + (a[17] * b[14]) + (a[18] * b[19]); + out[19] = (a[15] * b[4]) + (a[16] * b[9]) + (a[17] * b[14]) + (a[18] * b[19]) + a[19]; return out; } diff --git a/src/filters/colormatrix/colorMatrix.frag b/src/filters/colormatrix/colorMatrix.frag index 3aaf2b3..c73c0e9 100644 --- a/src/filters/colormatrix/colorMatrix.frag +++ b/src/filters/colormatrix/colorMatrix.frag @@ -4,32 +4,38 @@ void main(void) { - vec4 c = texture2D(uSampler, vTextureCoord); + // Un-premultiply alpha before applying the color matrix. See issue #3539. + if (c.a > 0.0) { + c.rgb /= c.a; + } + vec4 result; + result.r = (m[0] * c.r); + result.r += (m[1] * c.g); + result.r += (m[2] * c.b); + result.r += (m[3] * c.a); + result.r += m[4]; - gl_FragColor.r = (m[0] * c.r); - gl_FragColor.r += (m[1] * c.g); - gl_FragColor.r += (m[2] * c.b); - gl_FragColor.r += (m[3] * c.a); - gl_FragColor.r += m[4] * c.a; + result.g = (m[5] * c.r); + result.g += (m[6] * c.g); + result.g += (m[7] * c.b); + result.g += (m[8] * c.a); + result.g += m[9]; - gl_FragColor.g = (m[5] * c.r); - gl_FragColor.g += (m[6] * c.g); - gl_FragColor.g += (m[7] * c.b); - gl_FragColor.g += (m[8] * c.a); - gl_FragColor.g += m[9] * c.a; + result.b = (m[10] * c.r); + result.b += (m[11] * c.g); + result.b += (m[12] * c.b); + result.b += (m[13] * c.a); + result.b += m[14]; - gl_FragColor.b = (m[10] * c.r); - gl_FragColor.b += (m[11] * c.g); - gl_FragColor.b += (m[12] * c.b); - gl_FragColor.b += (m[13] * c.a); - gl_FragColor.b += m[14] * c.a; + result.a = (m[15] * c.r); + result.a += (m[16] * c.g); + result.a += (m[17] * c.b); + result.a += (m[18] * c.a); + result.a += m[19]; - gl_FragColor.a = (m[15] * c.r); - gl_FragColor.a += (m[16] * c.g); - gl_FragColor.a += (m[17] * c.b); - gl_FragColor.a += (m[18] * c.a); - gl_FragColor.a += m[19] * c.a; + // Premultiply alpha again. + result.rgb *= result.a; -// gl_FragColor = vec4(m[0]); + gl_FragColor = result; } diff --git a/src/filters/displacement/DisplacementFilter.js b/src/filters/displacement/DisplacementFilter.js index 112de0b..d5355c3 100644 --- a/src/filters/displacement/DisplacementFilter.js +++ b/src/filters/displacement/DisplacementFilter.js @@ -36,7 +36,7 @@ this.maskMatrix = maskMatrix; this.uniforms.mapSampler = sprite.texture; - this.uniforms.filterMatrix = maskMatrix.toArray(true); + this.uniforms.filterMatrix = maskMatrix; this.uniforms.scale = { x: 1, y: 1 }; if (scale === null || scale === undefined) diff --git a/src/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/InteractionData.js b/src/interaction/InteractionData.js index 02d53a3..818da35 100644 --- a/src/interaction/InteractionData.js +++ b/src/interaction/InteractionData.js @@ -21,9 +21,9 @@ this.global = new core.Point(); /** - * The target Sprite that was interacted with + * The target DisplayObject that was interacted with * - * @member {PIXI.Sprite} + * @member {PIXI.DisplayObject} */ this.target = null; @@ -33,6 +33,13 @@ * @member {Event} */ this.originalEvent = null; + + /** + * Unique identifier for this interaction + * + * @member {number} + */ + this.identifier = null; } /** diff --git a/src/interaction/InteractionEvent.js b/src/interaction/InteractionEvent.js index 75b2966..dc554f9 100644 --- a/src/interaction/InteractionEvent.js +++ b/src/interaction/InteractionEvent.js @@ -58,7 +58,7 @@ } /** - * Prevents event from reaching any objects other than the current object. + * Resets the event. * * @private */ diff --git a/src/interaction/InteractionManager.js b/src/interaction/InteractionManager.js index 7d51134..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/src/prepare/BasePrepare.js b/src/prepare/BasePrepare.js index a2005f5..09eb5e5 100644 --- a/src/prepare/BasePrepare.js +++ b/src/prepare/BasePrepare.js @@ -20,7 +20,7 @@ * * @abstract * @class - * @memberof PIXI + * @memberof PIXI.prepare */ export default class BasePrepare { @@ -107,9 +107,9 @@ /** * Upload all the textures and graphics to the GPU. * - * @param {Function|PIXI.DisplayObject|PIXI.Container} item - Either - * the container or display object to search for items to upload or - * the callback function, if items have been added using `prepare.add`. + * @param {Function|PIXI.DisplayObject|PIXI.Container|PIXI.BaseTexture|PIXI.Texture|PIXI.Graphics|PIXI.Text} item - + * Either the container or display object to search for items to upload, the items to upload themselves, + * or the callback function, if items have been added using `prepare.add`. * @param {Function} [done] - Optional callback when all queued uploads have completed */ upload(item, done) @@ -236,7 +236,8 @@ /** * Manually add an item to the uploading queue. * - * @param {PIXI.DisplayObject|PIXI.Container|*} item - Object to add to the queue + * @param {PIXI.DisplayObject|PIXI.Container|PIXI.BaseTexture|PIXI.Texture|PIXI.Graphics|PIXI.Text|*} item - Object to + * add to the queue * @return {PIXI.CanvasPrepare} Instance of plugin for chaining. */ add(item) diff --git a/src/prepare/canvas/CanvasPrepare.js b/src/prepare/canvas/CanvasPrepare.js index 4b99d22..0224bb5 100644 --- a/src/prepare/canvas/CanvasPrepare.js +++ b/src/prepare/canvas/CanvasPrepare.js @@ -12,7 +12,8 @@ * An instance of this class is automatically created by default, and can be found at renderer.plugins.prepare * * @class - * @memberof PIXI + * @extends PIXI.prepare.BasePrepare + * @memberof PIXI.prepare */ export default class CanvasPrepare extends BasePrepare { diff --git a/src/prepare/webgl/WebGLPrepare.js b/src/prepare/webgl/WebGLPrepare.js index 1ef7467..eb7023f 100644 --- a/src/prepare/webgl/WebGLPrepare.js +++ b/src/prepare/webgl/WebGLPrepare.js @@ -7,7 +7,8 @@ * An instance of this class is automatically created by default, and can be found at renderer.plugins.prepare * * @class - * @memberof PIXI + * @extends PIXI.prepare.BasePrepare + * @memberof PIXI.prepare */ export default class WebGLPrepare extends BasePrepare { diff --git a/test/core/Circle.js b/test/core/Circle.js index 183f98f..f621992 100644 --- a/test/core/Circle.js +++ b/test/core/Circle.js @@ -53,6 +53,10 @@ expect(circ1.contains(10, 16)).to.be.false; expect(circ1.contains(11, 15)).to.be.false; expect(circ1.contains(0, 0)).to.be.false; + + const circ2 = new PIXI.Circle(10, 10, 0); + + expect(circ2.contains(10, 10)).to.be.false; }); it('should return framing rectangle', function () diff --git a/test/core/Container.js b/test/core/Container.js index 8a10b40..cefde54 100644 --- a/test/core/Container.js +++ b/test/core/Container.js @@ -1,5 +1,39 @@ 'use strict'; +function testAddChild(fn) +{ + return function () + { + fn(function (container, obj) + { + container.addChild(obj); + }); + fn(function (container, obj) + { + container.addChildAt(obj); + }); + }; +} + +function testRemoveChild(fn) +{ + return function () + { + 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 +104,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 +279,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/Ellipse.js b/test/core/Ellipse.js index f2e8234..3f47442 100644 --- a/test/core/Ellipse.js +++ b/test/core/Ellipse.js @@ -56,6 +56,10 @@ expect(ellipse1.contains(10, 16)).to.be.false; expect(ellipse1.contains(11, 15)).to.be.false; expect(ellipse1.contains(0, 0)).to.be.false; + + const ellipse2 = new PIXI.Ellipse(10, 10, 0, 0); + + expect(ellipse2.contains(10, 10)).to.be.false; }); it('should return framing rectangle', function () diff --git a/test/core/Graphics.js b/test/core/Graphics.js index 2a64946..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 () @@ -127,6 +130,16 @@ expect(graphics.containsPoint(point)).to.be.false; }); + + it('should return false when no fill', function () + { + const point = new PIXI.Point(1, 1); + const graphics = new PIXI.Graphics(); + + graphics.drawRect(0, 0, 10, 10); + + expect(graphics.containsPoint(point)).to.be.false; + }); }); describe('arc', function () @@ -208,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/Matrix.js b/test/core/Matrix.js index 9d2694b..76bd57c 100644 --- a/test/core/Matrix.js +++ b/test/core/Matrix.js @@ -107,10 +107,60 @@ expect(m1.ty).to.equal(m2.ty); }); + it('should prepend matrix', function () + { + const m1 = new PIXI.Matrix(); + const m2 = new PIXI.Matrix(); + + m2.set(2, 3, 4, 5, 100, 200); + m1.prepend(m2); + + expect(m1.a).to.equal(m2.a); + expect(m1.b).to.equal(m2.b); + expect(m1.c).to.equal(m2.c); + expect(m1.d).to.equal(m2.d); + expect(m1.tx).to.equal(m2.tx); + expect(m1.ty).to.equal(m2.ty); + + const m3 = new PIXI.Matrix(); + const m4 = new PIXI.Matrix(); + + m3.prepend(m4); + + expect(m3.a).to.equal(m4.a); + expect(m3.b).to.equal(m4.b); + expect(m3.c).to.equal(m4.c); + expect(m3.d).to.equal(m4.d); + expect(m3.tx).to.equal(m4.tx); + expect(m3.ty).to.equal(m4.ty); + }); + it('should get IDENTITY and TEMP_MATRIX', function () { expect(PIXI.Matrix.IDENTITY instanceof PIXI.Matrix).to.be.true; expect(PIXI.Matrix.TEMP_MATRIX instanceof PIXI.Matrix).to.be.true; }); -}); + it('should reset matrix to default when identity() is called', function () + { + const matrix = new PIXI.Matrix(); + + matrix.set(2, 3, 4, 5, 100, 200); + + expect(matrix.a).to.equal(2); + expect(matrix.b).to.equal(3); + expect(matrix.c).to.equal(4); + expect(matrix.d).to.equal(5); + expect(matrix.tx).to.equal(100); + expect(matrix.ty).to.equal(200); + + matrix.identity(); + + expect(matrix.a).to.equal(1); + expect(matrix.b).to.equal(0); + expect(matrix.c).to.equal(0); + expect(matrix.d).to.equal(1); + expect(matrix.tx).to.equal(0); + expect(matrix.ty).to.equal(0); + }); +}); 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/Text.js b/test/core/Text.js index b8ce561..5799ad4 100644 --- a/test/core/Text.js +++ b/test/core/Text.js @@ -112,4 +112,42 @@ expect(text.width).to.equal(300); }); }); + + describe('text', function () + { + it('should convert numbers into strings', function () + { + const text = new PIXI.Text(2); + + expect(text.text).to.equal('2'); + }); + + it('should not change 0 to \'\'', function () + { + const text = new PIXI.Text(0); + + expect(text.text).to.equal('0'); + }); + + it('should prevent setting null', function () + { + const text = new PIXI.Text(null); + + expect(text.text).to.equal(' '); + }); + + it('should prevent setting undefined', function () + { + const text = new PIXI.Text(); + + expect(text.text).to.equal(' '); + }); + + it('should prevent setting \'\'', function () + { + const text = new PIXI.Text(''); + + expect(text.text).to.equal(' '); + }); + }); }); diff --git a/test/core/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/TilingSprite.js b/test/core/TilingSprite.js index 2e449a7..7694792 100644 --- a/test/core/TilingSprite.js +++ b/test/core/TilingSprite.js @@ -24,4 +24,25 @@ expect(bounds.height).to.equal(600); }); }); + + it('checks if tilingSprite contains a point', function () + { + const texture = new PIXI.Texture(new PIXI.BaseTexture()); + const tilingSprite = new PIXI.extras.TilingSprite(texture, 200, 300); + + expect(tilingSprite.containsPoint(new PIXI.Point(1, 1))).to.equal(true); + expect(tilingSprite.containsPoint(new PIXI.Point(300, 400))).to.equal(false); + }); + + it('gets and sets height and width correctly', function () + { + const texture = new PIXI.Texture(new PIXI.BaseTexture()); + const tilingSprite = new PIXI.extras.TilingSprite(texture, 200, 300); + + tilingSprite.width = 400; + tilingSprite.height = 600; + + expect(tilingSprite.width).to.equal(400); + expect(tilingSprite.height).to.equal(600); + }); }); diff --git a/test/core/WebGLRenderer.js b/test/core/WebGLRenderer.js new file mode 100644 index 0000000..3d6cf98 --- /dev/null +++ b/test/core/WebGLRenderer.js @@ -0,0 +1,21 @@ +'use strict'; + +const withGL = require('../withGL'); + +describe('PIXI.WebGLRenderer', function () +{ + it('setting option legacy should disable VAOs and SPRITE_MAX_TEXTURES', withGL(function () + { + const renderer = new PIXI.WebGLRenderer(1, 1, { legacy: true }); + + 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/getLocalBounds.js b/test/core/getLocalBounds.js index d160a56..309da0e 100644 --- a/test/core/getLocalBounds.js +++ b/test/core/getLocalBounds.js @@ -196,4 +196,13 @@ expect(bounds.width).to.equal(100); expect(bounds.height).to.equal(100); }); + + it('should register correct local-bounds with a Text', function () + { + const text = new PIXI.Text('hello'); + const bounds = text.getLocalBounds(); + + expect(bounds.width).to.not.equal(0); + expect(bounds.height).to.not.equal(0); + }); }); diff --git a/package.json b/package.json index 1271fef..71ade15 100644 --- a/package.json +++ b/package.json @@ -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/scripts/release.js b/scripts/release.js index c4c8e36..156620c 100644 --- a/scripts/release.js +++ b/scripts/release.js @@ -12,7 +12,8 @@ 'dist/**/*', 'lib/**/*', 'src/**/*', - 'scripts/**/*', + 'scripts/*', + 'scripts/renders/*', 'test/**/*', '*.json', '*.md', diff --git a/scripts/renders/client.js b/scripts/renders/client.js index 16c9565..6faee34 100755 --- a/scripts/renders/client.js +++ b/scripts/renders/client.js @@ -1,6 +1,6 @@ 'use strict'; -require('../../bin/pixi'); +require('../../dist/pixi'); PIXI.utils.skipHello(); const fs = require('fs'); 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 bc730db..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. @@ -33,8 +33,11 @@ * need to call toDataUrl on the webgl context * @param {number} [options.resolution=1] - The resolution / device pixel ratio of the renderer, retina would be 2 * @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 @@ -43,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. */ @@ -73,7 +99,7 @@ */ stop() { - this.ticker.stop(); + this._ticker.stop(); } /** @@ -81,7 +107,7 @@ */ start() { - this.ticker.start(); + this._ticker.start(); } /** @@ -95,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 index 81e076b..78b17e6 100644 --- a/src/core/Shader.js +++ b/src/core/Shader.js @@ -1,9 +1,7 @@ import { GLShader } from 'pixi-gl-core'; import settings from './settings'; -const { PRECISION } = settings; - -function checkPrecision(src) +function checkPrecision(src, def) { if (src instanceof Array) { @@ -11,14 +9,14 @@ { const copy = src.slice(0); - copy.unshift(`precision ${PRECISION} float;`); + copy.unshift(`precision ${def} float;`); return copy; } } else if (src.substring(0, 9) !== 'precision') { - return `precision ${PRECISION} float;\n${src}`; + return `precision ${def} float;\n${src}`; } return src; @@ -42,6 +40,7 @@ */ constructor(gl, vertexSrc, fragmentSrc) { - super(gl, checkPrecision(vertexSrc), checkPrecision(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 3425b41..f9235e4 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -181,7 +181,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 b3792ef..17fde8d 100644 --- a/src/core/display/Container.js +++ b/src/core/display/Container.js @@ -73,13 +73,14 @@ } child.parent = this; - - // ensure a transform will be recalculated.. - this.transform._parentID = -1; - this._boundsID++; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.push(child); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(this.children.length - 1); child.emit('added', this); @@ -108,9 +109,14 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.splice(index, 0, child); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(index); child.emit('added', this); @@ -174,6 +180,7 @@ removeItems(this.children, currentIndex, 1); // remove from old position this.children.splice(index, 0, child); // add at new position + this.onChildrenChange(index); } @@ -220,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! @@ -244,9 +252,14 @@ { const child = this.getChildAt(index); + // ensure child transform will be recalculated.. child.parent = null; + child.transform._parentID = -1; removeItems(this.children, index, 1); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(index); child.emit('removed', this); @@ -275,8 +288,14 @@ for (let i = 0; i < removed.length; ++i) { removed[i].parent = null; + if (removed[i].transform) + { + removed[i].transform._parentID = -1; + } } + this._boundsID++; + this.onChildrenChange(beginIndex); for (let i = 0; i < removed.length; ++i) diff --git a/src/core/display/DisplayObject.js b/src/core/display/DisplayObject.js index 3affe36..4e6c77d 100644 --- a/src/core/display/DisplayObject.js +++ b/src/core/display/DisplayObject.js @@ -109,7 +109,7 @@ /** * The original, cached mask of the object * - * @member {PIXI.Rectangle} + * @member {PIXI.Graphics|PIXI.Sprite} * @private */ this._mask = null; diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index 95f2af4..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 ); @@ -1065,10 +1061,15 @@ canvasRenderer = new CanvasRenderer(); } - tempMatrix.tx = -bounds.x; - tempMatrix.ty = -bounds.y; + this.transform.updateLocalTransform(); + this.transform.localTransform.copy(tempMatrix); - canvasRenderer.render(this, canvasBuffer, false, tempMatrix); + tempMatrix.invert(); + + tempMatrix.tx -= bounds.x; + tempMatrix.ty -= bounds.y; + + canvasRenderer.render(this, canvasBuffer, true, tempMatrix); const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode); 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 e67680f..867dde2 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/buildCircle.js b/src/core/graphics/webgl/utils/buildCircle.js index 0400d13..abf5d02 100644 --- a/src/core/graphics/webgl/utils/buildCircle.js +++ b/src/core/graphics/webgl/utils/buildCircle.js @@ -33,6 +33,11 @@ height = circleData.height; } + if (width === 0 || height === 0) + { + return; + } + const totalSegs = Math.floor(30 * Math.sqrt(circleData.radius)) || Math.floor(15 * Math.sqrt(circleData.width + circleData.height)); diff --git a/src/core/graphics/webgl/utils/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 90155f9..f03fe15 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -30,6 +30,7 @@ export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; export { default as CanvasGraphicsRenderer } from './graphics/canvas/CanvasGraphicsRenderer'; +export { default as Spritesheet } from './textures/Spritesheet'; export { default as Texture } from './textures/Texture'; export { default as BaseTexture } from './textures/BaseTexture'; export { default as RenderTexture } from './textures/RenderTexture'; diff --git a/src/core/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 ccce680..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); } /** @@ -162,6 +162,9 @@ if (transform) { transform.copy(tempWt); + + // lets not forget to flag the parent transform as dirty... + this._tempDisplayObjectParent.transform._worldID = -1; } else { @@ -169,6 +172,7 @@ } displayObject.parent = this._tempDisplayObjectParent; + displayObject.updateTransform(); displayObject.parent = cacheParent; // displayObject.hitArea = //TODO add a temp hit area @@ -279,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/canvas/utils/CanvasRenderTarget.js b/src/core/renderers/canvas/utils/CanvasRenderTarget.js index 3de0aef..b409891 100644 --- a/src/core/renderers/canvas/utils/CanvasRenderTarget.js +++ b/src/core/renderers/canvas/utils/CanvasRenderTarget.js @@ -1,5 +1,4 @@ import settings from '../../../settings'; -const { RESOLUTION } = settings; /** * Creates a Canvas element of the given size. @@ -30,7 +29,7 @@ */ this.context = this.canvas.getContext('2d'); - this.resolution = resolution || RESOLUTION; + this.resolution = resolution || settings.RESOLUTION; this.resize(width, height); } 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 ce0e1c4..788ed6c 100644 --- a/src/core/renderers/webgl/WebGLRenderer.js +++ b/src/core/renderers/webgl/WebGLRenderer.js @@ -30,8 +30,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 @@ -49,10 +49,19 @@ * enable this if you need to call toDataUrl on the webgl context. * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when * rendering, stopping pixel interpolation. + * @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; + + if (this.legacy) + { + glCore.VertexArrayObject.FORCE_NATIVE = true; + } /** * The type of this renderer as a standardised const @@ -229,7 +238,7 @@ this.emit('context', gl); // setup the width/height properties and gl viewport - this.resize(this.width, this.height); + this.resize(this.screen.width, this.screen.height); } /** @@ -323,16 +332,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) { @@ -376,6 +385,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 @@ -440,9 +469,10 @@ * Changes the current shader to the one given in parameter * * @param {PIXI.Shader} shader - the new shader + * @param {boolean} [autoProject=true] - Whether automatically set the projection matrix * @return {PIXI.WebGLRenderer} Returns itself. */ - bindShader(shader) + bindShader(shader, autoProject) { // TODO cache if (this._activeShader !== shader) @@ -450,8 +480,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 bbab3b3..944f41d 100644 --- a/src/core/renderers/webgl/filters/Filter.js +++ b/src/core/renderers/webgl/filters/Filter.js @@ -1,6 +1,7 @@ import extractUniformsFromSrc from './extractUniformsFromSrc'; import { uid } from '../../../utils'; import { BLEND_MODES } from '../../../const'; +import settings from '../../../settings'; const SOURCE_KEY_MAP = {}; @@ -10,7 +11,7 @@ * @memberof PIXI * @extends PIXI.Shader */ -class Filter +export default class Filter { /** * @param {string} [vertexSrc] - The source of the vertex shader. @@ -35,8 +36,6 @@ this.blendMode = BLEND_MODES.NORMAL; - // pull out the vertex and shader uniforms if they are not specified.. - // currently this does not extract structs only default types this.uniformData = uniforms || extractUniformsFromSrc(this.vertexSrc, this.fragmentSrc, 'projectionMatrix|uSampler'); /** @@ -80,7 +79,7 @@ * * @member {number} */ - this.resolution = 1; + this.resolution = settings.RESOLUTION; /** * If enabled is true the filter is applied, if false it will not. @@ -97,8 +96,11 @@ * @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 { // --- // // this.uniforms.filterMatrix = filterManager.calculateSpriteMatrix(tempMatrix, window.panda ); @@ -170,5 +172,3 @@ ].join('\n'); } } - -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 dc7072a..6c7422d 100644 --- a/src/core/renderers/webgl/managers/FilterManager.js +++ b/src/core/renderers/webgl/managers/FilterManager.js @@ -154,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 @@ -176,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; @@ -184,7 +184,7 @@ flop = t; } - filters[i].apply(this, flip, lastState.renderTarget, true); + filters[i].apply(this, flip, lastState.renderTarget, false, currentState); this.freePotRenderTarget(flip); this.freePotRenderTarget(flop); @@ -291,9 +291,13 @@ let textureCount = 1; let currentState; - if (shader.uniforms.data.filterArea) + // 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; @@ -306,9 +310,9 @@ // use this to clamp displaced texture coords so they belong to filterArea // see displacementFilter fragment shader for an example - if (shader.uniforms.data.filterClamp) + if (shader.uniforms.filterClamp) { - currentState = this.filterData.stack[this.filterData.index]; + currentState = currentState || this.filterData.stack[this.filterData.index]; const filterClamp = shader.uniforms.filterClamp; @@ -485,7 +489,7 @@ */ destroy() { - this.shaderCache = []; + this.shaderCache = {}; this.emptyPool(); } diff --git a/src/core/renderers/webgl/utils/RenderTarget.js b/src/core/renderers/webgl/utils/RenderTarget.js index afe51ab..6d2467f 100644 --- a/src/core/renderers/webgl/utils/RenderTarget.js +++ b/src/core/renderers/webgl/utils/RenderTarget.js @@ -124,7 +124,7 @@ * @default PIXI.settings.SCALE_MODE * @see PIXI.SCALE_MODES */ - this.scaleMode = scaleMode || settings.SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; /** * Whether this object is the root element or not 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 36ea5bb..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; @@ -346,8 +346,8 @@ /** * Gets the local bounds of the sprite object. * - * @param {Rectangle} rect - The output rectangle. - * @return {Rectangle} The bounds. + * @param {PIXI.Rectangle} rect - The output rectangle. + * @return {PIXI.Rectangle} The bounds. */ getLocalBounds(rect) { @@ -440,7 +440,7 @@ * * @static * @param {number|string|PIXI.BaseTexture|HTMLCanvasElement|HTMLVideoElement} source Source to create texture from - * @return {PIXI.Texture} 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 e77b39a..90b08c0 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++) @@ -103,11 +102,18 @@ { const gl = this.renderer.gl; - // step 1: first check max textures the GPU can handle. - this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); + if (this.renderer.legacy) + { + this.MAX_TEXTURES = 1; + } + else + { + // step 1: first check max textures the GPU can handle. + this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); - // step 2: check the maximum number of if statements the shader can have too.. - this.MAX_TEXTURES = checkMaxIfStatmentsInShader(this.MAX_TEXTURES, gl); + // step 2: check the maximum number of if statements the shader can have too.. + this.MAX_TEXTURES = checkMaxIfStatmentsInShader(this.MAX_TEXTURES, gl); + } const shader = this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); @@ -130,8 +136,12 @@ .addIndex(this.indexBuffer) .addAttribute(this.vertexBuffers[i], shader.attributes.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) .addAttribute(this.vertexBuffers[i], shader.attributes.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) - .addAttribute(this.vertexBuffers[i], shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4) - .addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + .addAttribute(this.vertexBuffers[i], shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); + + if (shader.attributes.aTextureId) + { + this.vaos[i].addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + } /* eslint-enable max-len */ } @@ -378,8 +388,12 @@ .addIndex(this.indexBuffer) .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) - .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4) - .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); + + if (this.shader.attributes.aTextureId) + { + this.vaos[this.vertexCount].addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + } /* eslint-enable max-len */ } diff --git a/src/core/text/Text.js b/src/core/text/Text.js index 013625a..cec8c32 100644 --- a/src/core/text/Text.js +++ b/src/core/text/Text.js @@ -6,6 +6,7 @@ import { TEXT_GRADIENT } from '../const'; import settings from '../settings'; import TextStyle from './TextStyle'; +import trimCanvas from '../utils/trimCanvas'; const defaultDestroyOptions = { texture: true, @@ -57,7 +58,7 @@ /** * The canvas 2d context that everything is drawn with - * @member {HTMLCanvasElement} + * @member {CanvasRenderingContext2D} */ this.context = this.canvas.getContext('2d'); @@ -189,10 +190,12 @@ if (style.dropShadow) { + this.context.shadowBlur = style.dropShadowBlur; + this.context.globalAlpha = style.dropShadowAlpha; + if (style.dropShadowBlur > 0) { this.context.shadowColor = style.dropShadowColor; - this.context.shadowBlur = style.dropShadowBlur; } else { @@ -237,6 +240,10 @@ } } + // reset the shadow blur and alpha that was set by the drop shadow, for the regular text + this.context.shadowBlur = 0; + this.context.globalAlpha = 1; + // set canvas text styles this.context.fillStyle = this._generateFillStyle(style, lines); @@ -326,6 +333,15 @@ */ updateTexture() { + if (this._style.trim) + { + const trimmed = trimCanvas(this.canvas); + + this.canvas.width = trimmed.width; + this.canvas.height = trimmed.height; + this.context.putImageData(trimmed.data, 0, 0); + } + const texture = this._texture; const style = this._style; @@ -475,6 +491,19 @@ } /** + * Gets the local bounds of the text object. + * + * @param {Rectangle} rect - The output rectangle. + * @return {Rectangle} The bounds. + */ + getLocalBounds(rect) + { + this.updateText(true); + + return super.getLocalBounds.call(this, rect); + } + + /** * calculates the bounds of the Text as a rectangle. The bounds calculation takes the worldTransform into account. */ _calculateBounds() @@ -525,6 +554,29 @@ const width = this.canvas.width / this.resolution; const height = this.canvas.height / this.resolution; + // make a copy of the style settings, so we can manipulate them later + const fill = style.fill.slice(); + const fillGradientStops = style.fillGradientStops.slice(); + + // wanting to evenly distribute the fills. So an array of 4 colours should give fills of 0.25, 0.5 and 0.75 + if (!fillGradientStops.length) + { + const lengthPlus1 = fill.length + 1; + + for (let i = 1; i < lengthPlus1; ++i) + { + fillGradientStops.push(i / lengthPlus1); + } + } + + // stop the bleeding of the last gradient on the line above to the top gradient of the this line + // by hard defining the first gradient colour at point 0, and last gradient colour at point 1 + fill.unshift(style.fill[0]); + fillGradientStops.unshift(0); + + fill.push(style.fill[style.fill.length - 1]); + fillGradientStops.push(1); + if (style.fillGradientType === TEXT_GRADIENT.LINEAR_VERTICAL) { // start the gradient at the top center of the canvas, and end at the bottom middle of the canvas @@ -532,15 +584,22 @@ // we need to repeat the gradient so that each individual line of text has the same vertical gradient effect // ['#FF0000', '#00FF00', '#0000FF'] over 2 lines would create stops at 0.125, 0.25, 0.375, 0.625, 0.75, 0.875 - totalIterations = (style.fill.length + 1) * lines.length; + totalIterations = (fill.length + 1) * lines.length; currentIteration = 0; for (let i = 0; i < lines.length; i++) { currentIteration += 1; - for (let j = 0; j < style.fill.length; j++) + for (let j = 0; j < fill.length; j++) { - stop = (currentIteration / totalIterations); - gradient.addColorStop(stop, style.fill[j]); + if (fillGradientStops[j]) + { + stop = (fillGradientStops[j] / lines.length) + (i / lines.length); + } + else + { + stop = currentIteration / totalIterations; + } + gradient.addColorStop(stop, fill[j]); currentIteration++; } } @@ -552,13 +611,20 @@ // can just evenly space out the gradients in this case, as multiple lines makes no difference // to an even left to right gradient - totalIterations = style.fill.length + 1; + totalIterations = fill.length + 1; currentIteration = 1; - for (let i = 0; i < style.fill.length; i++) + for (let i = 0; i < fill.length; i++) { - stop = currentIteration / totalIterations; - gradient.addColorStop(stop, style.fill[i]); + if (fillGradientStops[i]) + { + stop = fillGradientStops[i]; + } + else + { + stop = currentIteration / totalIterations; + } + gradient.addColorStop(stop, fill[i]); currentIteration++; } } @@ -569,7 +635,7 @@ /** * Destroys this text object. * Note* Unlike a Sprite, a Text object will automatically destroy its baseTexture and texture as - * the majorety of the time the texture will not be shared with any other Sprites. + * the majority of the time the texture will not be shared with any other Sprites. * * @param {object|boolean} [options] - Options parameter. A boolean will act as if all options * have been set to that value @@ -680,7 +746,7 @@ set text(text) // eslint-disable-line require-jsdoc { - text = String(text || ' '); + text = String(text === '' || text === null || text === undefined ? ' ' : text); if (this._text === text) { diff --git a/src/core/text/TextStyle.js b/src/core/text/TextStyle.js index 3b25528..890950b 100644 --- a/src/core/text/TextStyle.js +++ b/src/core/text/TextStyle.js @@ -8,12 +8,14 @@ align: 'left', breakWords: false, dropShadow: false, + dropShadowAlpha: 1, dropShadowAngle: Math.PI / 6, dropShadowBlur: 0, dropShadowColor: '#000000', dropShadowDistance: 5, fill: 'black', fillGradientType: TEXT_GRADIENT.LINEAR_VERTICAL, + fillGradientStops: [], fontFamily: 'Arial', fontSize: 26, fontStyle: 'normal', @@ -27,6 +29,7 @@ stroke: 'black', strokeThickness: 0, textBaseline: 'alphabetic', + trim: false, wordWrap: false, wordWrapWidth: 100, }; @@ -47,6 +50,7 @@ * @param {boolean} [style.breakWords=false] - Indicates if lines can be wrapped within words, it * needs wordWrap to be set to true * @param {boolean} [style.dropShadow=false] - Set a drop shadow for the text + * @param {number} [style.dropShadowAlpha=1] - Set alpha for the drop shadow * @param {number} [style.dropShadowAngle=Math.PI/6] - Set a angle of the drop shadow * @param {number} [style.dropShadowBlur=0] - Set a shadow blur radius * @param {string} [style.dropShadowColor='#000000'] - A fill style to be used on the dropshadow e.g 'red', '#00FF00' @@ -55,8 +59,10 @@ * fillstyle that will be used on the text e.g 'red', '#00FF00'. Can be an array to create a gradient * eg ['#000000','#FFFFFF'] * {@link https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/fillStyle|MDN} - * @param {number} [style.fillGradientType=PIXI.TEXT_GRADIENT.LINEAR_VERTICAL] - If fills styles are - * supplied, this can change the type/direction of the gradient. See {@link PIXI.TEXT_GRADIENT} for possible values + * @param {number} [style.fillGradientType=PIXI.TEXT_GRADIENT.LINEAR_VERTICAL] - If fill is an array of colours + * to create a gradient, this can change the type/direction of the gradient. See {@link PIXI.TEXT_GRADIENT} + * @param {number[]} [style.fillGradientStops] - If fill is an array of colours to create a gradient, this array can set + * the stop points (numbers between 0 and 1) for the color, overriding the default behaviour of evenly spacing them. * @param {string|string[]} [style.fontFamily='Arial'] - The font family * @param {number|string} [style.fontSize=26] - The font size (as a number it converts to px, but as a string, * equivalents are '26px','20pt','160%' or '1.6em') @@ -76,6 +82,7 @@ * e.g 'blue', '#FCFF00' * @param {number} [style.strokeThickness=0] - A number that represents the thickness of the stroke. * Default is 0 (no stroke) + * @param {boolean} [style.trim=false] - Trim transparent borders * @param {string} [style.textBaseline='alphabetic'] - The baseline of the text that is rendered. * @param {boolean} [style.wordWrap=false] - Indicates if word wrap should be used * @param {number} [style.wordWrapWidth=100] - The width at which text will wrap, it needs wordWrap to be set to true @@ -152,6 +159,19 @@ } } + get dropShadowAlpha() + { + return this._dropShadowAlpha; + } + set dropShadowAlpha(dropShadowAlpha) + { + if (this._dropShadowAlpha !== dropShadowAlpha) + { + this._dropShadowAlpha = dropShadowAlpha; + this.styleID++; + } + } + get dropShadowAngle() { return this._dropShadowAngle; @@ -232,6 +252,19 @@ } } + get fillGradientStops() + { + return this._fillGradientStops; + } + set fillGradientStops(fillGradientStops) + { + if (!areArraysEqual(this._fillGradientStops,fillGradientStops)) + { + this._fillGradientStops = fillGradientStops; + this.styleID++; + } + } + get fontFamily() { return this._fontFamily; @@ -402,6 +435,19 @@ } } + get trim() + { + return this._trim; + } + set trim(trim) + { + if (this._trim !== trim) + { + this._trim = trim; + this.styleID++; + } + } + get wordWrap() { return this._wordWrap; @@ -475,3 +521,34 @@ return color; } } + +/** + * Utility function to convert hexadecimal colors to strings, and simply return the color if it's a string. + * This version can also convert array of colors + * + * @param {Array} array1 First array to compare + * @param {Array} array2 Second array to compare + * @return {boolean} Do the arrays contain the same values in the same order + */ +function areArraysEqual(array1, array2) +{ + if (!Array.isArray(array1) || !Array.isArray(array2)) + { + return false; + } + + if (array1.length !== array2.length) + { + return false; + } + + for (let i = 0; i < array1.length; ++i) + { + if (array1[i] !== array2[i]) + { + return false; + } + } + + return true; +} diff --git a/src/core/textures/BaseRenderTexture.js b/src/core/textures/BaseRenderTexture.js index 5d50146..aa727f0 100644 --- a/src/core/textures/BaseRenderTexture.js +++ b/src/core/textures/BaseRenderTexture.js @@ -1,8 +1,6 @@ import BaseTexture from './BaseTexture'; import settings from '../settings'; -const { RESOLUTION, SCALE_MODE } = settings; - /** * A BaseRenderTexture is a special texture that allows any Pixi display object to be rendered to it. * @@ -54,7 +52,7 @@ { super(null, scaleMode); - this.resolution = resolution || RESOLUTION; + this.resolution = resolution || settings.RESOLUTION; this.width = width; this.height = height; @@ -62,7 +60,7 @@ this.realWidth = this.width * this.resolution; this.realHeight = this.height * this.resolution; - this.scaleMode = scaleMode || SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; this.hasLoaded = true; /** diff --git a/src/core/textures/BaseTexture.js b/src/core/textures/BaseTexture.js index 676dca3..aeccf39 100644 --- a/src/core/textures/BaseTexture.js +++ b/src/core/textures/BaseTexture.js @@ -77,7 +77,7 @@ * @default PIXI.settings.SCALE_MODE * @see PIXI.SCALE_MODES */ - this.scaleMode = scaleMode || settings.SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; /** * Set to true once the base texture has successfully loaded. @@ -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/RenderTexture.js b/src/core/textures/RenderTexture.js index 4c5034f..b3c6297 100644 --- a/src/core/textures/RenderTexture.js +++ b/src/core/textures/RenderTexture.js @@ -54,8 +54,8 @@ /* eslint-disable prefer-rest-params, no-console */ const width = arguments[1]; const height = arguments[2]; - const scaleMode = arguments[3] || 0; - const resolution = arguments[4] || 1; + const scaleMode = arguments[3]; + const resolution = arguments[4]; // we have an old render texture.. console.warn(`Please use RenderTexture.create(${width}, ${height}) instead of the ctor directly.`); 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 ca31456..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 @@ -459,7 +497,9 @@ if (frame.x + frame.width > this.baseTexture.width || frame.y + frame.height > this.baseTexture.height) { - throw new Error(`Texture Error: frame does not fit inside the base Texture dimensions ${this}`); + throw new Error('Texture Error: frame does not fit inside the base Texture dimensions: ' + + `X: ${frame.x} + ${frame.width} > ${this.baseTexture.width} ` + + `Y: ${frame.y} + ${frame.height} > ${this.baseTexture.height}`); } // this.valid = frame && frame.width && frame.height && this.baseTexture.source && this.baseTexture.hasLoaded; @@ -520,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. @@ -528,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 2a7497b..99e585a 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)); } /** @@ -360,3 +362,43 @@ * @private */ export const BaseTextureCache = {}; + +/** + * 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/core/utils/trimCanvas.js b/src/core/utils/trimCanvas.js new file mode 100644 index 0000000..5af6de0 --- /dev/null +++ b/src/core/utils/trimCanvas.js @@ -0,0 +1,83 @@ +/** + * Trim transparent borders from a canvas + * + * @memberof PIXI + * @function trimCanvas + * @private + * @param {HTMLCanvasElement} canvas - the canvas to trim + * @returns {object} Trim data + */ +export default function trimCanvas(canvas) +{ + // https://gist.github.com/remy/784508 + + let width = canvas.width; + let height = canvas.height; + + const context = canvas.getContext('2d'); + const imageData = context.getImageData(0, 0, width, height); + const pixels = imageData.data; + const len = pixels.length; + + const bound = { + top: null, + left: null, + right: null, + bottom: null, + }; + let i; + let x; + let y; + + for (i = 0; i < len; i += 4) + { + if (pixels[i + 3] !== 0) + { + x = (i / 4) % width; + y = ~~((i / 4) / width); + + if (bound.top === null) + { + bound.top = y; + } + + if (bound.left === null) + { + bound.left = x; + } + else if (x < bound.left) + { + bound.left = x; + } + + if (bound.right === null) + { + bound.right = x + 1; + } + else if (bound.right < x) + { + bound.right = x + 1; + } + + if (bound.bottom === null) + { + bound.bottom = y; + } + else if (bound.bottom < y) + { + bound.bottom = y; + } + } + } + + width = bound.right - bound.left; + height = bound.bottom - bound.top + 1; + + const data = context.getImageData(bound.left, bound.top, width, height); + + return { + height, + width, + data, + }; +} diff --git a/src/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/extras/TilingSprite.js b/src/extras/TilingSprite.js index f90d248..fe82451 100644 --- a/src/extras/TilingSprite.js +++ b/src/extras/TilingSprite.js @@ -220,20 +220,33 @@ transform.tx * resolution, transform.ty * resolution); - // TODO - this should be rolled into the setTransform above.. - context.scale(this.tileScale.x / baseTextureResolution, this.tileScale.y / baseTextureResolution); - - context.translate(modX + (this.anchor.x * -this._width), - modY + (this.anchor.y * -this._height)); - renderer.setBlendMode(this.blendMode); // fill the pattern! context.fillStyle = this._canvasPattern; - context.fillRect(-modX, - -modY, - this._width / this.tileScale.x * baseTextureResolution, - this._height / this.tileScale.y * baseTextureResolution); + + // TODO - this should be rolled into the setTransform above.. + context.scale(this.tileScale.x / baseTextureResolution, this.tileScale.y / baseTextureResolution); + + const anchorX = this.anchor.x * -this._width; + const anchorY = this.anchor.y * -this._height; + + if (this.uvRespectAnchor) + { + context.translate(modX, modY); + + context.fillRect(-modX + anchorX, -modY + anchorY, + this._width / this.tileScale.x * baseTextureResolution, + this._height / this.tileScale.y * baseTextureResolution); + } + else + { + context.translate(modX + anchorX, modY + anchorY); + + context.fillRect(-modX, -modY, + this._width / this.tileScale.x * baseTextureResolution, + this._height / this.tileScale.y * baseTextureResolution); + } } /** diff --git a/src/extras/cacheAsBitmap.js b/src/extras/cacheAsBitmap.js index 18043a7..5dd5640 100644 --- a/src/extras/cacheAsBitmap.js +++ b/src/extras/cacheAsBitmap.js @@ -41,6 +41,9 @@ * provide a performance benefit for complex static displayObjects. * To remove simply set this property to 'false' * + * IMPORTANT GOTCHA - make sure that all your textures are preloaded BEFORE setting this property to true + * as it will take a snapshot of what is currently there. If the textures have not loaded then they will not appear. + * * @member {boolean} * @memberof PIXI.DisplayObject# */ diff --git a/src/filters/blur/BlurFilter.js b/src/filters/blur/BlurFilter.js index 340fc58..1575db9 100644 --- a/src/filters/blur/BlurFilter.js +++ b/src/filters/blur/BlurFilter.js @@ -24,10 +24,9 @@ this.blurXFilter = new BlurXFilter(strength, quality, resolution, kernelSize); this.blurYFilter = new BlurYFilter(strength, quality, resolution, kernelSize); - this.resolution = 1; this.padding = 0; - this.resolution = resolution || 1; + this.resolution = resolution || core.settings.RESOLUTION; this.quality = quality || 4; this.blur = strength || 8; } diff --git a/src/filters/blur/BlurXFilter.js b/src/filters/blur/BlurXFilter.js index def99c3..0f48889 100644 --- a/src/filters/blur/BlurXFilter.js +++ b/src/filters/blur/BlurXFilter.js @@ -31,7 +31,7 @@ fragSrc ); - this.resolution = resolution || 1; + this.resolution = resolution || core.settings.RESOLUTION; this._quality = 0; diff --git a/src/filters/blur/BlurYFilter.js b/src/filters/blur/BlurYFilter.js index 638e0f0..6d489c7 100644 --- a/src/filters/blur/BlurYFilter.js +++ b/src/filters/blur/BlurYFilter.js @@ -31,7 +31,7 @@ fragSrc ); - this.resolution = resolution || 1; + this.resolution = resolution || core.settings.RESOLUTION; this._quality = 0; diff --git a/src/filters/colormatrix/ColorMatrixFilter.js b/src/filters/colormatrix/ColorMatrixFilter.js index 8509505..679f8ec 100644 --- a/src/filters/colormatrix/ColorMatrixFilter.js +++ b/src/filters/colormatrix/ColorMatrixFilter.js @@ -75,28 +75,28 @@ out[1] = (a[0] * b[1]) + (a[1] * b[6]) + (a[2] * b[11]) + (a[3] * b[16]); out[2] = (a[0] * b[2]) + (a[1] * b[7]) + (a[2] * b[12]) + (a[3] * b[17]); out[3] = (a[0] * b[3]) + (a[1] * b[8]) + (a[2] * b[13]) + (a[3] * b[18]); - out[4] = (a[0] * b[4]) + (a[1] * b[9]) + (a[2] * b[14]) + (a[3] * b[19]); + out[4] = (a[0] * b[4]) + (a[1] * b[9]) + (a[2] * b[14]) + (a[3] * b[19]) + a[4]; // Green Channel out[5] = (a[5] * b[0]) + (a[6] * b[5]) + (a[7] * b[10]) + (a[8] * b[15]); out[6] = (a[5] * b[1]) + (a[6] * b[6]) + (a[7] * b[11]) + (a[8] * b[16]); out[7] = (a[5] * b[2]) + (a[6] * b[7]) + (a[7] * b[12]) + (a[8] * b[17]); out[8] = (a[5] * b[3]) + (a[6] * b[8]) + (a[7] * b[13]) + (a[8] * b[18]); - out[9] = (a[5] * b[4]) + (a[6] * b[9]) + (a[7] * b[14]) + (a[8] * b[19]); + out[9] = (a[5] * b[4]) + (a[6] * b[9]) + (a[7] * b[14]) + (a[8] * b[19]) + a[9]; // Blue Channel out[10] = (a[10] * b[0]) + (a[11] * b[5]) + (a[12] * b[10]) + (a[13] * b[15]); out[11] = (a[10] * b[1]) + (a[11] * b[6]) + (a[12] * b[11]) + (a[13] * b[16]); out[12] = (a[10] * b[2]) + (a[11] * b[7]) + (a[12] * b[12]) + (a[13] * b[17]); out[13] = (a[10] * b[3]) + (a[11] * b[8]) + (a[12] * b[13]) + (a[13] * b[18]); - out[14] = (a[10] * b[4]) + (a[11] * b[9]) + (a[12] * b[14]) + (a[13] * b[19]); + out[14] = (a[10] * b[4]) + (a[11] * b[9]) + (a[12] * b[14]) + (a[13] * b[19]) + a[14]; // Alpha Channel out[15] = (a[15] * b[0]) + (a[16] * b[5]) + (a[17] * b[10]) + (a[18] * b[15]); out[16] = (a[15] * b[1]) + (a[16] * b[6]) + (a[17] * b[11]) + (a[18] * b[16]); out[17] = (a[15] * b[2]) + (a[16] * b[7]) + (a[17] * b[12]) + (a[18] * b[17]); out[18] = (a[15] * b[3]) + (a[16] * b[8]) + (a[17] * b[13]) + (a[18] * b[18]); - out[19] = (a[15] * b[4]) + (a[16] * b[9]) + (a[17] * b[14]) + (a[18] * b[19]); + out[19] = (a[15] * b[4]) + (a[16] * b[9]) + (a[17] * b[14]) + (a[18] * b[19]) + a[19]; return out; } diff --git a/src/filters/colormatrix/colorMatrix.frag b/src/filters/colormatrix/colorMatrix.frag index 3aaf2b3..c73c0e9 100644 --- a/src/filters/colormatrix/colorMatrix.frag +++ b/src/filters/colormatrix/colorMatrix.frag @@ -4,32 +4,38 @@ void main(void) { - vec4 c = texture2D(uSampler, vTextureCoord); + // Un-premultiply alpha before applying the color matrix. See issue #3539. + if (c.a > 0.0) { + c.rgb /= c.a; + } + vec4 result; + result.r = (m[0] * c.r); + result.r += (m[1] * c.g); + result.r += (m[2] * c.b); + result.r += (m[3] * c.a); + result.r += m[4]; - gl_FragColor.r = (m[0] * c.r); - gl_FragColor.r += (m[1] * c.g); - gl_FragColor.r += (m[2] * c.b); - gl_FragColor.r += (m[3] * c.a); - gl_FragColor.r += m[4] * c.a; + result.g = (m[5] * c.r); + result.g += (m[6] * c.g); + result.g += (m[7] * c.b); + result.g += (m[8] * c.a); + result.g += m[9]; - gl_FragColor.g = (m[5] * c.r); - gl_FragColor.g += (m[6] * c.g); - gl_FragColor.g += (m[7] * c.b); - gl_FragColor.g += (m[8] * c.a); - gl_FragColor.g += m[9] * c.a; + result.b = (m[10] * c.r); + result.b += (m[11] * c.g); + result.b += (m[12] * c.b); + result.b += (m[13] * c.a); + result.b += m[14]; - gl_FragColor.b = (m[10] * c.r); - gl_FragColor.b += (m[11] * c.g); - gl_FragColor.b += (m[12] * c.b); - gl_FragColor.b += (m[13] * c.a); - gl_FragColor.b += m[14] * c.a; + result.a = (m[15] * c.r); + result.a += (m[16] * c.g); + result.a += (m[17] * c.b); + result.a += (m[18] * c.a); + result.a += m[19]; - gl_FragColor.a = (m[15] * c.r); - gl_FragColor.a += (m[16] * c.g); - gl_FragColor.a += (m[17] * c.b); - gl_FragColor.a += (m[18] * c.a); - gl_FragColor.a += m[19] * c.a; + // Premultiply alpha again. + result.rgb *= result.a; -// gl_FragColor = vec4(m[0]); + gl_FragColor = result; } diff --git a/src/filters/displacement/DisplacementFilter.js b/src/filters/displacement/DisplacementFilter.js index 112de0b..d5355c3 100644 --- a/src/filters/displacement/DisplacementFilter.js +++ b/src/filters/displacement/DisplacementFilter.js @@ -36,7 +36,7 @@ this.maskMatrix = maskMatrix; this.uniforms.mapSampler = sprite.texture; - this.uniforms.filterMatrix = maskMatrix.toArray(true); + this.uniforms.filterMatrix = maskMatrix; this.uniforms.scale = { x: 1, y: 1 }; if (scale === null || scale === undefined) diff --git a/src/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/InteractionData.js b/src/interaction/InteractionData.js index 02d53a3..818da35 100644 --- a/src/interaction/InteractionData.js +++ b/src/interaction/InteractionData.js @@ -21,9 +21,9 @@ this.global = new core.Point(); /** - * The target Sprite that was interacted with + * The target DisplayObject that was interacted with * - * @member {PIXI.Sprite} + * @member {PIXI.DisplayObject} */ this.target = null; @@ -33,6 +33,13 @@ * @member {Event} */ this.originalEvent = null; + + /** + * Unique identifier for this interaction + * + * @member {number} + */ + this.identifier = null; } /** diff --git a/src/interaction/InteractionEvent.js b/src/interaction/InteractionEvent.js index 75b2966..dc554f9 100644 --- a/src/interaction/InteractionEvent.js +++ b/src/interaction/InteractionEvent.js @@ -58,7 +58,7 @@ } /** - * Prevents event from reaching any objects other than the current object. + * Resets the event. * * @private */ diff --git a/src/interaction/InteractionManager.js b/src/interaction/InteractionManager.js index 7d51134..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/src/prepare/BasePrepare.js b/src/prepare/BasePrepare.js index a2005f5..09eb5e5 100644 --- a/src/prepare/BasePrepare.js +++ b/src/prepare/BasePrepare.js @@ -20,7 +20,7 @@ * * @abstract * @class - * @memberof PIXI + * @memberof PIXI.prepare */ export default class BasePrepare { @@ -107,9 +107,9 @@ /** * Upload all the textures and graphics to the GPU. * - * @param {Function|PIXI.DisplayObject|PIXI.Container} item - Either - * the container or display object to search for items to upload or - * the callback function, if items have been added using `prepare.add`. + * @param {Function|PIXI.DisplayObject|PIXI.Container|PIXI.BaseTexture|PIXI.Texture|PIXI.Graphics|PIXI.Text} item - + * Either the container or display object to search for items to upload, the items to upload themselves, + * or the callback function, if items have been added using `prepare.add`. * @param {Function} [done] - Optional callback when all queued uploads have completed */ upload(item, done) @@ -236,7 +236,8 @@ /** * Manually add an item to the uploading queue. * - * @param {PIXI.DisplayObject|PIXI.Container|*} item - Object to add to the queue + * @param {PIXI.DisplayObject|PIXI.Container|PIXI.BaseTexture|PIXI.Texture|PIXI.Graphics|PIXI.Text|*} item - Object to + * add to the queue * @return {PIXI.CanvasPrepare} Instance of plugin for chaining. */ add(item) diff --git a/src/prepare/canvas/CanvasPrepare.js b/src/prepare/canvas/CanvasPrepare.js index 4b99d22..0224bb5 100644 --- a/src/prepare/canvas/CanvasPrepare.js +++ b/src/prepare/canvas/CanvasPrepare.js @@ -12,7 +12,8 @@ * An instance of this class is automatically created by default, and can be found at renderer.plugins.prepare * * @class - * @memberof PIXI + * @extends PIXI.prepare.BasePrepare + * @memberof PIXI.prepare */ export default class CanvasPrepare extends BasePrepare { diff --git a/src/prepare/webgl/WebGLPrepare.js b/src/prepare/webgl/WebGLPrepare.js index 1ef7467..eb7023f 100644 --- a/src/prepare/webgl/WebGLPrepare.js +++ b/src/prepare/webgl/WebGLPrepare.js @@ -7,7 +7,8 @@ * An instance of this class is automatically created by default, and can be found at renderer.plugins.prepare * * @class - * @memberof PIXI + * @extends PIXI.prepare.BasePrepare + * @memberof PIXI.prepare */ export default class WebGLPrepare extends BasePrepare { diff --git a/test/core/Circle.js b/test/core/Circle.js index 183f98f..f621992 100644 --- a/test/core/Circle.js +++ b/test/core/Circle.js @@ -53,6 +53,10 @@ expect(circ1.contains(10, 16)).to.be.false; expect(circ1.contains(11, 15)).to.be.false; expect(circ1.contains(0, 0)).to.be.false; + + const circ2 = new PIXI.Circle(10, 10, 0); + + expect(circ2.contains(10, 10)).to.be.false; }); it('should return framing rectangle', function () diff --git a/test/core/Container.js b/test/core/Container.js index 8a10b40..cefde54 100644 --- a/test/core/Container.js +++ b/test/core/Container.js @@ -1,5 +1,39 @@ 'use strict'; +function testAddChild(fn) +{ + return function () + { + fn(function (container, obj) + { + container.addChild(obj); + }); + fn(function (container, obj) + { + container.addChildAt(obj); + }); + }; +} + +function testRemoveChild(fn) +{ + return function () + { + 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 +104,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 +279,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/Ellipse.js b/test/core/Ellipse.js index f2e8234..3f47442 100644 --- a/test/core/Ellipse.js +++ b/test/core/Ellipse.js @@ -56,6 +56,10 @@ expect(ellipse1.contains(10, 16)).to.be.false; expect(ellipse1.contains(11, 15)).to.be.false; expect(ellipse1.contains(0, 0)).to.be.false; + + const ellipse2 = new PIXI.Ellipse(10, 10, 0, 0); + + expect(ellipse2.contains(10, 10)).to.be.false; }); it('should return framing rectangle', function () diff --git a/test/core/Graphics.js b/test/core/Graphics.js index 2a64946..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 () @@ -127,6 +130,16 @@ expect(graphics.containsPoint(point)).to.be.false; }); + + it('should return false when no fill', function () + { + const point = new PIXI.Point(1, 1); + const graphics = new PIXI.Graphics(); + + graphics.drawRect(0, 0, 10, 10); + + expect(graphics.containsPoint(point)).to.be.false; + }); }); describe('arc', function () @@ -208,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/Matrix.js b/test/core/Matrix.js index 9d2694b..76bd57c 100644 --- a/test/core/Matrix.js +++ b/test/core/Matrix.js @@ -107,10 +107,60 @@ expect(m1.ty).to.equal(m2.ty); }); + it('should prepend matrix', function () + { + const m1 = new PIXI.Matrix(); + const m2 = new PIXI.Matrix(); + + m2.set(2, 3, 4, 5, 100, 200); + m1.prepend(m2); + + expect(m1.a).to.equal(m2.a); + expect(m1.b).to.equal(m2.b); + expect(m1.c).to.equal(m2.c); + expect(m1.d).to.equal(m2.d); + expect(m1.tx).to.equal(m2.tx); + expect(m1.ty).to.equal(m2.ty); + + const m3 = new PIXI.Matrix(); + const m4 = new PIXI.Matrix(); + + m3.prepend(m4); + + expect(m3.a).to.equal(m4.a); + expect(m3.b).to.equal(m4.b); + expect(m3.c).to.equal(m4.c); + expect(m3.d).to.equal(m4.d); + expect(m3.tx).to.equal(m4.tx); + expect(m3.ty).to.equal(m4.ty); + }); + it('should get IDENTITY and TEMP_MATRIX', function () { expect(PIXI.Matrix.IDENTITY instanceof PIXI.Matrix).to.be.true; expect(PIXI.Matrix.TEMP_MATRIX instanceof PIXI.Matrix).to.be.true; }); -}); + it('should reset matrix to default when identity() is called', function () + { + const matrix = new PIXI.Matrix(); + + matrix.set(2, 3, 4, 5, 100, 200); + + expect(matrix.a).to.equal(2); + expect(matrix.b).to.equal(3); + expect(matrix.c).to.equal(4); + expect(matrix.d).to.equal(5); + expect(matrix.tx).to.equal(100); + expect(matrix.ty).to.equal(200); + + matrix.identity(); + + expect(matrix.a).to.equal(1); + expect(matrix.b).to.equal(0); + expect(matrix.c).to.equal(0); + expect(matrix.d).to.equal(1); + expect(matrix.tx).to.equal(0); + expect(matrix.ty).to.equal(0); + }); +}); 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/Text.js b/test/core/Text.js index b8ce561..5799ad4 100644 --- a/test/core/Text.js +++ b/test/core/Text.js @@ -112,4 +112,42 @@ expect(text.width).to.equal(300); }); }); + + describe('text', function () + { + it('should convert numbers into strings', function () + { + const text = new PIXI.Text(2); + + expect(text.text).to.equal('2'); + }); + + it('should not change 0 to \'\'', function () + { + const text = new PIXI.Text(0); + + expect(text.text).to.equal('0'); + }); + + it('should prevent setting null', function () + { + const text = new PIXI.Text(null); + + expect(text.text).to.equal(' '); + }); + + it('should prevent setting undefined', function () + { + const text = new PIXI.Text(); + + expect(text.text).to.equal(' '); + }); + + it('should prevent setting \'\'', function () + { + const text = new PIXI.Text(''); + + expect(text.text).to.equal(' '); + }); + }); }); diff --git a/test/core/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/TilingSprite.js b/test/core/TilingSprite.js index 2e449a7..7694792 100644 --- a/test/core/TilingSprite.js +++ b/test/core/TilingSprite.js @@ -24,4 +24,25 @@ expect(bounds.height).to.equal(600); }); }); + + it('checks if tilingSprite contains a point', function () + { + const texture = new PIXI.Texture(new PIXI.BaseTexture()); + const tilingSprite = new PIXI.extras.TilingSprite(texture, 200, 300); + + expect(tilingSprite.containsPoint(new PIXI.Point(1, 1))).to.equal(true); + expect(tilingSprite.containsPoint(new PIXI.Point(300, 400))).to.equal(false); + }); + + it('gets and sets height and width correctly', function () + { + const texture = new PIXI.Texture(new PIXI.BaseTexture()); + const tilingSprite = new PIXI.extras.TilingSprite(texture, 200, 300); + + tilingSprite.width = 400; + tilingSprite.height = 600; + + expect(tilingSprite.width).to.equal(400); + expect(tilingSprite.height).to.equal(600); + }); }); diff --git a/test/core/WebGLRenderer.js b/test/core/WebGLRenderer.js new file mode 100644 index 0000000..3d6cf98 --- /dev/null +++ b/test/core/WebGLRenderer.js @@ -0,0 +1,21 @@ +'use strict'; + +const withGL = require('../withGL'); + +describe('PIXI.WebGLRenderer', function () +{ + it('setting option legacy should disable VAOs and SPRITE_MAX_TEXTURES', withGL(function () + { + const renderer = new PIXI.WebGLRenderer(1, 1, { legacy: true }); + + 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/getLocalBounds.js b/test/core/getLocalBounds.js index d160a56..309da0e 100644 --- a/test/core/getLocalBounds.js +++ b/test/core/getLocalBounds.js @@ -196,4 +196,13 @@ expect(bounds.width).to.equal(100); expect(bounds.height).to.equal(100); }); + + it('should register correct local-bounds with a Text', function () + { + const text = new PIXI.Text('hello'); + const bounds = text.getLocalBounds(); + + expect(bounds.width).to.not.equal(0); + expect(bounds.height).to.not.equal(0); + }); }); diff --git a/test/core/index.js b/test/core/index.js index 2a056a2..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'); @@ -23,4 +24,7 @@ require('./Circle'); require('./Graphics'); require('./SpriteRenderer'); +require('./WebGLRenderer'); require('./Ellipse'); +require('./Texture'); +require('./filters'); diff --git a/package.json b/package.json index 1271fef..71ade15 100644 --- a/package.json +++ b/package.json @@ -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/scripts/release.js b/scripts/release.js index c4c8e36..156620c 100644 --- a/scripts/release.js +++ b/scripts/release.js @@ -12,7 +12,8 @@ 'dist/**/*', 'lib/**/*', 'src/**/*', - 'scripts/**/*', + 'scripts/*', + 'scripts/renders/*', 'test/**/*', '*.json', '*.md', diff --git a/scripts/renders/client.js b/scripts/renders/client.js index 16c9565..6faee34 100755 --- a/scripts/renders/client.js +++ b/scripts/renders/client.js @@ -1,6 +1,6 @@ 'use strict'; -require('../../bin/pixi'); +require('../../dist/pixi'); PIXI.utils.skipHello(); const fs = require('fs'); 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 bc730db..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. @@ -33,8 +33,11 @@ * need to call toDataUrl on the webgl context * @param {number} [options.resolution=1] - The resolution / device pixel ratio of the renderer, retina would be 2 * @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 @@ -43,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. */ @@ -73,7 +99,7 @@ */ stop() { - this.ticker.stop(); + this._ticker.stop(); } /** @@ -81,7 +107,7 @@ */ start() { - this.ticker.start(); + this._ticker.start(); } /** @@ -95,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 index 81e076b..78b17e6 100644 --- a/src/core/Shader.js +++ b/src/core/Shader.js @@ -1,9 +1,7 @@ import { GLShader } from 'pixi-gl-core'; import settings from './settings'; -const { PRECISION } = settings; - -function checkPrecision(src) +function checkPrecision(src, def) { if (src instanceof Array) { @@ -11,14 +9,14 @@ { const copy = src.slice(0); - copy.unshift(`precision ${PRECISION} float;`); + copy.unshift(`precision ${def} float;`); return copy; } } else if (src.substring(0, 9) !== 'precision') { - return `precision ${PRECISION} float;\n${src}`; + return `precision ${def} float;\n${src}`; } return src; @@ -42,6 +40,7 @@ */ constructor(gl, vertexSrc, fragmentSrc) { - super(gl, checkPrecision(vertexSrc), checkPrecision(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 3425b41..f9235e4 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -181,7 +181,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 b3792ef..17fde8d 100644 --- a/src/core/display/Container.js +++ b/src/core/display/Container.js @@ -73,13 +73,14 @@ } child.parent = this; - - // ensure a transform will be recalculated.. - this.transform._parentID = -1; - this._boundsID++; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.push(child); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(this.children.length - 1); child.emit('added', this); @@ -108,9 +109,14 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.splice(index, 0, child); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(index); child.emit('added', this); @@ -174,6 +180,7 @@ removeItems(this.children, currentIndex, 1); // remove from old position this.children.splice(index, 0, child); // add at new position + this.onChildrenChange(index); } @@ -220,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! @@ -244,9 +252,14 @@ { const child = this.getChildAt(index); + // ensure child transform will be recalculated.. child.parent = null; + child.transform._parentID = -1; removeItems(this.children, index, 1); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(index); child.emit('removed', this); @@ -275,8 +288,14 @@ for (let i = 0; i < removed.length; ++i) { removed[i].parent = null; + if (removed[i].transform) + { + removed[i].transform._parentID = -1; + } } + this._boundsID++; + this.onChildrenChange(beginIndex); for (let i = 0; i < removed.length; ++i) diff --git a/src/core/display/DisplayObject.js b/src/core/display/DisplayObject.js index 3affe36..4e6c77d 100644 --- a/src/core/display/DisplayObject.js +++ b/src/core/display/DisplayObject.js @@ -109,7 +109,7 @@ /** * The original, cached mask of the object * - * @member {PIXI.Rectangle} + * @member {PIXI.Graphics|PIXI.Sprite} * @private */ this._mask = null; diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index 95f2af4..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 ); @@ -1065,10 +1061,15 @@ canvasRenderer = new CanvasRenderer(); } - tempMatrix.tx = -bounds.x; - tempMatrix.ty = -bounds.y; + this.transform.updateLocalTransform(); + this.transform.localTransform.copy(tempMatrix); - canvasRenderer.render(this, canvasBuffer, false, tempMatrix); + tempMatrix.invert(); + + tempMatrix.tx -= bounds.x; + tempMatrix.ty -= bounds.y; + + canvasRenderer.render(this, canvasBuffer, true, tempMatrix); const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode); 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 e67680f..867dde2 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/buildCircle.js b/src/core/graphics/webgl/utils/buildCircle.js index 0400d13..abf5d02 100644 --- a/src/core/graphics/webgl/utils/buildCircle.js +++ b/src/core/graphics/webgl/utils/buildCircle.js @@ -33,6 +33,11 @@ height = circleData.height; } + if (width === 0 || height === 0) + { + return; + } + const totalSegs = Math.floor(30 * Math.sqrt(circleData.radius)) || Math.floor(15 * Math.sqrt(circleData.width + circleData.height)); diff --git a/src/core/graphics/webgl/utils/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 90155f9..f03fe15 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -30,6 +30,7 @@ export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; export { default as CanvasGraphicsRenderer } from './graphics/canvas/CanvasGraphicsRenderer'; +export { default as Spritesheet } from './textures/Spritesheet'; export { default as Texture } from './textures/Texture'; export { default as BaseTexture } from './textures/BaseTexture'; export { default as RenderTexture } from './textures/RenderTexture'; diff --git a/src/core/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 ccce680..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); } /** @@ -162,6 +162,9 @@ if (transform) { transform.copy(tempWt); + + // lets not forget to flag the parent transform as dirty... + this._tempDisplayObjectParent.transform._worldID = -1; } else { @@ -169,6 +172,7 @@ } displayObject.parent = this._tempDisplayObjectParent; + displayObject.updateTransform(); displayObject.parent = cacheParent; // displayObject.hitArea = //TODO add a temp hit area @@ -279,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/canvas/utils/CanvasRenderTarget.js b/src/core/renderers/canvas/utils/CanvasRenderTarget.js index 3de0aef..b409891 100644 --- a/src/core/renderers/canvas/utils/CanvasRenderTarget.js +++ b/src/core/renderers/canvas/utils/CanvasRenderTarget.js @@ -1,5 +1,4 @@ import settings from '../../../settings'; -const { RESOLUTION } = settings; /** * Creates a Canvas element of the given size. @@ -30,7 +29,7 @@ */ this.context = this.canvas.getContext('2d'); - this.resolution = resolution || RESOLUTION; + this.resolution = resolution || settings.RESOLUTION; this.resize(width, height); } 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 ce0e1c4..788ed6c 100644 --- a/src/core/renderers/webgl/WebGLRenderer.js +++ b/src/core/renderers/webgl/WebGLRenderer.js @@ -30,8 +30,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 @@ -49,10 +49,19 @@ * enable this if you need to call toDataUrl on the webgl context. * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when * rendering, stopping pixel interpolation. + * @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; + + if (this.legacy) + { + glCore.VertexArrayObject.FORCE_NATIVE = true; + } /** * The type of this renderer as a standardised const @@ -229,7 +238,7 @@ this.emit('context', gl); // setup the width/height properties and gl viewport - this.resize(this.width, this.height); + this.resize(this.screen.width, this.screen.height); } /** @@ -323,16 +332,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) { @@ -376,6 +385,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 @@ -440,9 +469,10 @@ * Changes the current shader to the one given in parameter * * @param {PIXI.Shader} shader - the new shader + * @param {boolean} [autoProject=true] - Whether automatically set the projection matrix * @return {PIXI.WebGLRenderer} Returns itself. */ - bindShader(shader) + bindShader(shader, autoProject) { // TODO cache if (this._activeShader !== shader) @@ -450,8 +480,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 bbab3b3..944f41d 100644 --- a/src/core/renderers/webgl/filters/Filter.js +++ b/src/core/renderers/webgl/filters/Filter.js @@ -1,6 +1,7 @@ import extractUniformsFromSrc from './extractUniformsFromSrc'; import { uid } from '../../../utils'; import { BLEND_MODES } from '../../../const'; +import settings from '../../../settings'; const SOURCE_KEY_MAP = {}; @@ -10,7 +11,7 @@ * @memberof PIXI * @extends PIXI.Shader */ -class Filter +export default class Filter { /** * @param {string} [vertexSrc] - The source of the vertex shader. @@ -35,8 +36,6 @@ this.blendMode = BLEND_MODES.NORMAL; - // pull out the vertex and shader uniforms if they are not specified.. - // currently this does not extract structs only default types this.uniformData = uniforms || extractUniformsFromSrc(this.vertexSrc, this.fragmentSrc, 'projectionMatrix|uSampler'); /** @@ -80,7 +79,7 @@ * * @member {number} */ - this.resolution = 1; + this.resolution = settings.RESOLUTION; /** * If enabled is true the filter is applied, if false it will not. @@ -97,8 +96,11 @@ * @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 { // --- // // this.uniforms.filterMatrix = filterManager.calculateSpriteMatrix(tempMatrix, window.panda ); @@ -170,5 +172,3 @@ ].join('\n'); } } - -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 dc7072a..6c7422d 100644 --- a/src/core/renderers/webgl/managers/FilterManager.js +++ b/src/core/renderers/webgl/managers/FilterManager.js @@ -154,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 @@ -176,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; @@ -184,7 +184,7 @@ flop = t; } - filters[i].apply(this, flip, lastState.renderTarget, true); + filters[i].apply(this, flip, lastState.renderTarget, false, currentState); this.freePotRenderTarget(flip); this.freePotRenderTarget(flop); @@ -291,9 +291,13 @@ let textureCount = 1; let currentState; - if (shader.uniforms.data.filterArea) + // 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; @@ -306,9 +310,9 @@ // use this to clamp displaced texture coords so they belong to filterArea // see displacementFilter fragment shader for an example - if (shader.uniforms.data.filterClamp) + if (shader.uniforms.filterClamp) { - currentState = this.filterData.stack[this.filterData.index]; + currentState = currentState || this.filterData.stack[this.filterData.index]; const filterClamp = shader.uniforms.filterClamp; @@ -485,7 +489,7 @@ */ destroy() { - this.shaderCache = []; + this.shaderCache = {}; this.emptyPool(); } diff --git a/src/core/renderers/webgl/utils/RenderTarget.js b/src/core/renderers/webgl/utils/RenderTarget.js index afe51ab..6d2467f 100644 --- a/src/core/renderers/webgl/utils/RenderTarget.js +++ b/src/core/renderers/webgl/utils/RenderTarget.js @@ -124,7 +124,7 @@ * @default PIXI.settings.SCALE_MODE * @see PIXI.SCALE_MODES */ - this.scaleMode = scaleMode || settings.SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; /** * Whether this object is the root element or not 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 36ea5bb..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; @@ -346,8 +346,8 @@ /** * Gets the local bounds of the sprite object. * - * @param {Rectangle} rect - The output rectangle. - * @return {Rectangle} The bounds. + * @param {PIXI.Rectangle} rect - The output rectangle. + * @return {PIXI.Rectangle} The bounds. */ getLocalBounds(rect) { @@ -440,7 +440,7 @@ * * @static * @param {number|string|PIXI.BaseTexture|HTMLCanvasElement|HTMLVideoElement} source Source to create texture from - * @return {PIXI.Texture} 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 e77b39a..90b08c0 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++) @@ -103,11 +102,18 @@ { const gl = this.renderer.gl; - // step 1: first check max textures the GPU can handle. - this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); + if (this.renderer.legacy) + { + this.MAX_TEXTURES = 1; + } + else + { + // step 1: first check max textures the GPU can handle. + this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); - // step 2: check the maximum number of if statements the shader can have too.. - this.MAX_TEXTURES = checkMaxIfStatmentsInShader(this.MAX_TEXTURES, gl); + // step 2: check the maximum number of if statements the shader can have too.. + this.MAX_TEXTURES = checkMaxIfStatmentsInShader(this.MAX_TEXTURES, gl); + } const shader = this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); @@ -130,8 +136,12 @@ .addIndex(this.indexBuffer) .addAttribute(this.vertexBuffers[i], shader.attributes.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) .addAttribute(this.vertexBuffers[i], shader.attributes.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) - .addAttribute(this.vertexBuffers[i], shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4) - .addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + .addAttribute(this.vertexBuffers[i], shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); + + if (shader.attributes.aTextureId) + { + this.vaos[i].addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + } /* eslint-enable max-len */ } @@ -378,8 +388,12 @@ .addIndex(this.indexBuffer) .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) - .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4) - .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); + + if (this.shader.attributes.aTextureId) + { + this.vaos[this.vertexCount].addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + } /* eslint-enable max-len */ } diff --git a/src/core/text/Text.js b/src/core/text/Text.js index 013625a..cec8c32 100644 --- a/src/core/text/Text.js +++ b/src/core/text/Text.js @@ -6,6 +6,7 @@ import { TEXT_GRADIENT } from '../const'; import settings from '../settings'; import TextStyle from './TextStyle'; +import trimCanvas from '../utils/trimCanvas'; const defaultDestroyOptions = { texture: true, @@ -57,7 +58,7 @@ /** * The canvas 2d context that everything is drawn with - * @member {HTMLCanvasElement} + * @member {CanvasRenderingContext2D} */ this.context = this.canvas.getContext('2d'); @@ -189,10 +190,12 @@ if (style.dropShadow) { + this.context.shadowBlur = style.dropShadowBlur; + this.context.globalAlpha = style.dropShadowAlpha; + if (style.dropShadowBlur > 0) { this.context.shadowColor = style.dropShadowColor; - this.context.shadowBlur = style.dropShadowBlur; } else { @@ -237,6 +240,10 @@ } } + // reset the shadow blur and alpha that was set by the drop shadow, for the regular text + this.context.shadowBlur = 0; + this.context.globalAlpha = 1; + // set canvas text styles this.context.fillStyle = this._generateFillStyle(style, lines); @@ -326,6 +333,15 @@ */ updateTexture() { + if (this._style.trim) + { + const trimmed = trimCanvas(this.canvas); + + this.canvas.width = trimmed.width; + this.canvas.height = trimmed.height; + this.context.putImageData(trimmed.data, 0, 0); + } + const texture = this._texture; const style = this._style; @@ -475,6 +491,19 @@ } /** + * Gets the local bounds of the text object. + * + * @param {Rectangle} rect - The output rectangle. + * @return {Rectangle} The bounds. + */ + getLocalBounds(rect) + { + this.updateText(true); + + return super.getLocalBounds.call(this, rect); + } + + /** * calculates the bounds of the Text as a rectangle. The bounds calculation takes the worldTransform into account. */ _calculateBounds() @@ -525,6 +554,29 @@ const width = this.canvas.width / this.resolution; const height = this.canvas.height / this.resolution; + // make a copy of the style settings, so we can manipulate them later + const fill = style.fill.slice(); + const fillGradientStops = style.fillGradientStops.slice(); + + // wanting to evenly distribute the fills. So an array of 4 colours should give fills of 0.25, 0.5 and 0.75 + if (!fillGradientStops.length) + { + const lengthPlus1 = fill.length + 1; + + for (let i = 1; i < lengthPlus1; ++i) + { + fillGradientStops.push(i / lengthPlus1); + } + } + + // stop the bleeding of the last gradient on the line above to the top gradient of the this line + // by hard defining the first gradient colour at point 0, and last gradient colour at point 1 + fill.unshift(style.fill[0]); + fillGradientStops.unshift(0); + + fill.push(style.fill[style.fill.length - 1]); + fillGradientStops.push(1); + if (style.fillGradientType === TEXT_GRADIENT.LINEAR_VERTICAL) { // start the gradient at the top center of the canvas, and end at the bottom middle of the canvas @@ -532,15 +584,22 @@ // we need to repeat the gradient so that each individual line of text has the same vertical gradient effect // ['#FF0000', '#00FF00', '#0000FF'] over 2 lines would create stops at 0.125, 0.25, 0.375, 0.625, 0.75, 0.875 - totalIterations = (style.fill.length + 1) * lines.length; + totalIterations = (fill.length + 1) * lines.length; currentIteration = 0; for (let i = 0; i < lines.length; i++) { currentIteration += 1; - for (let j = 0; j < style.fill.length; j++) + for (let j = 0; j < fill.length; j++) { - stop = (currentIteration / totalIterations); - gradient.addColorStop(stop, style.fill[j]); + if (fillGradientStops[j]) + { + stop = (fillGradientStops[j] / lines.length) + (i / lines.length); + } + else + { + stop = currentIteration / totalIterations; + } + gradient.addColorStop(stop, fill[j]); currentIteration++; } } @@ -552,13 +611,20 @@ // can just evenly space out the gradients in this case, as multiple lines makes no difference // to an even left to right gradient - totalIterations = style.fill.length + 1; + totalIterations = fill.length + 1; currentIteration = 1; - for (let i = 0; i < style.fill.length; i++) + for (let i = 0; i < fill.length; i++) { - stop = currentIteration / totalIterations; - gradient.addColorStop(stop, style.fill[i]); + if (fillGradientStops[i]) + { + stop = fillGradientStops[i]; + } + else + { + stop = currentIteration / totalIterations; + } + gradient.addColorStop(stop, fill[i]); currentIteration++; } } @@ -569,7 +635,7 @@ /** * Destroys this text object. * Note* Unlike a Sprite, a Text object will automatically destroy its baseTexture and texture as - * the majorety of the time the texture will not be shared with any other Sprites. + * the majority of the time the texture will not be shared with any other Sprites. * * @param {object|boolean} [options] - Options parameter. A boolean will act as if all options * have been set to that value @@ -680,7 +746,7 @@ set text(text) // eslint-disable-line require-jsdoc { - text = String(text || ' '); + text = String(text === '' || text === null || text === undefined ? ' ' : text); if (this._text === text) { diff --git a/src/core/text/TextStyle.js b/src/core/text/TextStyle.js index 3b25528..890950b 100644 --- a/src/core/text/TextStyle.js +++ b/src/core/text/TextStyle.js @@ -8,12 +8,14 @@ align: 'left', breakWords: false, dropShadow: false, + dropShadowAlpha: 1, dropShadowAngle: Math.PI / 6, dropShadowBlur: 0, dropShadowColor: '#000000', dropShadowDistance: 5, fill: 'black', fillGradientType: TEXT_GRADIENT.LINEAR_VERTICAL, + fillGradientStops: [], fontFamily: 'Arial', fontSize: 26, fontStyle: 'normal', @@ -27,6 +29,7 @@ stroke: 'black', strokeThickness: 0, textBaseline: 'alphabetic', + trim: false, wordWrap: false, wordWrapWidth: 100, }; @@ -47,6 +50,7 @@ * @param {boolean} [style.breakWords=false] - Indicates if lines can be wrapped within words, it * needs wordWrap to be set to true * @param {boolean} [style.dropShadow=false] - Set a drop shadow for the text + * @param {number} [style.dropShadowAlpha=1] - Set alpha for the drop shadow * @param {number} [style.dropShadowAngle=Math.PI/6] - Set a angle of the drop shadow * @param {number} [style.dropShadowBlur=0] - Set a shadow blur radius * @param {string} [style.dropShadowColor='#000000'] - A fill style to be used on the dropshadow e.g 'red', '#00FF00' @@ -55,8 +59,10 @@ * fillstyle that will be used on the text e.g 'red', '#00FF00'. Can be an array to create a gradient * eg ['#000000','#FFFFFF'] * {@link https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/fillStyle|MDN} - * @param {number} [style.fillGradientType=PIXI.TEXT_GRADIENT.LINEAR_VERTICAL] - If fills styles are - * supplied, this can change the type/direction of the gradient. See {@link PIXI.TEXT_GRADIENT} for possible values + * @param {number} [style.fillGradientType=PIXI.TEXT_GRADIENT.LINEAR_VERTICAL] - If fill is an array of colours + * to create a gradient, this can change the type/direction of the gradient. See {@link PIXI.TEXT_GRADIENT} + * @param {number[]} [style.fillGradientStops] - If fill is an array of colours to create a gradient, this array can set + * the stop points (numbers between 0 and 1) for the color, overriding the default behaviour of evenly spacing them. * @param {string|string[]} [style.fontFamily='Arial'] - The font family * @param {number|string} [style.fontSize=26] - The font size (as a number it converts to px, but as a string, * equivalents are '26px','20pt','160%' or '1.6em') @@ -76,6 +82,7 @@ * e.g 'blue', '#FCFF00' * @param {number} [style.strokeThickness=0] - A number that represents the thickness of the stroke. * Default is 0 (no stroke) + * @param {boolean} [style.trim=false] - Trim transparent borders * @param {string} [style.textBaseline='alphabetic'] - The baseline of the text that is rendered. * @param {boolean} [style.wordWrap=false] - Indicates if word wrap should be used * @param {number} [style.wordWrapWidth=100] - The width at which text will wrap, it needs wordWrap to be set to true @@ -152,6 +159,19 @@ } } + get dropShadowAlpha() + { + return this._dropShadowAlpha; + } + set dropShadowAlpha(dropShadowAlpha) + { + if (this._dropShadowAlpha !== dropShadowAlpha) + { + this._dropShadowAlpha = dropShadowAlpha; + this.styleID++; + } + } + get dropShadowAngle() { return this._dropShadowAngle; @@ -232,6 +252,19 @@ } } + get fillGradientStops() + { + return this._fillGradientStops; + } + set fillGradientStops(fillGradientStops) + { + if (!areArraysEqual(this._fillGradientStops,fillGradientStops)) + { + this._fillGradientStops = fillGradientStops; + this.styleID++; + } + } + get fontFamily() { return this._fontFamily; @@ -402,6 +435,19 @@ } } + get trim() + { + return this._trim; + } + set trim(trim) + { + if (this._trim !== trim) + { + this._trim = trim; + this.styleID++; + } + } + get wordWrap() { return this._wordWrap; @@ -475,3 +521,34 @@ return color; } } + +/** + * Utility function to convert hexadecimal colors to strings, and simply return the color if it's a string. + * This version can also convert array of colors + * + * @param {Array} array1 First array to compare + * @param {Array} array2 Second array to compare + * @return {boolean} Do the arrays contain the same values in the same order + */ +function areArraysEqual(array1, array2) +{ + if (!Array.isArray(array1) || !Array.isArray(array2)) + { + return false; + } + + if (array1.length !== array2.length) + { + return false; + } + + for (let i = 0; i < array1.length; ++i) + { + if (array1[i] !== array2[i]) + { + return false; + } + } + + return true; +} diff --git a/src/core/textures/BaseRenderTexture.js b/src/core/textures/BaseRenderTexture.js index 5d50146..aa727f0 100644 --- a/src/core/textures/BaseRenderTexture.js +++ b/src/core/textures/BaseRenderTexture.js @@ -1,8 +1,6 @@ import BaseTexture from './BaseTexture'; import settings from '../settings'; -const { RESOLUTION, SCALE_MODE } = settings; - /** * A BaseRenderTexture is a special texture that allows any Pixi display object to be rendered to it. * @@ -54,7 +52,7 @@ { super(null, scaleMode); - this.resolution = resolution || RESOLUTION; + this.resolution = resolution || settings.RESOLUTION; this.width = width; this.height = height; @@ -62,7 +60,7 @@ this.realWidth = this.width * this.resolution; this.realHeight = this.height * this.resolution; - this.scaleMode = scaleMode || SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; this.hasLoaded = true; /** diff --git a/src/core/textures/BaseTexture.js b/src/core/textures/BaseTexture.js index 676dca3..aeccf39 100644 --- a/src/core/textures/BaseTexture.js +++ b/src/core/textures/BaseTexture.js @@ -77,7 +77,7 @@ * @default PIXI.settings.SCALE_MODE * @see PIXI.SCALE_MODES */ - this.scaleMode = scaleMode || settings.SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; /** * Set to true once the base texture has successfully loaded. @@ -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/RenderTexture.js b/src/core/textures/RenderTexture.js index 4c5034f..b3c6297 100644 --- a/src/core/textures/RenderTexture.js +++ b/src/core/textures/RenderTexture.js @@ -54,8 +54,8 @@ /* eslint-disable prefer-rest-params, no-console */ const width = arguments[1]; const height = arguments[2]; - const scaleMode = arguments[3] || 0; - const resolution = arguments[4] || 1; + const scaleMode = arguments[3]; + const resolution = arguments[4]; // we have an old render texture.. console.warn(`Please use RenderTexture.create(${width}, ${height}) instead of the ctor directly.`); 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 ca31456..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 @@ -459,7 +497,9 @@ if (frame.x + frame.width > this.baseTexture.width || frame.y + frame.height > this.baseTexture.height) { - throw new Error(`Texture Error: frame does not fit inside the base Texture dimensions ${this}`); + throw new Error('Texture Error: frame does not fit inside the base Texture dimensions: ' + + `X: ${frame.x} + ${frame.width} > ${this.baseTexture.width} ` + + `Y: ${frame.y} + ${frame.height} > ${this.baseTexture.height}`); } // this.valid = frame && frame.width && frame.height && this.baseTexture.source && this.baseTexture.hasLoaded; @@ -520,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. @@ -528,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 2a7497b..99e585a 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)); } /** @@ -360,3 +362,43 @@ * @private */ export const BaseTextureCache = {}; + +/** + * 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/core/utils/trimCanvas.js b/src/core/utils/trimCanvas.js new file mode 100644 index 0000000..5af6de0 --- /dev/null +++ b/src/core/utils/trimCanvas.js @@ -0,0 +1,83 @@ +/** + * Trim transparent borders from a canvas + * + * @memberof PIXI + * @function trimCanvas + * @private + * @param {HTMLCanvasElement} canvas - the canvas to trim + * @returns {object} Trim data + */ +export default function trimCanvas(canvas) +{ + // https://gist.github.com/remy/784508 + + let width = canvas.width; + let height = canvas.height; + + const context = canvas.getContext('2d'); + const imageData = context.getImageData(0, 0, width, height); + const pixels = imageData.data; + const len = pixels.length; + + const bound = { + top: null, + left: null, + right: null, + bottom: null, + }; + let i; + let x; + let y; + + for (i = 0; i < len; i += 4) + { + if (pixels[i + 3] !== 0) + { + x = (i / 4) % width; + y = ~~((i / 4) / width); + + if (bound.top === null) + { + bound.top = y; + } + + if (bound.left === null) + { + bound.left = x; + } + else if (x < bound.left) + { + bound.left = x; + } + + if (bound.right === null) + { + bound.right = x + 1; + } + else if (bound.right < x) + { + bound.right = x + 1; + } + + if (bound.bottom === null) + { + bound.bottom = y; + } + else if (bound.bottom < y) + { + bound.bottom = y; + } + } + } + + width = bound.right - bound.left; + height = bound.bottom - bound.top + 1; + + const data = context.getImageData(bound.left, bound.top, width, height); + + return { + height, + width, + data, + }; +} diff --git a/src/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/extras/TilingSprite.js b/src/extras/TilingSprite.js index f90d248..fe82451 100644 --- a/src/extras/TilingSprite.js +++ b/src/extras/TilingSprite.js @@ -220,20 +220,33 @@ transform.tx * resolution, transform.ty * resolution); - // TODO - this should be rolled into the setTransform above.. - context.scale(this.tileScale.x / baseTextureResolution, this.tileScale.y / baseTextureResolution); - - context.translate(modX + (this.anchor.x * -this._width), - modY + (this.anchor.y * -this._height)); - renderer.setBlendMode(this.blendMode); // fill the pattern! context.fillStyle = this._canvasPattern; - context.fillRect(-modX, - -modY, - this._width / this.tileScale.x * baseTextureResolution, - this._height / this.tileScale.y * baseTextureResolution); + + // TODO - this should be rolled into the setTransform above.. + context.scale(this.tileScale.x / baseTextureResolution, this.tileScale.y / baseTextureResolution); + + const anchorX = this.anchor.x * -this._width; + const anchorY = this.anchor.y * -this._height; + + if (this.uvRespectAnchor) + { + context.translate(modX, modY); + + context.fillRect(-modX + anchorX, -modY + anchorY, + this._width / this.tileScale.x * baseTextureResolution, + this._height / this.tileScale.y * baseTextureResolution); + } + else + { + context.translate(modX + anchorX, modY + anchorY); + + context.fillRect(-modX, -modY, + this._width / this.tileScale.x * baseTextureResolution, + this._height / this.tileScale.y * baseTextureResolution); + } } /** diff --git a/src/extras/cacheAsBitmap.js b/src/extras/cacheAsBitmap.js index 18043a7..5dd5640 100644 --- a/src/extras/cacheAsBitmap.js +++ b/src/extras/cacheAsBitmap.js @@ -41,6 +41,9 @@ * provide a performance benefit for complex static displayObjects. * To remove simply set this property to 'false' * + * IMPORTANT GOTCHA - make sure that all your textures are preloaded BEFORE setting this property to true + * as it will take a snapshot of what is currently there. If the textures have not loaded then they will not appear. + * * @member {boolean} * @memberof PIXI.DisplayObject# */ diff --git a/src/filters/blur/BlurFilter.js b/src/filters/blur/BlurFilter.js index 340fc58..1575db9 100644 --- a/src/filters/blur/BlurFilter.js +++ b/src/filters/blur/BlurFilter.js @@ -24,10 +24,9 @@ this.blurXFilter = new BlurXFilter(strength, quality, resolution, kernelSize); this.blurYFilter = new BlurYFilter(strength, quality, resolution, kernelSize); - this.resolution = 1; this.padding = 0; - this.resolution = resolution || 1; + this.resolution = resolution || core.settings.RESOLUTION; this.quality = quality || 4; this.blur = strength || 8; } diff --git a/src/filters/blur/BlurXFilter.js b/src/filters/blur/BlurXFilter.js index def99c3..0f48889 100644 --- a/src/filters/blur/BlurXFilter.js +++ b/src/filters/blur/BlurXFilter.js @@ -31,7 +31,7 @@ fragSrc ); - this.resolution = resolution || 1; + this.resolution = resolution || core.settings.RESOLUTION; this._quality = 0; diff --git a/src/filters/blur/BlurYFilter.js b/src/filters/blur/BlurYFilter.js index 638e0f0..6d489c7 100644 --- a/src/filters/blur/BlurYFilter.js +++ b/src/filters/blur/BlurYFilter.js @@ -31,7 +31,7 @@ fragSrc ); - this.resolution = resolution || 1; + this.resolution = resolution || core.settings.RESOLUTION; this._quality = 0; diff --git a/src/filters/colormatrix/ColorMatrixFilter.js b/src/filters/colormatrix/ColorMatrixFilter.js index 8509505..679f8ec 100644 --- a/src/filters/colormatrix/ColorMatrixFilter.js +++ b/src/filters/colormatrix/ColorMatrixFilter.js @@ -75,28 +75,28 @@ out[1] = (a[0] * b[1]) + (a[1] * b[6]) + (a[2] * b[11]) + (a[3] * b[16]); out[2] = (a[0] * b[2]) + (a[1] * b[7]) + (a[2] * b[12]) + (a[3] * b[17]); out[3] = (a[0] * b[3]) + (a[1] * b[8]) + (a[2] * b[13]) + (a[3] * b[18]); - out[4] = (a[0] * b[4]) + (a[1] * b[9]) + (a[2] * b[14]) + (a[3] * b[19]); + out[4] = (a[0] * b[4]) + (a[1] * b[9]) + (a[2] * b[14]) + (a[3] * b[19]) + a[4]; // Green Channel out[5] = (a[5] * b[0]) + (a[6] * b[5]) + (a[7] * b[10]) + (a[8] * b[15]); out[6] = (a[5] * b[1]) + (a[6] * b[6]) + (a[7] * b[11]) + (a[8] * b[16]); out[7] = (a[5] * b[2]) + (a[6] * b[7]) + (a[7] * b[12]) + (a[8] * b[17]); out[8] = (a[5] * b[3]) + (a[6] * b[8]) + (a[7] * b[13]) + (a[8] * b[18]); - out[9] = (a[5] * b[4]) + (a[6] * b[9]) + (a[7] * b[14]) + (a[8] * b[19]); + out[9] = (a[5] * b[4]) + (a[6] * b[9]) + (a[7] * b[14]) + (a[8] * b[19]) + a[9]; // Blue Channel out[10] = (a[10] * b[0]) + (a[11] * b[5]) + (a[12] * b[10]) + (a[13] * b[15]); out[11] = (a[10] * b[1]) + (a[11] * b[6]) + (a[12] * b[11]) + (a[13] * b[16]); out[12] = (a[10] * b[2]) + (a[11] * b[7]) + (a[12] * b[12]) + (a[13] * b[17]); out[13] = (a[10] * b[3]) + (a[11] * b[8]) + (a[12] * b[13]) + (a[13] * b[18]); - out[14] = (a[10] * b[4]) + (a[11] * b[9]) + (a[12] * b[14]) + (a[13] * b[19]); + out[14] = (a[10] * b[4]) + (a[11] * b[9]) + (a[12] * b[14]) + (a[13] * b[19]) + a[14]; // Alpha Channel out[15] = (a[15] * b[0]) + (a[16] * b[5]) + (a[17] * b[10]) + (a[18] * b[15]); out[16] = (a[15] * b[1]) + (a[16] * b[6]) + (a[17] * b[11]) + (a[18] * b[16]); out[17] = (a[15] * b[2]) + (a[16] * b[7]) + (a[17] * b[12]) + (a[18] * b[17]); out[18] = (a[15] * b[3]) + (a[16] * b[8]) + (a[17] * b[13]) + (a[18] * b[18]); - out[19] = (a[15] * b[4]) + (a[16] * b[9]) + (a[17] * b[14]) + (a[18] * b[19]); + out[19] = (a[15] * b[4]) + (a[16] * b[9]) + (a[17] * b[14]) + (a[18] * b[19]) + a[19]; return out; } diff --git a/src/filters/colormatrix/colorMatrix.frag b/src/filters/colormatrix/colorMatrix.frag index 3aaf2b3..c73c0e9 100644 --- a/src/filters/colormatrix/colorMatrix.frag +++ b/src/filters/colormatrix/colorMatrix.frag @@ -4,32 +4,38 @@ void main(void) { - vec4 c = texture2D(uSampler, vTextureCoord); + // Un-premultiply alpha before applying the color matrix. See issue #3539. + if (c.a > 0.0) { + c.rgb /= c.a; + } + vec4 result; + result.r = (m[0] * c.r); + result.r += (m[1] * c.g); + result.r += (m[2] * c.b); + result.r += (m[3] * c.a); + result.r += m[4]; - gl_FragColor.r = (m[0] * c.r); - gl_FragColor.r += (m[1] * c.g); - gl_FragColor.r += (m[2] * c.b); - gl_FragColor.r += (m[3] * c.a); - gl_FragColor.r += m[4] * c.a; + result.g = (m[5] * c.r); + result.g += (m[6] * c.g); + result.g += (m[7] * c.b); + result.g += (m[8] * c.a); + result.g += m[9]; - gl_FragColor.g = (m[5] * c.r); - gl_FragColor.g += (m[6] * c.g); - gl_FragColor.g += (m[7] * c.b); - gl_FragColor.g += (m[8] * c.a); - gl_FragColor.g += m[9] * c.a; + result.b = (m[10] * c.r); + result.b += (m[11] * c.g); + result.b += (m[12] * c.b); + result.b += (m[13] * c.a); + result.b += m[14]; - gl_FragColor.b = (m[10] * c.r); - gl_FragColor.b += (m[11] * c.g); - gl_FragColor.b += (m[12] * c.b); - gl_FragColor.b += (m[13] * c.a); - gl_FragColor.b += m[14] * c.a; + result.a = (m[15] * c.r); + result.a += (m[16] * c.g); + result.a += (m[17] * c.b); + result.a += (m[18] * c.a); + result.a += m[19]; - gl_FragColor.a = (m[15] * c.r); - gl_FragColor.a += (m[16] * c.g); - gl_FragColor.a += (m[17] * c.b); - gl_FragColor.a += (m[18] * c.a); - gl_FragColor.a += m[19] * c.a; + // Premultiply alpha again. + result.rgb *= result.a; -// gl_FragColor = vec4(m[0]); + gl_FragColor = result; } diff --git a/src/filters/displacement/DisplacementFilter.js b/src/filters/displacement/DisplacementFilter.js index 112de0b..d5355c3 100644 --- a/src/filters/displacement/DisplacementFilter.js +++ b/src/filters/displacement/DisplacementFilter.js @@ -36,7 +36,7 @@ this.maskMatrix = maskMatrix; this.uniforms.mapSampler = sprite.texture; - this.uniforms.filterMatrix = maskMatrix.toArray(true); + this.uniforms.filterMatrix = maskMatrix; this.uniforms.scale = { x: 1, y: 1 }; if (scale === null || scale === undefined) diff --git a/src/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/InteractionData.js b/src/interaction/InteractionData.js index 02d53a3..818da35 100644 --- a/src/interaction/InteractionData.js +++ b/src/interaction/InteractionData.js @@ -21,9 +21,9 @@ this.global = new core.Point(); /** - * The target Sprite that was interacted with + * The target DisplayObject that was interacted with * - * @member {PIXI.Sprite} + * @member {PIXI.DisplayObject} */ this.target = null; @@ -33,6 +33,13 @@ * @member {Event} */ this.originalEvent = null; + + /** + * Unique identifier for this interaction + * + * @member {number} + */ + this.identifier = null; } /** diff --git a/src/interaction/InteractionEvent.js b/src/interaction/InteractionEvent.js index 75b2966..dc554f9 100644 --- a/src/interaction/InteractionEvent.js +++ b/src/interaction/InteractionEvent.js @@ -58,7 +58,7 @@ } /** - * Prevents event from reaching any objects other than the current object. + * Resets the event. * * @private */ diff --git a/src/interaction/InteractionManager.js b/src/interaction/InteractionManager.js index 7d51134..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/src/prepare/BasePrepare.js b/src/prepare/BasePrepare.js index a2005f5..09eb5e5 100644 --- a/src/prepare/BasePrepare.js +++ b/src/prepare/BasePrepare.js @@ -20,7 +20,7 @@ * * @abstract * @class - * @memberof PIXI + * @memberof PIXI.prepare */ export default class BasePrepare { @@ -107,9 +107,9 @@ /** * Upload all the textures and graphics to the GPU. * - * @param {Function|PIXI.DisplayObject|PIXI.Container} item - Either - * the container or display object to search for items to upload or - * the callback function, if items have been added using `prepare.add`. + * @param {Function|PIXI.DisplayObject|PIXI.Container|PIXI.BaseTexture|PIXI.Texture|PIXI.Graphics|PIXI.Text} item - + * Either the container or display object to search for items to upload, the items to upload themselves, + * or the callback function, if items have been added using `prepare.add`. * @param {Function} [done] - Optional callback when all queued uploads have completed */ upload(item, done) @@ -236,7 +236,8 @@ /** * Manually add an item to the uploading queue. * - * @param {PIXI.DisplayObject|PIXI.Container|*} item - Object to add to the queue + * @param {PIXI.DisplayObject|PIXI.Container|PIXI.BaseTexture|PIXI.Texture|PIXI.Graphics|PIXI.Text|*} item - Object to + * add to the queue * @return {PIXI.CanvasPrepare} Instance of plugin for chaining. */ add(item) diff --git a/src/prepare/canvas/CanvasPrepare.js b/src/prepare/canvas/CanvasPrepare.js index 4b99d22..0224bb5 100644 --- a/src/prepare/canvas/CanvasPrepare.js +++ b/src/prepare/canvas/CanvasPrepare.js @@ -12,7 +12,8 @@ * An instance of this class is automatically created by default, and can be found at renderer.plugins.prepare * * @class - * @memberof PIXI + * @extends PIXI.prepare.BasePrepare + * @memberof PIXI.prepare */ export default class CanvasPrepare extends BasePrepare { diff --git a/src/prepare/webgl/WebGLPrepare.js b/src/prepare/webgl/WebGLPrepare.js index 1ef7467..eb7023f 100644 --- a/src/prepare/webgl/WebGLPrepare.js +++ b/src/prepare/webgl/WebGLPrepare.js @@ -7,7 +7,8 @@ * An instance of this class is automatically created by default, and can be found at renderer.plugins.prepare * * @class - * @memberof PIXI + * @extends PIXI.prepare.BasePrepare + * @memberof PIXI.prepare */ export default class WebGLPrepare extends BasePrepare { diff --git a/test/core/Circle.js b/test/core/Circle.js index 183f98f..f621992 100644 --- a/test/core/Circle.js +++ b/test/core/Circle.js @@ -53,6 +53,10 @@ expect(circ1.contains(10, 16)).to.be.false; expect(circ1.contains(11, 15)).to.be.false; expect(circ1.contains(0, 0)).to.be.false; + + const circ2 = new PIXI.Circle(10, 10, 0); + + expect(circ2.contains(10, 10)).to.be.false; }); it('should return framing rectangle', function () diff --git a/test/core/Container.js b/test/core/Container.js index 8a10b40..cefde54 100644 --- a/test/core/Container.js +++ b/test/core/Container.js @@ -1,5 +1,39 @@ 'use strict'; +function testAddChild(fn) +{ + return function () + { + fn(function (container, obj) + { + container.addChild(obj); + }); + fn(function (container, obj) + { + container.addChildAt(obj); + }); + }; +} + +function testRemoveChild(fn) +{ + return function () + { + 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 +104,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 +279,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/Ellipse.js b/test/core/Ellipse.js index f2e8234..3f47442 100644 --- a/test/core/Ellipse.js +++ b/test/core/Ellipse.js @@ -56,6 +56,10 @@ expect(ellipse1.contains(10, 16)).to.be.false; expect(ellipse1.contains(11, 15)).to.be.false; expect(ellipse1.contains(0, 0)).to.be.false; + + const ellipse2 = new PIXI.Ellipse(10, 10, 0, 0); + + expect(ellipse2.contains(10, 10)).to.be.false; }); it('should return framing rectangle', function () diff --git a/test/core/Graphics.js b/test/core/Graphics.js index 2a64946..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 () @@ -127,6 +130,16 @@ expect(graphics.containsPoint(point)).to.be.false; }); + + it('should return false when no fill', function () + { + const point = new PIXI.Point(1, 1); + const graphics = new PIXI.Graphics(); + + graphics.drawRect(0, 0, 10, 10); + + expect(graphics.containsPoint(point)).to.be.false; + }); }); describe('arc', function () @@ -208,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/Matrix.js b/test/core/Matrix.js index 9d2694b..76bd57c 100644 --- a/test/core/Matrix.js +++ b/test/core/Matrix.js @@ -107,10 +107,60 @@ expect(m1.ty).to.equal(m2.ty); }); + it('should prepend matrix', function () + { + const m1 = new PIXI.Matrix(); + const m2 = new PIXI.Matrix(); + + m2.set(2, 3, 4, 5, 100, 200); + m1.prepend(m2); + + expect(m1.a).to.equal(m2.a); + expect(m1.b).to.equal(m2.b); + expect(m1.c).to.equal(m2.c); + expect(m1.d).to.equal(m2.d); + expect(m1.tx).to.equal(m2.tx); + expect(m1.ty).to.equal(m2.ty); + + const m3 = new PIXI.Matrix(); + const m4 = new PIXI.Matrix(); + + m3.prepend(m4); + + expect(m3.a).to.equal(m4.a); + expect(m3.b).to.equal(m4.b); + expect(m3.c).to.equal(m4.c); + expect(m3.d).to.equal(m4.d); + expect(m3.tx).to.equal(m4.tx); + expect(m3.ty).to.equal(m4.ty); + }); + it('should get IDENTITY and TEMP_MATRIX', function () { expect(PIXI.Matrix.IDENTITY instanceof PIXI.Matrix).to.be.true; expect(PIXI.Matrix.TEMP_MATRIX instanceof PIXI.Matrix).to.be.true; }); -}); + it('should reset matrix to default when identity() is called', function () + { + const matrix = new PIXI.Matrix(); + + matrix.set(2, 3, 4, 5, 100, 200); + + expect(matrix.a).to.equal(2); + expect(matrix.b).to.equal(3); + expect(matrix.c).to.equal(4); + expect(matrix.d).to.equal(5); + expect(matrix.tx).to.equal(100); + expect(matrix.ty).to.equal(200); + + matrix.identity(); + + expect(matrix.a).to.equal(1); + expect(matrix.b).to.equal(0); + expect(matrix.c).to.equal(0); + expect(matrix.d).to.equal(1); + expect(matrix.tx).to.equal(0); + expect(matrix.ty).to.equal(0); + }); +}); 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/Text.js b/test/core/Text.js index b8ce561..5799ad4 100644 --- a/test/core/Text.js +++ b/test/core/Text.js @@ -112,4 +112,42 @@ expect(text.width).to.equal(300); }); }); + + describe('text', function () + { + it('should convert numbers into strings', function () + { + const text = new PIXI.Text(2); + + expect(text.text).to.equal('2'); + }); + + it('should not change 0 to \'\'', function () + { + const text = new PIXI.Text(0); + + expect(text.text).to.equal('0'); + }); + + it('should prevent setting null', function () + { + const text = new PIXI.Text(null); + + expect(text.text).to.equal(' '); + }); + + it('should prevent setting undefined', function () + { + const text = new PIXI.Text(); + + expect(text.text).to.equal(' '); + }); + + it('should prevent setting \'\'', function () + { + const text = new PIXI.Text(''); + + expect(text.text).to.equal(' '); + }); + }); }); diff --git a/test/core/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/TilingSprite.js b/test/core/TilingSprite.js index 2e449a7..7694792 100644 --- a/test/core/TilingSprite.js +++ b/test/core/TilingSprite.js @@ -24,4 +24,25 @@ expect(bounds.height).to.equal(600); }); }); + + it('checks if tilingSprite contains a point', function () + { + const texture = new PIXI.Texture(new PIXI.BaseTexture()); + const tilingSprite = new PIXI.extras.TilingSprite(texture, 200, 300); + + expect(tilingSprite.containsPoint(new PIXI.Point(1, 1))).to.equal(true); + expect(tilingSprite.containsPoint(new PIXI.Point(300, 400))).to.equal(false); + }); + + it('gets and sets height and width correctly', function () + { + const texture = new PIXI.Texture(new PIXI.BaseTexture()); + const tilingSprite = new PIXI.extras.TilingSprite(texture, 200, 300); + + tilingSprite.width = 400; + tilingSprite.height = 600; + + expect(tilingSprite.width).to.equal(400); + expect(tilingSprite.height).to.equal(600); + }); }); diff --git a/test/core/WebGLRenderer.js b/test/core/WebGLRenderer.js new file mode 100644 index 0000000..3d6cf98 --- /dev/null +++ b/test/core/WebGLRenderer.js @@ -0,0 +1,21 @@ +'use strict'; + +const withGL = require('../withGL'); + +describe('PIXI.WebGLRenderer', function () +{ + it('setting option legacy should disable VAOs and SPRITE_MAX_TEXTURES', withGL(function () + { + const renderer = new PIXI.WebGLRenderer(1, 1, { legacy: true }); + + 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/getLocalBounds.js b/test/core/getLocalBounds.js index d160a56..309da0e 100644 --- a/test/core/getLocalBounds.js +++ b/test/core/getLocalBounds.js @@ -196,4 +196,13 @@ expect(bounds.width).to.equal(100); expect(bounds.height).to.equal(100); }); + + it('should register correct local-bounds with a Text', function () + { + const text = new PIXI.Text('hello'); + const bounds = text.getLocalBounds(); + + expect(bounds.width).to.not.equal(0); + expect(bounds.height).to.not.equal(0); + }); }); diff --git a/test/core/index.js b/test/core/index.js index 2a056a2..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'); @@ -23,4 +24,7 @@ require('./Circle'); require('./Graphics'); 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 1271fef..71ade15 100644 --- a/package.json +++ b/package.json @@ -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/scripts/release.js b/scripts/release.js index c4c8e36..156620c 100644 --- a/scripts/release.js +++ b/scripts/release.js @@ -12,7 +12,8 @@ 'dist/**/*', 'lib/**/*', 'src/**/*', - 'scripts/**/*', + 'scripts/*', + 'scripts/renders/*', 'test/**/*', '*.json', '*.md', diff --git a/scripts/renders/client.js b/scripts/renders/client.js index 16c9565..6faee34 100755 --- a/scripts/renders/client.js +++ b/scripts/renders/client.js @@ -1,6 +1,6 @@ 'use strict'; -require('../../bin/pixi'); +require('../../dist/pixi'); PIXI.utils.skipHello(); const fs = require('fs'); 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 bc730db..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. @@ -33,8 +33,11 @@ * need to call toDataUrl on the webgl context * @param {number} [options.resolution=1] - The resolution / device pixel ratio of the renderer, retina would be 2 * @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 @@ -43,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. */ @@ -73,7 +99,7 @@ */ stop() { - this.ticker.stop(); + this._ticker.stop(); } /** @@ -81,7 +107,7 @@ */ start() { - this.ticker.start(); + this._ticker.start(); } /** @@ -95,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 index 81e076b..78b17e6 100644 --- a/src/core/Shader.js +++ b/src/core/Shader.js @@ -1,9 +1,7 @@ import { GLShader } from 'pixi-gl-core'; import settings from './settings'; -const { PRECISION } = settings; - -function checkPrecision(src) +function checkPrecision(src, def) { if (src instanceof Array) { @@ -11,14 +9,14 @@ { const copy = src.slice(0); - copy.unshift(`precision ${PRECISION} float;`); + copy.unshift(`precision ${def} float;`); return copy; } } else if (src.substring(0, 9) !== 'precision') { - return `precision ${PRECISION} float;\n${src}`; + return `precision ${def} float;\n${src}`; } return src; @@ -42,6 +40,7 @@ */ constructor(gl, vertexSrc, fragmentSrc) { - super(gl, checkPrecision(vertexSrc), checkPrecision(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 3425b41..f9235e4 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -181,7 +181,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 b3792ef..17fde8d 100644 --- a/src/core/display/Container.js +++ b/src/core/display/Container.js @@ -73,13 +73,14 @@ } child.parent = this; - - // ensure a transform will be recalculated.. - this.transform._parentID = -1; - this._boundsID++; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.push(child); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(this.children.length - 1); child.emit('added', this); @@ -108,9 +109,14 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.splice(index, 0, child); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(index); child.emit('added', this); @@ -174,6 +180,7 @@ removeItems(this.children, currentIndex, 1); // remove from old position this.children.splice(index, 0, child); // add at new position + this.onChildrenChange(index); } @@ -220,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! @@ -244,9 +252,14 @@ { const child = this.getChildAt(index); + // ensure child transform will be recalculated.. child.parent = null; + child.transform._parentID = -1; removeItems(this.children, index, 1); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(index); child.emit('removed', this); @@ -275,8 +288,14 @@ for (let i = 0; i < removed.length; ++i) { removed[i].parent = null; + if (removed[i].transform) + { + removed[i].transform._parentID = -1; + } } + this._boundsID++; + this.onChildrenChange(beginIndex); for (let i = 0; i < removed.length; ++i) diff --git a/src/core/display/DisplayObject.js b/src/core/display/DisplayObject.js index 3affe36..4e6c77d 100644 --- a/src/core/display/DisplayObject.js +++ b/src/core/display/DisplayObject.js @@ -109,7 +109,7 @@ /** * The original, cached mask of the object * - * @member {PIXI.Rectangle} + * @member {PIXI.Graphics|PIXI.Sprite} * @private */ this._mask = null; diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index 95f2af4..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 ); @@ -1065,10 +1061,15 @@ canvasRenderer = new CanvasRenderer(); } - tempMatrix.tx = -bounds.x; - tempMatrix.ty = -bounds.y; + this.transform.updateLocalTransform(); + this.transform.localTransform.copy(tempMatrix); - canvasRenderer.render(this, canvasBuffer, false, tempMatrix); + tempMatrix.invert(); + + tempMatrix.tx -= bounds.x; + tempMatrix.ty -= bounds.y; + + canvasRenderer.render(this, canvasBuffer, true, tempMatrix); const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode); 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 e67680f..867dde2 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/buildCircle.js b/src/core/graphics/webgl/utils/buildCircle.js index 0400d13..abf5d02 100644 --- a/src/core/graphics/webgl/utils/buildCircle.js +++ b/src/core/graphics/webgl/utils/buildCircle.js @@ -33,6 +33,11 @@ height = circleData.height; } + if (width === 0 || height === 0) + { + return; + } + const totalSegs = Math.floor(30 * Math.sqrt(circleData.radius)) || Math.floor(15 * Math.sqrt(circleData.width + circleData.height)); diff --git a/src/core/graphics/webgl/utils/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 90155f9..f03fe15 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -30,6 +30,7 @@ export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; export { default as CanvasGraphicsRenderer } from './graphics/canvas/CanvasGraphicsRenderer'; +export { default as Spritesheet } from './textures/Spritesheet'; export { default as Texture } from './textures/Texture'; export { default as BaseTexture } from './textures/BaseTexture'; export { default as RenderTexture } from './textures/RenderTexture'; diff --git a/src/core/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 ccce680..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); } /** @@ -162,6 +162,9 @@ if (transform) { transform.copy(tempWt); + + // lets not forget to flag the parent transform as dirty... + this._tempDisplayObjectParent.transform._worldID = -1; } else { @@ -169,6 +172,7 @@ } displayObject.parent = this._tempDisplayObjectParent; + displayObject.updateTransform(); displayObject.parent = cacheParent; // displayObject.hitArea = //TODO add a temp hit area @@ -279,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/canvas/utils/CanvasRenderTarget.js b/src/core/renderers/canvas/utils/CanvasRenderTarget.js index 3de0aef..b409891 100644 --- a/src/core/renderers/canvas/utils/CanvasRenderTarget.js +++ b/src/core/renderers/canvas/utils/CanvasRenderTarget.js @@ -1,5 +1,4 @@ import settings from '../../../settings'; -const { RESOLUTION } = settings; /** * Creates a Canvas element of the given size. @@ -30,7 +29,7 @@ */ this.context = this.canvas.getContext('2d'); - this.resolution = resolution || RESOLUTION; + this.resolution = resolution || settings.RESOLUTION; this.resize(width, height); } 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 ce0e1c4..788ed6c 100644 --- a/src/core/renderers/webgl/WebGLRenderer.js +++ b/src/core/renderers/webgl/WebGLRenderer.js @@ -30,8 +30,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 @@ -49,10 +49,19 @@ * enable this if you need to call toDataUrl on the webgl context. * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when * rendering, stopping pixel interpolation. + * @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; + + if (this.legacy) + { + glCore.VertexArrayObject.FORCE_NATIVE = true; + } /** * The type of this renderer as a standardised const @@ -229,7 +238,7 @@ this.emit('context', gl); // setup the width/height properties and gl viewport - this.resize(this.width, this.height); + this.resize(this.screen.width, this.screen.height); } /** @@ -323,16 +332,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) { @@ -376,6 +385,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 @@ -440,9 +469,10 @@ * Changes the current shader to the one given in parameter * * @param {PIXI.Shader} shader - the new shader + * @param {boolean} [autoProject=true] - Whether automatically set the projection matrix * @return {PIXI.WebGLRenderer} Returns itself. */ - bindShader(shader) + bindShader(shader, autoProject) { // TODO cache if (this._activeShader !== shader) @@ -450,8 +480,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 bbab3b3..944f41d 100644 --- a/src/core/renderers/webgl/filters/Filter.js +++ b/src/core/renderers/webgl/filters/Filter.js @@ -1,6 +1,7 @@ import extractUniformsFromSrc from './extractUniformsFromSrc'; import { uid } from '../../../utils'; import { BLEND_MODES } from '../../../const'; +import settings from '../../../settings'; const SOURCE_KEY_MAP = {}; @@ -10,7 +11,7 @@ * @memberof PIXI * @extends PIXI.Shader */ -class Filter +export default class Filter { /** * @param {string} [vertexSrc] - The source of the vertex shader. @@ -35,8 +36,6 @@ this.blendMode = BLEND_MODES.NORMAL; - // pull out the vertex and shader uniforms if they are not specified.. - // currently this does not extract structs only default types this.uniformData = uniforms || extractUniformsFromSrc(this.vertexSrc, this.fragmentSrc, 'projectionMatrix|uSampler'); /** @@ -80,7 +79,7 @@ * * @member {number} */ - this.resolution = 1; + this.resolution = settings.RESOLUTION; /** * If enabled is true the filter is applied, if false it will not. @@ -97,8 +96,11 @@ * @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 { // --- // // this.uniforms.filterMatrix = filterManager.calculateSpriteMatrix(tempMatrix, window.panda ); @@ -170,5 +172,3 @@ ].join('\n'); } } - -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 dc7072a..6c7422d 100644 --- a/src/core/renderers/webgl/managers/FilterManager.js +++ b/src/core/renderers/webgl/managers/FilterManager.js @@ -154,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 @@ -176,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; @@ -184,7 +184,7 @@ flop = t; } - filters[i].apply(this, flip, lastState.renderTarget, true); + filters[i].apply(this, flip, lastState.renderTarget, false, currentState); this.freePotRenderTarget(flip); this.freePotRenderTarget(flop); @@ -291,9 +291,13 @@ let textureCount = 1; let currentState; - if (shader.uniforms.data.filterArea) + // 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; @@ -306,9 +310,9 @@ // use this to clamp displaced texture coords so they belong to filterArea // see displacementFilter fragment shader for an example - if (shader.uniforms.data.filterClamp) + if (shader.uniforms.filterClamp) { - currentState = this.filterData.stack[this.filterData.index]; + currentState = currentState || this.filterData.stack[this.filterData.index]; const filterClamp = shader.uniforms.filterClamp; @@ -485,7 +489,7 @@ */ destroy() { - this.shaderCache = []; + this.shaderCache = {}; this.emptyPool(); } diff --git a/src/core/renderers/webgl/utils/RenderTarget.js b/src/core/renderers/webgl/utils/RenderTarget.js index afe51ab..6d2467f 100644 --- a/src/core/renderers/webgl/utils/RenderTarget.js +++ b/src/core/renderers/webgl/utils/RenderTarget.js @@ -124,7 +124,7 @@ * @default PIXI.settings.SCALE_MODE * @see PIXI.SCALE_MODES */ - this.scaleMode = scaleMode || settings.SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; /** * Whether this object is the root element or not 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 36ea5bb..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; @@ -346,8 +346,8 @@ /** * Gets the local bounds of the sprite object. * - * @param {Rectangle} rect - The output rectangle. - * @return {Rectangle} The bounds. + * @param {PIXI.Rectangle} rect - The output rectangle. + * @return {PIXI.Rectangle} The bounds. */ getLocalBounds(rect) { @@ -440,7 +440,7 @@ * * @static * @param {number|string|PIXI.BaseTexture|HTMLCanvasElement|HTMLVideoElement} source Source to create texture from - * @return {PIXI.Texture} 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 e77b39a..90b08c0 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++) @@ -103,11 +102,18 @@ { const gl = this.renderer.gl; - // step 1: first check max textures the GPU can handle. - this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); + if (this.renderer.legacy) + { + this.MAX_TEXTURES = 1; + } + else + { + // step 1: first check max textures the GPU can handle. + this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); - // step 2: check the maximum number of if statements the shader can have too.. - this.MAX_TEXTURES = checkMaxIfStatmentsInShader(this.MAX_TEXTURES, gl); + // step 2: check the maximum number of if statements the shader can have too.. + this.MAX_TEXTURES = checkMaxIfStatmentsInShader(this.MAX_TEXTURES, gl); + } const shader = this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); @@ -130,8 +136,12 @@ .addIndex(this.indexBuffer) .addAttribute(this.vertexBuffers[i], shader.attributes.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) .addAttribute(this.vertexBuffers[i], shader.attributes.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) - .addAttribute(this.vertexBuffers[i], shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4) - .addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + .addAttribute(this.vertexBuffers[i], shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); + + if (shader.attributes.aTextureId) + { + this.vaos[i].addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + } /* eslint-enable max-len */ } @@ -378,8 +388,12 @@ .addIndex(this.indexBuffer) .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) - .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4) - .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); + + if (this.shader.attributes.aTextureId) + { + this.vaos[this.vertexCount].addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + } /* eslint-enable max-len */ } diff --git a/src/core/text/Text.js b/src/core/text/Text.js index 013625a..cec8c32 100644 --- a/src/core/text/Text.js +++ b/src/core/text/Text.js @@ -6,6 +6,7 @@ import { TEXT_GRADIENT } from '../const'; import settings from '../settings'; import TextStyle from './TextStyle'; +import trimCanvas from '../utils/trimCanvas'; const defaultDestroyOptions = { texture: true, @@ -57,7 +58,7 @@ /** * The canvas 2d context that everything is drawn with - * @member {HTMLCanvasElement} + * @member {CanvasRenderingContext2D} */ this.context = this.canvas.getContext('2d'); @@ -189,10 +190,12 @@ if (style.dropShadow) { + this.context.shadowBlur = style.dropShadowBlur; + this.context.globalAlpha = style.dropShadowAlpha; + if (style.dropShadowBlur > 0) { this.context.shadowColor = style.dropShadowColor; - this.context.shadowBlur = style.dropShadowBlur; } else { @@ -237,6 +240,10 @@ } } + // reset the shadow blur and alpha that was set by the drop shadow, for the regular text + this.context.shadowBlur = 0; + this.context.globalAlpha = 1; + // set canvas text styles this.context.fillStyle = this._generateFillStyle(style, lines); @@ -326,6 +333,15 @@ */ updateTexture() { + if (this._style.trim) + { + const trimmed = trimCanvas(this.canvas); + + this.canvas.width = trimmed.width; + this.canvas.height = trimmed.height; + this.context.putImageData(trimmed.data, 0, 0); + } + const texture = this._texture; const style = this._style; @@ -475,6 +491,19 @@ } /** + * Gets the local bounds of the text object. + * + * @param {Rectangle} rect - The output rectangle. + * @return {Rectangle} The bounds. + */ + getLocalBounds(rect) + { + this.updateText(true); + + return super.getLocalBounds.call(this, rect); + } + + /** * calculates the bounds of the Text as a rectangle. The bounds calculation takes the worldTransform into account. */ _calculateBounds() @@ -525,6 +554,29 @@ const width = this.canvas.width / this.resolution; const height = this.canvas.height / this.resolution; + // make a copy of the style settings, so we can manipulate them later + const fill = style.fill.slice(); + const fillGradientStops = style.fillGradientStops.slice(); + + // wanting to evenly distribute the fills. So an array of 4 colours should give fills of 0.25, 0.5 and 0.75 + if (!fillGradientStops.length) + { + const lengthPlus1 = fill.length + 1; + + for (let i = 1; i < lengthPlus1; ++i) + { + fillGradientStops.push(i / lengthPlus1); + } + } + + // stop the bleeding of the last gradient on the line above to the top gradient of the this line + // by hard defining the first gradient colour at point 0, and last gradient colour at point 1 + fill.unshift(style.fill[0]); + fillGradientStops.unshift(0); + + fill.push(style.fill[style.fill.length - 1]); + fillGradientStops.push(1); + if (style.fillGradientType === TEXT_GRADIENT.LINEAR_VERTICAL) { // start the gradient at the top center of the canvas, and end at the bottom middle of the canvas @@ -532,15 +584,22 @@ // we need to repeat the gradient so that each individual line of text has the same vertical gradient effect // ['#FF0000', '#00FF00', '#0000FF'] over 2 lines would create stops at 0.125, 0.25, 0.375, 0.625, 0.75, 0.875 - totalIterations = (style.fill.length + 1) * lines.length; + totalIterations = (fill.length + 1) * lines.length; currentIteration = 0; for (let i = 0; i < lines.length; i++) { currentIteration += 1; - for (let j = 0; j < style.fill.length; j++) + for (let j = 0; j < fill.length; j++) { - stop = (currentIteration / totalIterations); - gradient.addColorStop(stop, style.fill[j]); + if (fillGradientStops[j]) + { + stop = (fillGradientStops[j] / lines.length) + (i / lines.length); + } + else + { + stop = currentIteration / totalIterations; + } + gradient.addColorStop(stop, fill[j]); currentIteration++; } } @@ -552,13 +611,20 @@ // can just evenly space out the gradients in this case, as multiple lines makes no difference // to an even left to right gradient - totalIterations = style.fill.length + 1; + totalIterations = fill.length + 1; currentIteration = 1; - for (let i = 0; i < style.fill.length; i++) + for (let i = 0; i < fill.length; i++) { - stop = currentIteration / totalIterations; - gradient.addColorStop(stop, style.fill[i]); + if (fillGradientStops[i]) + { + stop = fillGradientStops[i]; + } + else + { + stop = currentIteration / totalIterations; + } + gradient.addColorStop(stop, fill[i]); currentIteration++; } } @@ -569,7 +635,7 @@ /** * Destroys this text object. * Note* Unlike a Sprite, a Text object will automatically destroy its baseTexture and texture as - * the majorety of the time the texture will not be shared with any other Sprites. + * the majority of the time the texture will not be shared with any other Sprites. * * @param {object|boolean} [options] - Options parameter. A boolean will act as if all options * have been set to that value @@ -680,7 +746,7 @@ set text(text) // eslint-disable-line require-jsdoc { - text = String(text || ' '); + text = String(text === '' || text === null || text === undefined ? ' ' : text); if (this._text === text) { diff --git a/src/core/text/TextStyle.js b/src/core/text/TextStyle.js index 3b25528..890950b 100644 --- a/src/core/text/TextStyle.js +++ b/src/core/text/TextStyle.js @@ -8,12 +8,14 @@ align: 'left', breakWords: false, dropShadow: false, + dropShadowAlpha: 1, dropShadowAngle: Math.PI / 6, dropShadowBlur: 0, dropShadowColor: '#000000', dropShadowDistance: 5, fill: 'black', fillGradientType: TEXT_GRADIENT.LINEAR_VERTICAL, + fillGradientStops: [], fontFamily: 'Arial', fontSize: 26, fontStyle: 'normal', @@ -27,6 +29,7 @@ stroke: 'black', strokeThickness: 0, textBaseline: 'alphabetic', + trim: false, wordWrap: false, wordWrapWidth: 100, }; @@ -47,6 +50,7 @@ * @param {boolean} [style.breakWords=false] - Indicates if lines can be wrapped within words, it * needs wordWrap to be set to true * @param {boolean} [style.dropShadow=false] - Set a drop shadow for the text + * @param {number} [style.dropShadowAlpha=1] - Set alpha for the drop shadow * @param {number} [style.dropShadowAngle=Math.PI/6] - Set a angle of the drop shadow * @param {number} [style.dropShadowBlur=0] - Set a shadow blur radius * @param {string} [style.dropShadowColor='#000000'] - A fill style to be used on the dropshadow e.g 'red', '#00FF00' @@ -55,8 +59,10 @@ * fillstyle that will be used on the text e.g 'red', '#00FF00'. Can be an array to create a gradient * eg ['#000000','#FFFFFF'] * {@link https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/fillStyle|MDN} - * @param {number} [style.fillGradientType=PIXI.TEXT_GRADIENT.LINEAR_VERTICAL] - If fills styles are - * supplied, this can change the type/direction of the gradient. See {@link PIXI.TEXT_GRADIENT} for possible values + * @param {number} [style.fillGradientType=PIXI.TEXT_GRADIENT.LINEAR_VERTICAL] - If fill is an array of colours + * to create a gradient, this can change the type/direction of the gradient. See {@link PIXI.TEXT_GRADIENT} + * @param {number[]} [style.fillGradientStops] - If fill is an array of colours to create a gradient, this array can set + * the stop points (numbers between 0 and 1) for the color, overriding the default behaviour of evenly spacing them. * @param {string|string[]} [style.fontFamily='Arial'] - The font family * @param {number|string} [style.fontSize=26] - The font size (as a number it converts to px, but as a string, * equivalents are '26px','20pt','160%' or '1.6em') @@ -76,6 +82,7 @@ * e.g 'blue', '#FCFF00' * @param {number} [style.strokeThickness=0] - A number that represents the thickness of the stroke. * Default is 0 (no stroke) + * @param {boolean} [style.trim=false] - Trim transparent borders * @param {string} [style.textBaseline='alphabetic'] - The baseline of the text that is rendered. * @param {boolean} [style.wordWrap=false] - Indicates if word wrap should be used * @param {number} [style.wordWrapWidth=100] - The width at which text will wrap, it needs wordWrap to be set to true @@ -152,6 +159,19 @@ } } + get dropShadowAlpha() + { + return this._dropShadowAlpha; + } + set dropShadowAlpha(dropShadowAlpha) + { + if (this._dropShadowAlpha !== dropShadowAlpha) + { + this._dropShadowAlpha = dropShadowAlpha; + this.styleID++; + } + } + get dropShadowAngle() { return this._dropShadowAngle; @@ -232,6 +252,19 @@ } } + get fillGradientStops() + { + return this._fillGradientStops; + } + set fillGradientStops(fillGradientStops) + { + if (!areArraysEqual(this._fillGradientStops,fillGradientStops)) + { + this._fillGradientStops = fillGradientStops; + this.styleID++; + } + } + get fontFamily() { return this._fontFamily; @@ -402,6 +435,19 @@ } } + get trim() + { + return this._trim; + } + set trim(trim) + { + if (this._trim !== trim) + { + this._trim = trim; + this.styleID++; + } + } + get wordWrap() { return this._wordWrap; @@ -475,3 +521,34 @@ return color; } } + +/** + * Utility function to convert hexadecimal colors to strings, and simply return the color if it's a string. + * This version can also convert array of colors + * + * @param {Array} array1 First array to compare + * @param {Array} array2 Second array to compare + * @return {boolean} Do the arrays contain the same values in the same order + */ +function areArraysEqual(array1, array2) +{ + if (!Array.isArray(array1) || !Array.isArray(array2)) + { + return false; + } + + if (array1.length !== array2.length) + { + return false; + } + + for (let i = 0; i < array1.length; ++i) + { + if (array1[i] !== array2[i]) + { + return false; + } + } + + return true; +} diff --git a/src/core/textures/BaseRenderTexture.js b/src/core/textures/BaseRenderTexture.js index 5d50146..aa727f0 100644 --- a/src/core/textures/BaseRenderTexture.js +++ b/src/core/textures/BaseRenderTexture.js @@ -1,8 +1,6 @@ import BaseTexture from './BaseTexture'; import settings from '../settings'; -const { RESOLUTION, SCALE_MODE } = settings; - /** * A BaseRenderTexture is a special texture that allows any Pixi display object to be rendered to it. * @@ -54,7 +52,7 @@ { super(null, scaleMode); - this.resolution = resolution || RESOLUTION; + this.resolution = resolution || settings.RESOLUTION; this.width = width; this.height = height; @@ -62,7 +60,7 @@ this.realWidth = this.width * this.resolution; this.realHeight = this.height * this.resolution; - this.scaleMode = scaleMode || SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; this.hasLoaded = true; /** diff --git a/src/core/textures/BaseTexture.js b/src/core/textures/BaseTexture.js index 676dca3..aeccf39 100644 --- a/src/core/textures/BaseTexture.js +++ b/src/core/textures/BaseTexture.js @@ -77,7 +77,7 @@ * @default PIXI.settings.SCALE_MODE * @see PIXI.SCALE_MODES */ - this.scaleMode = scaleMode || settings.SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; /** * Set to true once the base texture has successfully loaded. @@ -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/RenderTexture.js b/src/core/textures/RenderTexture.js index 4c5034f..b3c6297 100644 --- a/src/core/textures/RenderTexture.js +++ b/src/core/textures/RenderTexture.js @@ -54,8 +54,8 @@ /* eslint-disable prefer-rest-params, no-console */ const width = arguments[1]; const height = arguments[2]; - const scaleMode = arguments[3] || 0; - const resolution = arguments[4] || 1; + const scaleMode = arguments[3]; + const resolution = arguments[4]; // we have an old render texture.. console.warn(`Please use RenderTexture.create(${width}, ${height}) instead of the ctor directly.`); 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 ca31456..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 @@ -459,7 +497,9 @@ if (frame.x + frame.width > this.baseTexture.width || frame.y + frame.height > this.baseTexture.height) { - throw new Error(`Texture Error: frame does not fit inside the base Texture dimensions ${this}`); + throw new Error('Texture Error: frame does not fit inside the base Texture dimensions: ' + + `X: ${frame.x} + ${frame.width} > ${this.baseTexture.width} ` + + `Y: ${frame.y} + ${frame.height} > ${this.baseTexture.height}`); } // this.valid = frame && frame.width && frame.height && this.baseTexture.source && this.baseTexture.hasLoaded; @@ -520,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. @@ -528,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 2a7497b..99e585a 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)); } /** @@ -360,3 +362,43 @@ * @private */ export const BaseTextureCache = {}; + +/** + * 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/core/utils/trimCanvas.js b/src/core/utils/trimCanvas.js new file mode 100644 index 0000000..5af6de0 --- /dev/null +++ b/src/core/utils/trimCanvas.js @@ -0,0 +1,83 @@ +/** + * Trim transparent borders from a canvas + * + * @memberof PIXI + * @function trimCanvas + * @private + * @param {HTMLCanvasElement} canvas - the canvas to trim + * @returns {object} Trim data + */ +export default function trimCanvas(canvas) +{ + // https://gist.github.com/remy/784508 + + let width = canvas.width; + let height = canvas.height; + + const context = canvas.getContext('2d'); + const imageData = context.getImageData(0, 0, width, height); + const pixels = imageData.data; + const len = pixels.length; + + const bound = { + top: null, + left: null, + right: null, + bottom: null, + }; + let i; + let x; + let y; + + for (i = 0; i < len; i += 4) + { + if (pixels[i + 3] !== 0) + { + x = (i / 4) % width; + y = ~~((i / 4) / width); + + if (bound.top === null) + { + bound.top = y; + } + + if (bound.left === null) + { + bound.left = x; + } + else if (x < bound.left) + { + bound.left = x; + } + + if (bound.right === null) + { + bound.right = x + 1; + } + else if (bound.right < x) + { + bound.right = x + 1; + } + + if (bound.bottom === null) + { + bound.bottom = y; + } + else if (bound.bottom < y) + { + bound.bottom = y; + } + } + } + + width = bound.right - bound.left; + height = bound.bottom - bound.top + 1; + + const data = context.getImageData(bound.left, bound.top, width, height); + + return { + height, + width, + data, + }; +} diff --git a/src/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/extras/TilingSprite.js b/src/extras/TilingSprite.js index f90d248..fe82451 100644 --- a/src/extras/TilingSprite.js +++ b/src/extras/TilingSprite.js @@ -220,20 +220,33 @@ transform.tx * resolution, transform.ty * resolution); - // TODO - this should be rolled into the setTransform above.. - context.scale(this.tileScale.x / baseTextureResolution, this.tileScale.y / baseTextureResolution); - - context.translate(modX + (this.anchor.x * -this._width), - modY + (this.anchor.y * -this._height)); - renderer.setBlendMode(this.blendMode); // fill the pattern! context.fillStyle = this._canvasPattern; - context.fillRect(-modX, - -modY, - this._width / this.tileScale.x * baseTextureResolution, - this._height / this.tileScale.y * baseTextureResolution); + + // TODO - this should be rolled into the setTransform above.. + context.scale(this.tileScale.x / baseTextureResolution, this.tileScale.y / baseTextureResolution); + + const anchorX = this.anchor.x * -this._width; + const anchorY = this.anchor.y * -this._height; + + if (this.uvRespectAnchor) + { + context.translate(modX, modY); + + context.fillRect(-modX + anchorX, -modY + anchorY, + this._width / this.tileScale.x * baseTextureResolution, + this._height / this.tileScale.y * baseTextureResolution); + } + else + { + context.translate(modX + anchorX, modY + anchorY); + + context.fillRect(-modX, -modY, + this._width / this.tileScale.x * baseTextureResolution, + this._height / this.tileScale.y * baseTextureResolution); + } } /** diff --git a/src/extras/cacheAsBitmap.js b/src/extras/cacheAsBitmap.js index 18043a7..5dd5640 100644 --- a/src/extras/cacheAsBitmap.js +++ b/src/extras/cacheAsBitmap.js @@ -41,6 +41,9 @@ * provide a performance benefit for complex static displayObjects. * To remove simply set this property to 'false' * + * IMPORTANT GOTCHA - make sure that all your textures are preloaded BEFORE setting this property to true + * as it will take a snapshot of what is currently there. If the textures have not loaded then they will not appear. + * * @member {boolean} * @memberof PIXI.DisplayObject# */ diff --git a/src/filters/blur/BlurFilter.js b/src/filters/blur/BlurFilter.js index 340fc58..1575db9 100644 --- a/src/filters/blur/BlurFilter.js +++ b/src/filters/blur/BlurFilter.js @@ -24,10 +24,9 @@ this.blurXFilter = new BlurXFilter(strength, quality, resolution, kernelSize); this.blurYFilter = new BlurYFilter(strength, quality, resolution, kernelSize); - this.resolution = 1; this.padding = 0; - this.resolution = resolution || 1; + this.resolution = resolution || core.settings.RESOLUTION; this.quality = quality || 4; this.blur = strength || 8; } diff --git a/src/filters/blur/BlurXFilter.js b/src/filters/blur/BlurXFilter.js index def99c3..0f48889 100644 --- a/src/filters/blur/BlurXFilter.js +++ b/src/filters/blur/BlurXFilter.js @@ -31,7 +31,7 @@ fragSrc ); - this.resolution = resolution || 1; + this.resolution = resolution || core.settings.RESOLUTION; this._quality = 0; diff --git a/src/filters/blur/BlurYFilter.js b/src/filters/blur/BlurYFilter.js index 638e0f0..6d489c7 100644 --- a/src/filters/blur/BlurYFilter.js +++ b/src/filters/blur/BlurYFilter.js @@ -31,7 +31,7 @@ fragSrc ); - this.resolution = resolution || 1; + this.resolution = resolution || core.settings.RESOLUTION; this._quality = 0; diff --git a/src/filters/colormatrix/ColorMatrixFilter.js b/src/filters/colormatrix/ColorMatrixFilter.js index 8509505..679f8ec 100644 --- a/src/filters/colormatrix/ColorMatrixFilter.js +++ b/src/filters/colormatrix/ColorMatrixFilter.js @@ -75,28 +75,28 @@ out[1] = (a[0] * b[1]) + (a[1] * b[6]) + (a[2] * b[11]) + (a[3] * b[16]); out[2] = (a[0] * b[2]) + (a[1] * b[7]) + (a[2] * b[12]) + (a[3] * b[17]); out[3] = (a[0] * b[3]) + (a[1] * b[8]) + (a[2] * b[13]) + (a[3] * b[18]); - out[4] = (a[0] * b[4]) + (a[1] * b[9]) + (a[2] * b[14]) + (a[3] * b[19]); + out[4] = (a[0] * b[4]) + (a[1] * b[9]) + (a[2] * b[14]) + (a[3] * b[19]) + a[4]; // Green Channel out[5] = (a[5] * b[0]) + (a[6] * b[5]) + (a[7] * b[10]) + (a[8] * b[15]); out[6] = (a[5] * b[1]) + (a[6] * b[6]) + (a[7] * b[11]) + (a[8] * b[16]); out[7] = (a[5] * b[2]) + (a[6] * b[7]) + (a[7] * b[12]) + (a[8] * b[17]); out[8] = (a[5] * b[3]) + (a[6] * b[8]) + (a[7] * b[13]) + (a[8] * b[18]); - out[9] = (a[5] * b[4]) + (a[6] * b[9]) + (a[7] * b[14]) + (a[8] * b[19]); + out[9] = (a[5] * b[4]) + (a[6] * b[9]) + (a[7] * b[14]) + (a[8] * b[19]) + a[9]; // Blue Channel out[10] = (a[10] * b[0]) + (a[11] * b[5]) + (a[12] * b[10]) + (a[13] * b[15]); out[11] = (a[10] * b[1]) + (a[11] * b[6]) + (a[12] * b[11]) + (a[13] * b[16]); out[12] = (a[10] * b[2]) + (a[11] * b[7]) + (a[12] * b[12]) + (a[13] * b[17]); out[13] = (a[10] * b[3]) + (a[11] * b[8]) + (a[12] * b[13]) + (a[13] * b[18]); - out[14] = (a[10] * b[4]) + (a[11] * b[9]) + (a[12] * b[14]) + (a[13] * b[19]); + out[14] = (a[10] * b[4]) + (a[11] * b[9]) + (a[12] * b[14]) + (a[13] * b[19]) + a[14]; // Alpha Channel out[15] = (a[15] * b[0]) + (a[16] * b[5]) + (a[17] * b[10]) + (a[18] * b[15]); out[16] = (a[15] * b[1]) + (a[16] * b[6]) + (a[17] * b[11]) + (a[18] * b[16]); out[17] = (a[15] * b[2]) + (a[16] * b[7]) + (a[17] * b[12]) + (a[18] * b[17]); out[18] = (a[15] * b[3]) + (a[16] * b[8]) + (a[17] * b[13]) + (a[18] * b[18]); - out[19] = (a[15] * b[4]) + (a[16] * b[9]) + (a[17] * b[14]) + (a[18] * b[19]); + out[19] = (a[15] * b[4]) + (a[16] * b[9]) + (a[17] * b[14]) + (a[18] * b[19]) + a[19]; return out; } diff --git a/src/filters/colormatrix/colorMatrix.frag b/src/filters/colormatrix/colorMatrix.frag index 3aaf2b3..c73c0e9 100644 --- a/src/filters/colormatrix/colorMatrix.frag +++ b/src/filters/colormatrix/colorMatrix.frag @@ -4,32 +4,38 @@ void main(void) { - vec4 c = texture2D(uSampler, vTextureCoord); + // Un-premultiply alpha before applying the color matrix. See issue #3539. + if (c.a > 0.0) { + c.rgb /= c.a; + } + vec4 result; + result.r = (m[0] * c.r); + result.r += (m[1] * c.g); + result.r += (m[2] * c.b); + result.r += (m[3] * c.a); + result.r += m[4]; - gl_FragColor.r = (m[0] * c.r); - gl_FragColor.r += (m[1] * c.g); - gl_FragColor.r += (m[2] * c.b); - gl_FragColor.r += (m[3] * c.a); - gl_FragColor.r += m[4] * c.a; + result.g = (m[5] * c.r); + result.g += (m[6] * c.g); + result.g += (m[7] * c.b); + result.g += (m[8] * c.a); + result.g += m[9]; - gl_FragColor.g = (m[5] * c.r); - gl_FragColor.g += (m[6] * c.g); - gl_FragColor.g += (m[7] * c.b); - gl_FragColor.g += (m[8] * c.a); - gl_FragColor.g += m[9] * c.a; + result.b = (m[10] * c.r); + result.b += (m[11] * c.g); + result.b += (m[12] * c.b); + result.b += (m[13] * c.a); + result.b += m[14]; - gl_FragColor.b = (m[10] * c.r); - gl_FragColor.b += (m[11] * c.g); - gl_FragColor.b += (m[12] * c.b); - gl_FragColor.b += (m[13] * c.a); - gl_FragColor.b += m[14] * c.a; + result.a = (m[15] * c.r); + result.a += (m[16] * c.g); + result.a += (m[17] * c.b); + result.a += (m[18] * c.a); + result.a += m[19]; - gl_FragColor.a = (m[15] * c.r); - gl_FragColor.a += (m[16] * c.g); - gl_FragColor.a += (m[17] * c.b); - gl_FragColor.a += (m[18] * c.a); - gl_FragColor.a += m[19] * c.a; + // Premultiply alpha again. + result.rgb *= result.a; -// gl_FragColor = vec4(m[0]); + gl_FragColor = result; } diff --git a/src/filters/displacement/DisplacementFilter.js b/src/filters/displacement/DisplacementFilter.js index 112de0b..d5355c3 100644 --- a/src/filters/displacement/DisplacementFilter.js +++ b/src/filters/displacement/DisplacementFilter.js @@ -36,7 +36,7 @@ this.maskMatrix = maskMatrix; this.uniforms.mapSampler = sprite.texture; - this.uniforms.filterMatrix = maskMatrix.toArray(true); + this.uniforms.filterMatrix = maskMatrix; this.uniforms.scale = { x: 1, y: 1 }; if (scale === null || scale === undefined) diff --git a/src/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/InteractionData.js b/src/interaction/InteractionData.js index 02d53a3..818da35 100644 --- a/src/interaction/InteractionData.js +++ b/src/interaction/InteractionData.js @@ -21,9 +21,9 @@ this.global = new core.Point(); /** - * The target Sprite that was interacted with + * The target DisplayObject that was interacted with * - * @member {PIXI.Sprite} + * @member {PIXI.DisplayObject} */ this.target = null; @@ -33,6 +33,13 @@ * @member {Event} */ this.originalEvent = null; + + /** + * Unique identifier for this interaction + * + * @member {number} + */ + this.identifier = null; } /** diff --git a/src/interaction/InteractionEvent.js b/src/interaction/InteractionEvent.js index 75b2966..dc554f9 100644 --- a/src/interaction/InteractionEvent.js +++ b/src/interaction/InteractionEvent.js @@ -58,7 +58,7 @@ } /** - * Prevents event from reaching any objects other than the current object. + * Resets the event. * * @private */ diff --git a/src/interaction/InteractionManager.js b/src/interaction/InteractionManager.js index 7d51134..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/src/prepare/BasePrepare.js b/src/prepare/BasePrepare.js index a2005f5..09eb5e5 100644 --- a/src/prepare/BasePrepare.js +++ b/src/prepare/BasePrepare.js @@ -20,7 +20,7 @@ * * @abstract * @class - * @memberof PIXI + * @memberof PIXI.prepare */ export default class BasePrepare { @@ -107,9 +107,9 @@ /** * Upload all the textures and graphics to the GPU. * - * @param {Function|PIXI.DisplayObject|PIXI.Container} item - Either - * the container or display object to search for items to upload or - * the callback function, if items have been added using `prepare.add`. + * @param {Function|PIXI.DisplayObject|PIXI.Container|PIXI.BaseTexture|PIXI.Texture|PIXI.Graphics|PIXI.Text} item - + * Either the container or display object to search for items to upload, the items to upload themselves, + * or the callback function, if items have been added using `prepare.add`. * @param {Function} [done] - Optional callback when all queued uploads have completed */ upload(item, done) @@ -236,7 +236,8 @@ /** * Manually add an item to the uploading queue. * - * @param {PIXI.DisplayObject|PIXI.Container|*} item - Object to add to the queue + * @param {PIXI.DisplayObject|PIXI.Container|PIXI.BaseTexture|PIXI.Texture|PIXI.Graphics|PIXI.Text|*} item - Object to + * add to the queue * @return {PIXI.CanvasPrepare} Instance of plugin for chaining. */ add(item) diff --git a/src/prepare/canvas/CanvasPrepare.js b/src/prepare/canvas/CanvasPrepare.js index 4b99d22..0224bb5 100644 --- a/src/prepare/canvas/CanvasPrepare.js +++ b/src/prepare/canvas/CanvasPrepare.js @@ -12,7 +12,8 @@ * An instance of this class is automatically created by default, and can be found at renderer.plugins.prepare * * @class - * @memberof PIXI + * @extends PIXI.prepare.BasePrepare + * @memberof PIXI.prepare */ export default class CanvasPrepare extends BasePrepare { diff --git a/src/prepare/webgl/WebGLPrepare.js b/src/prepare/webgl/WebGLPrepare.js index 1ef7467..eb7023f 100644 --- a/src/prepare/webgl/WebGLPrepare.js +++ b/src/prepare/webgl/WebGLPrepare.js @@ -7,7 +7,8 @@ * An instance of this class is automatically created by default, and can be found at renderer.plugins.prepare * * @class - * @memberof PIXI + * @extends PIXI.prepare.BasePrepare + * @memberof PIXI.prepare */ export default class WebGLPrepare extends BasePrepare { diff --git a/test/core/Circle.js b/test/core/Circle.js index 183f98f..f621992 100644 --- a/test/core/Circle.js +++ b/test/core/Circle.js @@ -53,6 +53,10 @@ expect(circ1.contains(10, 16)).to.be.false; expect(circ1.contains(11, 15)).to.be.false; expect(circ1.contains(0, 0)).to.be.false; + + const circ2 = new PIXI.Circle(10, 10, 0); + + expect(circ2.contains(10, 10)).to.be.false; }); it('should return framing rectangle', function () diff --git a/test/core/Container.js b/test/core/Container.js index 8a10b40..cefde54 100644 --- a/test/core/Container.js +++ b/test/core/Container.js @@ -1,5 +1,39 @@ 'use strict'; +function testAddChild(fn) +{ + return function () + { + fn(function (container, obj) + { + container.addChild(obj); + }); + fn(function (container, obj) + { + container.addChildAt(obj); + }); + }; +} + +function testRemoveChild(fn) +{ + return function () + { + 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 +104,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 +279,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/Ellipse.js b/test/core/Ellipse.js index f2e8234..3f47442 100644 --- a/test/core/Ellipse.js +++ b/test/core/Ellipse.js @@ -56,6 +56,10 @@ expect(ellipse1.contains(10, 16)).to.be.false; expect(ellipse1.contains(11, 15)).to.be.false; expect(ellipse1.contains(0, 0)).to.be.false; + + const ellipse2 = new PIXI.Ellipse(10, 10, 0, 0); + + expect(ellipse2.contains(10, 10)).to.be.false; }); it('should return framing rectangle', function () diff --git a/test/core/Graphics.js b/test/core/Graphics.js index 2a64946..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 () @@ -127,6 +130,16 @@ expect(graphics.containsPoint(point)).to.be.false; }); + + it('should return false when no fill', function () + { + const point = new PIXI.Point(1, 1); + const graphics = new PIXI.Graphics(); + + graphics.drawRect(0, 0, 10, 10); + + expect(graphics.containsPoint(point)).to.be.false; + }); }); describe('arc', function () @@ -208,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/Matrix.js b/test/core/Matrix.js index 9d2694b..76bd57c 100644 --- a/test/core/Matrix.js +++ b/test/core/Matrix.js @@ -107,10 +107,60 @@ expect(m1.ty).to.equal(m2.ty); }); + it('should prepend matrix', function () + { + const m1 = new PIXI.Matrix(); + const m2 = new PIXI.Matrix(); + + m2.set(2, 3, 4, 5, 100, 200); + m1.prepend(m2); + + expect(m1.a).to.equal(m2.a); + expect(m1.b).to.equal(m2.b); + expect(m1.c).to.equal(m2.c); + expect(m1.d).to.equal(m2.d); + expect(m1.tx).to.equal(m2.tx); + expect(m1.ty).to.equal(m2.ty); + + const m3 = new PIXI.Matrix(); + const m4 = new PIXI.Matrix(); + + m3.prepend(m4); + + expect(m3.a).to.equal(m4.a); + expect(m3.b).to.equal(m4.b); + expect(m3.c).to.equal(m4.c); + expect(m3.d).to.equal(m4.d); + expect(m3.tx).to.equal(m4.tx); + expect(m3.ty).to.equal(m4.ty); + }); + it('should get IDENTITY and TEMP_MATRIX', function () { expect(PIXI.Matrix.IDENTITY instanceof PIXI.Matrix).to.be.true; expect(PIXI.Matrix.TEMP_MATRIX instanceof PIXI.Matrix).to.be.true; }); -}); + it('should reset matrix to default when identity() is called', function () + { + const matrix = new PIXI.Matrix(); + + matrix.set(2, 3, 4, 5, 100, 200); + + expect(matrix.a).to.equal(2); + expect(matrix.b).to.equal(3); + expect(matrix.c).to.equal(4); + expect(matrix.d).to.equal(5); + expect(matrix.tx).to.equal(100); + expect(matrix.ty).to.equal(200); + + matrix.identity(); + + expect(matrix.a).to.equal(1); + expect(matrix.b).to.equal(0); + expect(matrix.c).to.equal(0); + expect(matrix.d).to.equal(1); + expect(matrix.tx).to.equal(0); + expect(matrix.ty).to.equal(0); + }); +}); 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/Text.js b/test/core/Text.js index b8ce561..5799ad4 100644 --- a/test/core/Text.js +++ b/test/core/Text.js @@ -112,4 +112,42 @@ expect(text.width).to.equal(300); }); }); + + describe('text', function () + { + it('should convert numbers into strings', function () + { + const text = new PIXI.Text(2); + + expect(text.text).to.equal('2'); + }); + + it('should not change 0 to \'\'', function () + { + const text = new PIXI.Text(0); + + expect(text.text).to.equal('0'); + }); + + it('should prevent setting null', function () + { + const text = new PIXI.Text(null); + + expect(text.text).to.equal(' '); + }); + + it('should prevent setting undefined', function () + { + const text = new PIXI.Text(); + + expect(text.text).to.equal(' '); + }); + + it('should prevent setting \'\'', function () + { + const text = new PIXI.Text(''); + + expect(text.text).to.equal(' '); + }); + }); }); diff --git a/test/core/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/TilingSprite.js b/test/core/TilingSprite.js index 2e449a7..7694792 100644 --- a/test/core/TilingSprite.js +++ b/test/core/TilingSprite.js @@ -24,4 +24,25 @@ expect(bounds.height).to.equal(600); }); }); + + it('checks if tilingSprite contains a point', function () + { + const texture = new PIXI.Texture(new PIXI.BaseTexture()); + const tilingSprite = new PIXI.extras.TilingSprite(texture, 200, 300); + + expect(tilingSprite.containsPoint(new PIXI.Point(1, 1))).to.equal(true); + expect(tilingSprite.containsPoint(new PIXI.Point(300, 400))).to.equal(false); + }); + + it('gets and sets height and width correctly', function () + { + const texture = new PIXI.Texture(new PIXI.BaseTexture()); + const tilingSprite = new PIXI.extras.TilingSprite(texture, 200, 300); + + tilingSprite.width = 400; + tilingSprite.height = 600; + + expect(tilingSprite.width).to.equal(400); + expect(tilingSprite.height).to.equal(600); + }); }); diff --git a/test/core/WebGLRenderer.js b/test/core/WebGLRenderer.js new file mode 100644 index 0000000..3d6cf98 --- /dev/null +++ b/test/core/WebGLRenderer.js @@ -0,0 +1,21 @@ +'use strict'; + +const withGL = require('../withGL'); + +describe('PIXI.WebGLRenderer', function () +{ + it('setting option legacy should disable VAOs and SPRITE_MAX_TEXTURES', withGL(function () + { + const renderer = new PIXI.WebGLRenderer(1, 1, { legacy: true }); + + 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/getLocalBounds.js b/test/core/getLocalBounds.js index d160a56..309da0e 100644 --- a/test/core/getLocalBounds.js +++ b/test/core/getLocalBounds.js @@ -196,4 +196,13 @@ expect(bounds.width).to.equal(100); expect(bounds.height).to.equal(100); }); + + it('should register correct local-bounds with a Text', function () + { + const text = new PIXI.Text('hello'); + const bounds = text.getLocalBounds(); + + expect(bounds.width).to.not.equal(0); + expect(bounds.height).to.not.equal(0); + }); }); diff --git a/test/core/index.js b/test/core/index.js index 2a056a2..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'); @@ -23,4 +24,7 @@ require('./Circle'); require('./Graphics'); 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 1271fef..71ade15 100644 --- a/package.json +++ b/package.json @@ -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/scripts/release.js b/scripts/release.js index c4c8e36..156620c 100644 --- a/scripts/release.js +++ b/scripts/release.js @@ -12,7 +12,8 @@ 'dist/**/*', 'lib/**/*', 'src/**/*', - 'scripts/**/*', + 'scripts/*', + 'scripts/renders/*', 'test/**/*', '*.json', '*.md', diff --git a/scripts/renders/client.js b/scripts/renders/client.js index 16c9565..6faee34 100755 --- a/scripts/renders/client.js +++ b/scripts/renders/client.js @@ -1,6 +1,6 @@ 'use strict'; -require('../../bin/pixi'); +require('../../dist/pixi'); PIXI.utils.skipHello(); const fs = require('fs'); 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 bc730db..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. @@ -33,8 +33,11 @@ * need to call toDataUrl on the webgl context * @param {number} [options.resolution=1] - The resolution / device pixel ratio of the renderer, retina would be 2 * @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 @@ -43,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. */ @@ -73,7 +99,7 @@ */ stop() { - this.ticker.stop(); + this._ticker.stop(); } /** @@ -81,7 +107,7 @@ */ start() { - this.ticker.start(); + this._ticker.start(); } /** @@ -95,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 index 81e076b..78b17e6 100644 --- a/src/core/Shader.js +++ b/src/core/Shader.js @@ -1,9 +1,7 @@ import { GLShader } from 'pixi-gl-core'; import settings from './settings'; -const { PRECISION } = settings; - -function checkPrecision(src) +function checkPrecision(src, def) { if (src instanceof Array) { @@ -11,14 +9,14 @@ { const copy = src.slice(0); - copy.unshift(`precision ${PRECISION} float;`); + copy.unshift(`precision ${def} float;`); return copy; } } else if (src.substring(0, 9) !== 'precision') { - return `precision ${PRECISION} float;\n${src}`; + return `precision ${def} float;\n${src}`; } return src; @@ -42,6 +40,7 @@ */ constructor(gl, vertexSrc, fragmentSrc) { - super(gl, checkPrecision(vertexSrc), checkPrecision(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 3425b41..f9235e4 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -181,7 +181,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 b3792ef..17fde8d 100644 --- a/src/core/display/Container.js +++ b/src/core/display/Container.js @@ -73,13 +73,14 @@ } child.parent = this; - - // ensure a transform will be recalculated.. - this.transform._parentID = -1; - this._boundsID++; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.push(child); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(this.children.length - 1); child.emit('added', this); @@ -108,9 +109,14 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.splice(index, 0, child); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(index); child.emit('added', this); @@ -174,6 +180,7 @@ removeItems(this.children, currentIndex, 1); // remove from old position this.children.splice(index, 0, child); // add at new position + this.onChildrenChange(index); } @@ -220,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! @@ -244,9 +252,14 @@ { const child = this.getChildAt(index); + // ensure child transform will be recalculated.. child.parent = null; + child.transform._parentID = -1; removeItems(this.children, index, 1); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(index); child.emit('removed', this); @@ -275,8 +288,14 @@ for (let i = 0; i < removed.length; ++i) { removed[i].parent = null; + if (removed[i].transform) + { + removed[i].transform._parentID = -1; + } } + this._boundsID++; + this.onChildrenChange(beginIndex); for (let i = 0; i < removed.length; ++i) diff --git a/src/core/display/DisplayObject.js b/src/core/display/DisplayObject.js index 3affe36..4e6c77d 100644 --- a/src/core/display/DisplayObject.js +++ b/src/core/display/DisplayObject.js @@ -109,7 +109,7 @@ /** * The original, cached mask of the object * - * @member {PIXI.Rectangle} + * @member {PIXI.Graphics|PIXI.Sprite} * @private */ this._mask = null; diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index 95f2af4..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 ); @@ -1065,10 +1061,15 @@ canvasRenderer = new CanvasRenderer(); } - tempMatrix.tx = -bounds.x; - tempMatrix.ty = -bounds.y; + this.transform.updateLocalTransform(); + this.transform.localTransform.copy(tempMatrix); - canvasRenderer.render(this, canvasBuffer, false, tempMatrix); + tempMatrix.invert(); + + tempMatrix.tx -= bounds.x; + tempMatrix.ty -= bounds.y; + + canvasRenderer.render(this, canvasBuffer, true, tempMatrix); const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode); 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 e67680f..867dde2 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/buildCircle.js b/src/core/graphics/webgl/utils/buildCircle.js index 0400d13..abf5d02 100644 --- a/src/core/graphics/webgl/utils/buildCircle.js +++ b/src/core/graphics/webgl/utils/buildCircle.js @@ -33,6 +33,11 @@ height = circleData.height; } + if (width === 0 || height === 0) + { + return; + } + const totalSegs = Math.floor(30 * Math.sqrt(circleData.radius)) || Math.floor(15 * Math.sqrt(circleData.width + circleData.height)); diff --git a/src/core/graphics/webgl/utils/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 90155f9..f03fe15 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -30,6 +30,7 @@ export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; export { default as CanvasGraphicsRenderer } from './graphics/canvas/CanvasGraphicsRenderer'; +export { default as Spritesheet } from './textures/Spritesheet'; export { default as Texture } from './textures/Texture'; export { default as BaseTexture } from './textures/BaseTexture'; export { default as RenderTexture } from './textures/RenderTexture'; diff --git a/src/core/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 ccce680..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); } /** @@ -162,6 +162,9 @@ if (transform) { transform.copy(tempWt); + + // lets not forget to flag the parent transform as dirty... + this._tempDisplayObjectParent.transform._worldID = -1; } else { @@ -169,6 +172,7 @@ } displayObject.parent = this._tempDisplayObjectParent; + displayObject.updateTransform(); displayObject.parent = cacheParent; // displayObject.hitArea = //TODO add a temp hit area @@ -279,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/canvas/utils/CanvasRenderTarget.js b/src/core/renderers/canvas/utils/CanvasRenderTarget.js index 3de0aef..b409891 100644 --- a/src/core/renderers/canvas/utils/CanvasRenderTarget.js +++ b/src/core/renderers/canvas/utils/CanvasRenderTarget.js @@ -1,5 +1,4 @@ import settings from '../../../settings'; -const { RESOLUTION } = settings; /** * Creates a Canvas element of the given size. @@ -30,7 +29,7 @@ */ this.context = this.canvas.getContext('2d'); - this.resolution = resolution || RESOLUTION; + this.resolution = resolution || settings.RESOLUTION; this.resize(width, height); } 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 ce0e1c4..788ed6c 100644 --- a/src/core/renderers/webgl/WebGLRenderer.js +++ b/src/core/renderers/webgl/WebGLRenderer.js @@ -30,8 +30,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 @@ -49,10 +49,19 @@ * enable this if you need to call toDataUrl on the webgl context. * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when * rendering, stopping pixel interpolation. + * @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; + + if (this.legacy) + { + glCore.VertexArrayObject.FORCE_NATIVE = true; + } /** * The type of this renderer as a standardised const @@ -229,7 +238,7 @@ this.emit('context', gl); // setup the width/height properties and gl viewport - this.resize(this.width, this.height); + this.resize(this.screen.width, this.screen.height); } /** @@ -323,16 +332,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) { @@ -376,6 +385,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 @@ -440,9 +469,10 @@ * Changes the current shader to the one given in parameter * * @param {PIXI.Shader} shader - the new shader + * @param {boolean} [autoProject=true] - Whether automatically set the projection matrix * @return {PIXI.WebGLRenderer} Returns itself. */ - bindShader(shader) + bindShader(shader, autoProject) { // TODO cache if (this._activeShader !== shader) @@ -450,8 +480,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 bbab3b3..944f41d 100644 --- a/src/core/renderers/webgl/filters/Filter.js +++ b/src/core/renderers/webgl/filters/Filter.js @@ -1,6 +1,7 @@ import extractUniformsFromSrc from './extractUniformsFromSrc'; import { uid } from '../../../utils'; import { BLEND_MODES } from '../../../const'; +import settings from '../../../settings'; const SOURCE_KEY_MAP = {}; @@ -10,7 +11,7 @@ * @memberof PIXI * @extends PIXI.Shader */ -class Filter +export default class Filter { /** * @param {string} [vertexSrc] - The source of the vertex shader. @@ -35,8 +36,6 @@ this.blendMode = BLEND_MODES.NORMAL; - // pull out the vertex and shader uniforms if they are not specified.. - // currently this does not extract structs only default types this.uniformData = uniforms || extractUniformsFromSrc(this.vertexSrc, this.fragmentSrc, 'projectionMatrix|uSampler'); /** @@ -80,7 +79,7 @@ * * @member {number} */ - this.resolution = 1; + this.resolution = settings.RESOLUTION; /** * If enabled is true the filter is applied, if false it will not. @@ -97,8 +96,11 @@ * @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 { // --- // // this.uniforms.filterMatrix = filterManager.calculateSpriteMatrix(tempMatrix, window.panda ); @@ -170,5 +172,3 @@ ].join('\n'); } } - -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 dc7072a..6c7422d 100644 --- a/src/core/renderers/webgl/managers/FilterManager.js +++ b/src/core/renderers/webgl/managers/FilterManager.js @@ -154,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 @@ -176,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; @@ -184,7 +184,7 @@ flop = t; } - filters[i].apply(this, flip, lastState.renderTarget, true); + filters[i].apply(this, flip, lastState.renderTarget, false, currentState); this.freePotRenderTarget(flip); this.freePotRenderTarget(flop); @@ -291,9 +291,13 @@ let textureCount = 1; let currentState; - if (shader.uniforms.data.filterArea) + // 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; @@ -306,9 +310,9 @@ // use this to clamp displaced texture coords so they belong to filterArea // see displacementFilter fragment shader for an example - if (shader.uniforms.data.filterClamp) + if (shader.uniforms.filterClamp) { - currentState = this.filterData.stack[this.filterData.index]; + currentState = currentState || this.filterData.stack[this.filterData.index]; const filterClamp = shader.uniforms.filterClamp; @@ -485,7 +489,7 @@ */ destroy() { - this.shaderCache = []; + this.shaderCache = {}; this.emptyPool(); } diff --git a/src/core/renderers/webgl/utils/RenderTarget.js b/src/core/renderers/webgl/utils/RenderTarget.js index afe51ab..6d2467f 100644 --- a/src/core/renderers/webgl/utils/RenderTarget.js +++ b/src/core/renderers/webgl/utils/RenderTarget.js @@ -124,7 +124,7 @@ * @default PIXI.settings.SCALE_MODE * @see PIXI.SCALE_MODES */ - this.scaleMode = scaleMode || settings.SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; /** * Whether this object is the root element or not 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 36ea5bb..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; @@ -346,8 +346,8 @@ /** * Gets the local bounds of the sprite object. * - * @param {Rectangle} rect - The output rectangle. - * @return {Rectangle} The bounds. + * @param {PIXI.Rectangle} rect - The output rectangle. + * @return {PIXI.Rectangle} The bounds. */ getLocalBounds(rect) { @@ -440,7 +440,7 @@ * * @static * @param {number|string|PIXI.BaseTexture|HTMLCanvasElement|HTMLVideoElement} source Source to create texture from - * @return {PIXI.Texture} 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 e77b39a..90b08c0 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++) @@ -103,11 +102,18 @@ { const gl = this.renderer.gl; - // step 1: first check max textures the GPU can handle. - this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); + if (this.renderer.legacy) + { + this.MAX_TEXTURES = 1; + } + else + { + // step 1: first check max textures the GPU can handle. + this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); - // step 2: check the maximum number of if statements the shader can have too.. - this.MAX_TEXTURES = checkMaxIfStatmentsInShader(this.MAX_TEXTURES, gl); + // step 2: check the maximum number of if statements the shader can have too.. + this.MAX_TEXTURES = checkMaxIfStatmentsInShader(this.MAX_TEXTURES, gl); + } const shader = this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); @@ -130,8 +136,12 @@ .addIndex(this.indexBuffer) .addAttribute(this.vertexBuffers[i], shader.attributes.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) .addAttribute(this.vertexBuffers[i], shader.attributes.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) - .addAttribute(this.vertexBuffers[i], shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4) - .addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + .addAttribute(this.vertexBuffers[i], shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); + + if (shader.attributes.aTextureId) + { + this.vaos[i].addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + } /* eslint-enable max-len */ } @@ -378,8 +388,12 @@ .addIndex(this.indexBuffer) .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) - .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4) - .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); + + if (this.shader.attributes.aTextureId) + { + this.vaos[this.vertexCount].addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + } /* eslint-enable max-len */ } diff --git a/src/core/text/Text.js b/src/core/text/Text.js index 013625a..cec8c32 100644 --- a/src/core/text/Text.js +++ b/src/core/text/Text.js @@ -6,6 +6,7 @@ import { TEXT_GRADIENT } from '../const'; import settings from '../settings'; import TextStyle from './TextStyle'; +import trimCanvas from '../utils/trimCanvas'; const defaultDestroyOptions = { texture: true, @@ -57,7 +58,7 @@ /** * The canvas 2d context that everything is drawn with - * @member {HTMLCanvasElement} + * @member {CanvasRenderingContext2D} */ this.context = this.canvas.getContext('2d'); @@ -189,10 +190,12 @@ if (style.dropShadow) { + this.context.shadowBlur = style.dropShadowBlur; + this.context.globalAlpha = style.dropShadowAlpha; + if (style.dropShadowBlur > 0) { this.context.shadowColor = style.dropShadowColor; - this.context.shadowBlur = style.dropShadowBlur; } else { @@ -237,6 +240,10 @@ } } + // reset the shadow blur and alpha that was set by the drop shadow, for the regular text + this.context.shadowBlur = 0; + this.context.globalAlpha = 1; + // set canvas text styles this.context.fillStyle = this._generateFillStyle(style, lines); @@ -326,6 +333,15 @@ */ updateTexture() { + if (this._style.trim) + { + const trimmed = trimCanvas(this.canvas); + + this.canvas.width = trimmed.width; + this.canvas.height = trimmed.height; + this.context.putImageData(trimmed.data, 0, 0); + } + const texture = this._texture; const style = this._style; @@ -475,6 +491,19 @@ } /** + * Gets the local bounds of the text object. + * + * @param {Rectangle} rect - The output rectangle. + * @return {Rectangle} The bounds. + */ + getLocalBounds(rect) + { + this.updateText(true); + + return super.getLocalBounds.call(this, rect); + } + + /** * calculates the bounds of the Text as a rectangle. The bounds calculation takes the worldTransform into account. */ _calculateBounds() @@ -525,6 +554,29 @@ const width = this.canvas.width / this.resolution; const height = this.canvas.height / this.resolution; + // make a copy of the style settings, so we can manipulate them later + const fill = style.fill.slice(); + const fillGradientStops = style.fillGradientStops.slice(); + + // wanting to evenly distribute the fills. So an array of 4 colours should give fills of 0.25, 0.5 and 0.75 + if (!fillGradientStops.length) + { + const lengthPlus1 = fill.length + 1; + + for (let i = 1; i < lengthPlus1; ++i) + { + fillGradientStops.push(i / lengthPlus1); + } + } + + // stop the bleeding of the last gradient on the line above to the top gradient of the this line + // by hard defining the first gradient colour at point 0, and last gradient colour at point 1 + fill.unshift(style.fill[0]); + fillGradientStops.unshift(0); + + fill.push(style.fill[style.fill.length - 1]); + fillGradientStops.push(1); + if (style.fillGradientType === TEXT_GRADIENT.LINEAR_VERTICAL) { // start the gradient at the top center of the canvas, and end at the bottom middle of the canvas @@ -532,15 +584,22 @@ // we need to repeat the gradient so that each individual line of text has the same vertical gradient effect // ['#FF0000', '#00FF00', '#0000FF'] over 2 lines would create stops at 0.125, 0.25, 0.375, 0.625, 0.75, 0.875 - totalIterations = (style.fill.length + 1) * lines.length; + totalIterations = (fill.length + 1) * lines.length; currentIteration = 0; for (let i = 0; i < lines.length; i++) { currentIteration += 1; - for (let j = 0; j < style.fill.length; j++) + for (let j = 0; j < fill.length; j++) { - stop = (currentIteration / totalIterations); - gradient.addColorStop(stop, style.fill[j]); + if (fillGradientStops[j]) + { + stop = (fillGradientStops[j] / lines.length) + (i / lines.length); + } + else + { + stop = currentIteration / totalIterations; + } + gradient.addColorStop(stop, fill[j]); currentIteration++; } } @@ -552,13 +611,20 @@ // can just evenly space out the gradients in this case, as multiple lines makes no difference // to an even left to right gradient - totalIterations = style.fill.length + 1; + totalIterations = fill.length + 1; currentIteration = 1; - for (let i = 0; i < style.fill.length; i++) + for (let i = 0; i < fill.length; i++) { - stop = currentIteration / totalIterations; - gradient.addColorStop(stop, style.fill[i]); + if (fillGradientStops[i]) + { + stop = fillGradientStops[i]; + } + else + { + stop = currentIteration / totalIterations; + } + gradient.addColorStop(stop, fill[i]); currentIteration++; } } @@ -569,7 +635,7 @@ /** * Destroys this text object. * Note* Unlike a Sprite, a Text object will automatically destroy its baseTexture and texture as - * the majorety of the time the texture will not be shared with any other Sprites. + * the majority of the time the texture will not be shared with any other Sprites. * * @param {object|boolean} [options] - Options parameter. A boolean will act as if all options * have been set to that value @@ -680,7 +746,7 @@ set text(text) // eslint-disable-line require-jsdoc { - text = String(text || ' '); + text = String(text === '' || text === null || text === undefined ? ' ' : text); if (this._text === text) { diff --git a/src/core/text/TextStyle.js b/src/core/text/TextStyle.js index 3b25528..890950b 100644 --- a/src/core/text/TextStyle.js +++ b/src/core/text/TextStyle.js @@ -8,12 +8,14 @@ align: 'left', breakWords: false, dropShadow: false, + dropShadowAlpha: 1, dropShadowAngle: Math.PI / 6, dropShadowBlur: 0, dropShadowColor: '#000000', dropShadowDistance: 5, fill: 'black', fillGradientType: TEXT_GRADIENT.LINEAR_VERTICAL, + fillGradientStops: [], fontFamily: 'Arial', fontSize: 26, fontStyle: 'normal', @@ -27,6 +29,7 @@ stroke: 'black', strokeThickness: 0, textBaseline: 'alphabetic', + trim: false, wordWrap: false, wordWrapWidth: 100, }; @@ -47,6 +50,7 @@ * @param {boolean} [style.breakWords=false] - Indicates if lines can be wrapped within words, it * needs wordWrap to be set to true * @param {boolean} [style.dropShadow=false] - Set a drop shadow for the text + * @param {number} [style.dropShadowAlpha=1] - Set alpha for the drop shadow * @param {number} [style.dropShadowAngle=Math.PI/6] - Set a angle of the drop shadow * @param {number} [style.dropShadowBlur=0] - Set a shadow blur radius * @param {string} [style.dropShadowColor='#000000'] - A fill style to be used on the dropshadow e.g 'red', '#00FF00' @@ -55,8 +59,10 @@ * fillstyle that will be used on the text e.g 'red', '#00FF00'. Can be an array to create a gradient * eg ['#000000','#FFFFFF'] * {@link https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/fillStyle|MDN} - * @param {number} [style.fillGradientType=PIXI.TEXT_GRADIENT.LINEAR_VERTICAL] - If fills styles are - * supplied, this can change the type/direction of the gradient. See {@link PIXI.TEXT_GRADIENT} for possible values + * @param {number} [style.fillGradientType=PIXI.TEXT_GRADIENT.LINEAR_VERTICAL] - If fill is an array of colours + * to create a gradient, this can change the type/direction of the gradient. See {@link PIXI.TEXT_GRADIENT} + * @param {number[]} [style.fillGradientStops] - If fill is an array of colours to create a gradient, this array can set + * the stop points (numbers between 0 and 1) for the color, overriding the default behaviour of evenly spacing them. * @param {string|string[]} [style.fontFamily='Arial'] - The font family * @param {number|string} [style.fontSize=26] - The font size (as a number it converts to px, but as a string, * equivalents are '26px','20pt','160%' or '1.6em') @@ -76,6 +82,7 @@ * e.g 'blue', '#FCFF00' * @param {number} [style.strokeThickness=0] - A number that represents the thickness of the stroke. * Default is 0 (no stroke) + * @param {boolean} [style.trim=false] - Trim transparent borders * @param {string} [style.textBaseline='alphabetic'] - The baseline of the text that is rendered. * @param {boolean} [style.wordWrap=false] - Indicates if word wrap should be used * @param {number} [style.wordWrapWidth=100] - The width at which text will wrap, it needs wordWrap to be set to true @@ -152,6 +159,19 @@ } } + get dropShadowAlpha() + { + return this._dropShadowAlpha; + } + set dropShadowAlpha(dropShadowAlpha) + { + if (this._dropShadowAlpha !== dropShadowAlpha) + { + this._dropShadowAlpha = dropShadowAlpha; + this.styleID++; + } + } + get dropShadowAngle() { return this._dropShadowAngle; @@ -232,6 +252,19 @@ } } + get fillGradientStops() + { + return this._fillGradientStops; + } + set fillGradientStops(fillGradientStops) + { + if (!areArraysEqual(this._fillGradientStops,fillGradientStops)) + { + this._fillGradientStops = fillGradientStops; + this.styleID++; + } + } + get fontFamily() { return this._fontFamily; @@ -402,6 +435,19 @@ } } + get trim() + { + return this._trim; + } + set trim(trim) + { + if (this._trim !== trim) + { + this._trim = trim; + this.styleID++; + } + } + get wordWrap() { return this._wordWrap; @@ -475,3 +521,34 @@ return color; } } + +/** + * Utility function to convert hexadecimal colors to strings, and simply return the color if it's a string. + * This version can also convert array of colors + * + * @param {Array} array1 First array to compare + * @param {Array} array2 Second array to compare + * @return {boolean} Do the arrays contain the same values in the same order + */ +function areArraysEqual(array1, array2) +{ + if (!Array.isArray(array1) || !Array.isArray(array2)) + { + return false; + } + + if (array1.length !== array2.length) + { + return false; + } + + for (let i = 0; i < array1.length; ++i) + { + if (array1[i] !== array2[i]) + { + return false; + } + } + + return true; +} diff --git a/src/core/textures/BaseRenderTexture.js b/src/core/textures/BaseRenderTexture.js index 5d50146..aa727f0 100644 --- a/src/core/textures/BaseRenderTexture.js +++ b/src/core/textures/BaseRenderTexture.js @@ -1,8 +1,6 @@ import BaseTexture from './BaseTexture'; import settings from '../settings'; -const { RESOLUTION, SCALE_MODE } = settings; - /** * A BaseRenderTexture is a special texture that allows any Pixi display object to be rendered to it. * @@ -54,7 +52,7 @@ { super(null, scaleMode); - this.resolution = resolution || RESOLUTION; + this.resolution = resolution || settings.RESOLUTION; this.width = width; this.height = height; @@ -62,7 +60,7 @@ this.realWidth = this.width * this.resolution; this.realHeight = this.height * this.resolution; - this.scaleMode = scaleMode || SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; this.hasLoaded = true; /** diff --git a/src/core/textures/BaseTexture.js b/src/core/textures/BaseTexture.js index 676dca3..aeccf39 100644 --- a/src/core/textures/BaseTexture.js +++ b/src/core/textures/BaseTexture.js @@ -77,7 +77,7 @@ * @default PIXI.settings.SCALE_MODE * @see PIXI.SCALE_MODES */ - this.scaleMode = scaleMode || settings.SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; /** * Set to true once the base texture has successfully loaded. @@ -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/RenderTexture.js b/src/core/textures/RenderTexture.js index 4c5034f..b3c6297 100644 --- a/src/core/textures/RenderTexture.js +++ b/src/core/textures/RenderTexture.js @@ -54,8 +54,8 @@ /* eslint-disable prefer-rest-params, no-console */ const width = arguments[1]; const height = arguments[2]; - const scaleMode = arguments[3] || 0; - const resolution = arguments[4] || 1; + const scaleMode = arguments[3]; + const resolution = arguments[4]; // we have an old render texture.. console.warn(`Please use RenderTexture.create(${width}, ${height}) instead of the ctor directly.`); 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 ca31456..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 @@ -459,7 +497,9 @@ if (frame.x + frame.width > this.baseTexture.width || frame.y + frame.height > this.baseTexture.height) { - throw new Error(`Texture Error: frame does not fit inside the base Texture dimensions ${this}`); + throw new Error('Texture Error: frame does not fit inside the base Texture dimensions: ' + + `X: ${frame.x} + ${frame.width} > ${this.baseTexture.width} ` + + `Y: ${frame.y} + ${frame.height} > ${this.baseTexture.height}`); } // this.valid = frame && frame.width && frame.height && this.baseTexture.source && this.baseTexture.hasLoaded; @@ -520,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. @@ -528,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 2a7497b..99e585a 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)); } /** @@ -360,3 +362,43 @@ * @private */ export const BaseTextureCache = {}; + +/** + * 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/core/utils/trimCanvas.js b/src/core/utils/trimCanvas.js new file mode 100644 index 0000000..5af6de0 --- /dev/null +++ b/src/core/utils/trimCanvas.js @@ -0,0 +1,83 @@ +/** + * Trim transparent borders from a canvas + * + * @memberof PIXI + * @function trimCanvas + * @private + * @param {HTMLCanvasElement} canvas - the canvas to trim + * @returns {object} Trim data + */ +export default function trimCanvas(canvas) +{ + // https://gist.github.com/remy/784508 + + let width = canvas.width; + let height = canvas.height; + + const context = canvas.getContext('2d'); + const imageData = context.getImageData(0, 0, width, height); + const pixels = imageData.data; + const len = pixels.length; + + const bound = { + top: null, + left: null, + right: null, + bottom: null, + }; + let i; + let x; + let y; + + for (i = 0; i < len; i += 4) + { + if (pixels[i + 3] !== 0) + { + x = (i / 4) % width; + y = ~~((i / 4) / width); + + if (bound.top === null) + { + bound.top = y; + } + + if (bound.left === null) + { + bound.left = x; + } + else if (x < bound.left) + { + bound.left = x; + } + + if (bound.right === null) + { + bound.right = x + 1; + } + else if (bound.right < x) + { + bound.right = x + 1; + } + + if (bound.bottom === null) + { + bound.bottom = y; + } + else if (bound.bottom < y) + { + bound.bottom = y; + } + } + } + + width = bound.right - bound.left; + height = bound.bottom - bound.top + 1; + + const data = context.getImageData(bound.left, bound.top, width, height); + + return { + height, + width, + data, + }; +} diff --git a/src/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/extras/TilingSprite.js b/src/extras/TilingSprite.js index f90d248..fe82451 100644 --- a/src/extras/TilingSprite.js +++ b/src/extras/TilingSprite.js @@ -220,20 +220,33 @@ transform.tx * resolution, transform.ty * resolution); - // TODO - this should be rolled into the setTransform above.. - context.scale(this.tileScale.x / baseTextureResolution, this.tileScale.y / baseTextureResolution); - - context.translate(modX + (this.anchor.x * -this._width), - modY + (this.anchor.y * -this._height)); - renderer.setBlendMode(this.blendMode); // fill the pattern! context.fillStyle = this._canvasPattern; - context.fillRect(-modX, - -modY, - this._width / this.tileScale.x * baseTextureResolution, - this._height / this.tileScale.y * baseTextureResolution); + + // TODO - this should be rolled into the setTransform above.. + context.scale(this.tileScale.x / baseTextureResolution, this.tileScale.y / baseTextureResolution); + + const anchorX = this.anchor.x * -this._width; + const anchorY = this.anchor.y * -this._height; + + if (this.uvRespectAnchor) + { + context.translate(modX, modY); + + context.fillRect(-modX + anchorX, -modY + anchorY, + this._width / this.tileScale.x * baseTextureResolution, + this._height / this.tileScale.y * baseTextureResolution); + } + else + { + context.translate(modX + anchorX, modY + anchorY); + + context.fillRect(-modX, -modY, + this._width / this.tileScale.x * baseTextureResolution, + this._height / this.tileScale.y * baseTextureResolution); + } } /** diff --git a/src/extras/cacheAsBitmap.js b/src/extras/cacheAsBitmap.js index 18043a7..5dd5640 100644 --- a/src/extras/cacheAsBitmap.js +++ b/src/extras/cacheAsBitmap.js @@ -41,6 +41,9 @@ * provide a performance benefit for complex static displayObjects. * To remove simply set this property to 'false' * + * IMPORTANT GOTCHA - make sure that all your textures are preloaded BEFORE setting this property to true + * as it will take a snapshot of what is currently there. If the textures have not loaded then they will not appear. + * * @member {boolean} * @memberof PIXI.DisplayObject# */ diff --git a/src/filters/blur/BlurFilter.js b/src/filters/blur/BlurFilter.js index 340fc58..1575db9 100644 --- a/src/filters/blur/BlurFilter.js +++ b/src/filters/blur/BlurFilter.js @@ -24,10 +24,9 @@ this.blurXFilter = new BlurXFilter(strength, quality, resolution, kernelSize); this.blurYFilter = new BlurYFilter(strength, quality, resolution, kernelSize); - this.resolution = 1; this.padding = 0; - this.resolution = resolution || 1; + this.resolution = resolution || core.settings.RESOLUTION; this.quality = quality || 4; this.blur = strength || 8; } diff --git a/src/filters/blur/BlurXFilter.js b/src/filters/blur/BlurXFilter.js index def99c3..0f48889 100644 --- a/src/filters/blur/BlurXFilter.js +++ b/src/filters/blur/BlurXFilter.js @@ -31,7 +31,7 @@ fragSrc ); - this.resolution = resolution || 1; + this.resolution = resolution || core.settings.RESOLUTION; this._quality = 0; diff --git a/src/filters/blur/BlurYFilter.js b/src/filters/blur/BlurYFilter.js index 638e0f0..6d489c7 100644 --- a/src/filters/blur/BlurYFilter.js +++ b/src/filters/blur/BlurYFilter.js @@ -31,7 +31,7 @@ fragSrc ); - this.resolution = resolution || 1; + this.resolution = resolution || core.settings.RESOLUTION; this._quality = 0; diff --git a/src/filters/colormatrix/ColorMatrixFilter.js b/src/filters/colormatrix/ColorMatrixFilter.js index 8509505..679f8ec 100644 --- a/src/filters/colormatrix/ColorMatrixFilter.js +++ b/src/filters/colormatrix/ColorMatrixFilter.js @@ -75,28 +75,28 @@ out[1] = (a[0] * b[1]) + (a[1] * b[6]) + (a[2] * b[11]) + (a[3] * b[16]); out[2] = (a[0] * b[2]) + (a[1] * b[7]) + (a[2] * b[12]) + (a[3] * b[17]); out[3] = (a[0] * b[3]) + (a[1] * b[8]) + (a[2] * b[13]) + (a[3] * b[18]); - out[4] = (a[0] * b[4]) + (a[1] * b[9]) + (a[2] * b[14]) + (a[3] * b[19]); + out[4] = (a[0] * b[4]) + (a[1] * b[9]) + (a[2] * b[14]) + (a[3] * b[19]) + a[4]; // Green Channel out[5] = (a[5] * b[0]) + (a[6] * b[5]) + (a[7] * b[10]) + (a[8] * b[15]); out[6] = (a[5] * b[1]) + (a[6] * b[6]) + (a[7] * b[11]) + (a[8] * b[16]); out[7] = (a[5] * b[2]) + (a[6] * b[7]) + (a[7] * b[12]) + (a[8] * b[17]); out[8] = (a[5] * b[3]) + (a[6] * b[8]) + (a[7] * b[13]) + (a[8] * b[18]); - out[9] = (a[5] * b[4]) + (a[6] * b[9]) + (a[7] * b[14]) + (a[8] * b[19]); + out[9] = (a[5] * b[4]) + (a[6] * b[9]) + (a[7] * b[14]) + (a[8] * b[19]) + a[9]; // Blue Channel out[10] = (a[10] * b[0]) + (a[11] * b[5]) + (a[12] * b[10]) + (a[13] * b[15]); out[11] = (a[10] * b[1]) + (a[11] * b[6]) + (a[12] * b[11]) + (a[13] * b[16]); out[12] = (a[10] * b[2]) + (a[11] * b[7]) + (a[12] * b[12]) + (a[13] * b[17]); out[13] = (a[10] * b[3]) + (a[11] * b[8]) + (a[12] * b[13]) + (a[13] * b[18]); - out[14] = (a[10] * b[4]) + (a[11] * b[9]) + (a[12] * b[14]) + (a[13] * b[19]); + out[14] = (a[10] * b[4]) + (a[11] * b[9]) + (a[12] * b[14]) + (a[13] * b[19]) + a[14]; // Alpha Channel out[15] = (a[15] * b[0]) + (a[16] * b[5]) + (a[17] * b[10]) + (a[18] * b[15]); out[16] = (a[15] * b[1]) + (a[16] * b[6]) + (a[17] * b[11]) + (a[18] * b[16]); out[17] = (a[15] * b[2]) + (a[16] * b[7]) + (a[17] * b[12]) + (a[18] * b[17]); out[18] = (a[15] * b[3]) + (a[16] * b[8]) + (a[17] * b[13]) + (a[18] * b[18]); - out[19] = (a[15] * b[4]) + (a[16] * b[9]) + (a[17] * b[14]) + (a[18] * b[19]); + out[19] = (a[15] * b[4]) + (a[16] * b[9]) + (a[17] * b[14]) + (a[18] * b[19]) + a[19]; return out; } diff --git a/src/filters/colormatrix/colorMatrix.frag b/src/filters/colormatrix/colorMatrix.frag index 3aaf2b3..c73c0e9 100644 --- a/src/filters/colormatrix/colorMatrix.frag +++ b/src/filters/colormatrix/colorMatrix.frag @@ -4,32 +4,38 @@ void main(void) { - vec4 c = texture2D(uSampler, vTextureCoord); + // Un-premultiply alpha before applying the color matrix. See issue #3539. + if (c.a > 0.0) { + c.rgb /= c.a; + } + vec4 result; + result.r = (m[0] * c.r); + result.r += (m[1] * c.g); + result.r += (m[2] * c.b); + result.r += (m[3] * c.a); + result.r += m[4]; - gl_FragColor.r = (m[0] * c.r); - gl_FragColor.r += (m[1] * c.g); - gl_FragColor.r += (m[2] * c.b); - gl_FragColor.r += (m[3] * c.a); - gl_FragColor.r += m[4] * c.a; + result.g = (m[5] * c.r); + result.g += (m[6] * c.g); + result.g += (m[7] * c.b); + result.g += (m[8] * c.a); + result.g += m[9]; - gl_FragColor.g = (m[5] * c.r); - gl_FragColor.g += (m[6] * c.g); - gl_FragColor.g += (m[7] * c.b); - gl_FragColor.g += (m[8] * c.a); - gl_FragColor.g += m[9] * c.a; + result.b = (m[10] * c.r); + result.b += (m[11] * c.g); + result.b += (m[12] * c.b); + result.b += (m[13] * c.a); + result.b += m[14]; - gl_FragColor.b = (m[10] * c.r); - gl_FragColor.b += (m[11] * c.g); - gl_FragColor.b += (m[12] * c.b); - gl_FragColor.b += (m[13] * c.a); - gl_FragColor.b += m[14] * c.a; + result.a = (m[15] * c.r); + result.a += (m[16] * c.g); + result.a += (m[17] * c.b); + result.a += (m[18] * c.a); + result.a += m[19]; - gl_FragColor.a = (m[15] * c.r); - gl_FragColor.a += (m[16] * c.g); - gl_FragColor.a += (m[17] * c.b); - gl_FragColor.a += (m[18] * c.a); - gl_FragColor.a += m[19] * c.a; + // Premultiply alpha again. + result.rgb *= result.a; -// gl_FragColor = vec4(m[0]); + gl_FragColor = result; } diff --git a/src/filters/displacement/DisplacementFilter.js b/src/filters/displacement/DisplacementFilter.js index 112de0b..d5355c3 100644 --- a/src/filters/displacement/DisplacementFilter.js +++ b/src/filters/displacement/DisplacementFilter.js @@ -36,7 +36,7 @@ this.maskMatrix = maskMatrix; this.uniforms.mapSampler = sprite.texture; - this.uniforms.filterMatrix = maskMatrix.toArray(true); + this.uniforms.filterMatrix = maskMatrix; this.uniforms.scale = { x: 1, y: 1 }; if (scale === null || scale === undefined) diff --git a/src/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/InteractionData.js b/src/interaction/InteractionData.js index 02d53a3..818da35 100644 --- a/src/interaction/InteractionData.js +++ b/src/interaction/InteractionData.js @@ -21,9 +21,9 @@ this.global = new core.Point(); /** - * The target Sprite that was interacted with + * The target DisplayObject that was interacted with * - * @member {PIXI.Sprite} + * @member {PIXI.DisplayObject} */ this.target = null; @@ -33,6 +33,13 @@ * @member {Event} */ this.originalEvent = null; + + /** + * Unique identifier for this interaction + * + * @member {number} + */ + this.identifier = null; } /** diff --git a/src/interaction/InteractionEvent.js b/src/interaction/InteractionEvent.js index 75b2966..dc554f9 100644 --- a/src/interaction/InteractionEvent.js +++ b/src/interaction/InteractionEvent.js @@ -58,7 +58,7 @@ } /** - * Prevents event from reaching any objects other than the current object. + * Resets the event. * * @private */ diff --git a/src/interaction/InteractionManager.js b/src/interaction/InteractionManager.js index 7d51134..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/src/prepare/BasePrepare.js b/src/prepare/BasePrepare.js index a2005f5..09eb5e5 100644 --- a/src/prepare/BasePrepare.js +++ b/src/prepare/BasePrepare.js @@ -20,7 +20,7 @@ * * @abstract * @class - * @memberof PIXI + * @memberof PIXI.prepare */ export default class BasePrepare { @@ -107,9 +107,9 @@ /** * Upload all the textures and graphics to the GPU. * - * @param {Function|PIXI.DisplayObject|PIXI.Container} item - Either - * the container or display object to search for items to upload or - * the callback function, if items have been added using `prepare.add`. + * @param {Function|PIXI.DisplayObject|PIXI.Container|PIXI.BaseTexture|PIXI.Texture|PIXI.Graphics|PIXI.Text} item - + * Either the container or display object to search for items to upload, the items to upload themselves, + * or the callback function, if items have been added using `prepare.add`. * @param {Function} [done] - Optional callback when all queued uploads have completed */ upload(item, done) @@ -236,7 +236,8 @@ /** * Manually add an item to the uploading queue. * - * @param {PIXI.DisplayObject|PIXI.Container|*} item - Object to add to the queue + * @param {PIXI.DisplayObject|PIXI.Container|PIXI.BaseTexture|PIXI.Texture|PIXI.Graphics|PIXI.Text|*} item - Object to + * add to the queue * @return {PIXI.CanvasPrepare} Instance of plugin for chaining. */ add(item) diff --git a/src/prepare/canvas/CanvasPrepare.js b/src/prepare/canvas/CanvasPrepare.js index 4b99d22..0224bb5 100644 --- a/src/prepare/canvas/CanvasPrepare.js +++ b/src/prepare/canvas/CanvasPrepare.js @@ -12,7 +12,8 @@ * An instance of this class is automatically created by default, and can be found at renderer.plugins.prepare * * @class - * @memberof PIXI + * @extends PIXI.prepare.BasePrepare + * @memberof PIXI.prepare */ export default class CanvasPrepare extends BasePrepare { diff --git a/src/prepare/webgl/WebGLPrepare.js b/src/prepare/webgl/WebGLPrepare.js index 1ef7467..eb7023f 100644 --- a/src/prepare/webgl/WebGLPrepare.js +++ b/src/prepare/webgl/WebGLPrepare.js @@ -7,7 +7,8 @@ * An instance of this class is automatically created by default, and can be found at renderer.plugins.prepare * * @class - * @memberof PIXI + * @extends PIXI.prepare.BasePrepare + * @memberof PIXI.prepare */ export default class WebGLPrepare extends BasePrepare { diff --git a/test/core/Circle.js b/test/core/Circle.js index 183f98f..f621992 100644 --- a/test/core/Circle.js +++ b/test/core/Circle.js @@ -53,6 +53,10 @@ expect(circ1.contains(10, 16)).to.be.false; expect(circ1.contains(11, 15)).to.be.false; expect(circ1.contains(0, 0)).to.be.false; + + const circ2 = new PIXI.Circle(10, 10, 0); + + expect(circ2.contains(10, 10)).to.be.false; }); it('should return framing rectangle', function () diff --git a/test/core/Container.js b/test/core/Container.js index 8a10b40..cefde54 100644 --- a/test/core/Container.js +++ b/test/core/Container.js @@ -1,5 +1,39 @@ 'use strict'; +function testAddChild(fn) +{ + return function () + { + fn(function (container, obj) + { + container.addChild(obj); + }); + fn(function (container, obj) + { + container.addChildAt(obj); + }); + }; +} + +function testRemoveChild(fn) +{ + return function () + { + 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 +104,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 +279,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/Ellipse.js b/test/core/Ellipse.js index f2e8234..3f47442 100644 --- a/test/core/Ellipse.js +++ b/test/core/Ellipse.js @@ -56,6 +56,10 @@ expect(ellipse1.contains(10, 16)).to.be.false; expect(ellipse1.contains(11, 15)).to.be.false; expect(ellipse1.contains(0, 0)).to.be.false; + + const ellipse2 = new PIXI.Ellipse(10, 10, 0, 0); + + expect(ellipse2.contains(10, 10)).to.be.false; }); it('should return framing rectangle', function () diff --git a/test/core/Graphics.js b/test/core/Graphics.js index 2a64946..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 () @@ -127,6 +130,16 @@ expect(graphics.containsPoint(point)).to.be.false; }); + + it('should return false when no fill', function () + { + const point = new PIXI.Point(1, 1); + const graphics = new PIXI.Graphics(); + + graphics.drawRect(0, 0, 10, 10); + + expect(graphics.containsPoint(point)).to.be.false; + }); }); describe('arc', function () @@ -208,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/Matrix.js b/test/core/Matrix.js index 9d2694b..76bd57c 100644 --- a/test/core/Matrix.js +++ b/test/core/Matrix.js @@ -107,10 +107,60 @@ expect(m1.ty).to.equal(m2.ty); }); + it('should prepend matrix', function () + { + const m1 = new PIXI.Matrix(); + const m2 = new PIXI.Matrix(); + + m2.set(2, 3, 4, 5, 100, 200); + m1.prepend(m2); + + expect(m1.a).to.equal(m2.a); + expect(m1.b).to.equal(m2.b); + expect(m1.c).to.equal(m2.c); + expect(m1.d).to.equal(m2.d); + expect(m1.tx).to.equal(m2.tx); + expect(m1.ty).to.equal(m2.ty); + + const m3 = new PIXI.Matrix(); + const m4 = new PIXI.Matrix(); + + m3.prepend(m4); + + expect(m3.a).to.equal(m4.a); + expect(m3.b).to.equal(m4.b); + expect(m3.c).to.equal(m4.c); + expect(m3.d).to.equal(m4.d); + expect(m3.tx).to.equal(m4.tx); + expect(m3.ty).to.equal(m4.ty); + }); + it('should get IDENTITY and TEMP_MATRIX', function () { expect(PIXI.Matrix.IDENTITY instanceof PIXI.Matrix).to.be.true; expect(PIXI.Matrix.TEMP_MATRIX instanceof PIXI.Matrix).to.be.true; }); -}); + it('should reset matrix to default when identity() is called', function () + { + const matrix = new PIXI.Matrix(); + + matrix.set(2, 3, 4, 5, 100, 200); + + expect(matrix.a).to.equal(2); + expect(matrix.b).to.equal(3); + expect(matrix.c).to.equal(4); + expect(matrix.d).to.equal(5); + expect(matrix.tx).to.equal(100); + expect(matrix.ty).to.equal(200); + + matrix.identity(); + + expect(matrix.a).to.equal(1); + expect(matrix.b).to.equal(0); + expect(matrix.c).to.equal(0); + expect(matrix.d).to.equal(1); + expect(matrix.tx).to.equal(0); + expect(matrix.ty).to.equal(0); + }); +}); 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/Text.js b/test/core/Text.js index b8ce561..5799ad4 100644 --- a/test/core/Text.js +++ b/test/core/Text.js @@ -112,4 +112,42 @@ expect(text.width).to.equal(300); }); }); + + describe('text', function () + { + it('should convert numbers into strings', function () + { + const text = new PIXI.Text(2); + + expect(text.text).to.equal('2'); + }); + + it('should not change 0 to \'\'', function () + { + const text = new PIXI.Text(0); + + expect(text.text).to.equal('0'); + }); + + it('should prevent setting null', function () + { + const text = new PIXI.Text(null); + + expect(text.text).to.equal(' '); + }); + + it('should prevent setting undefined', function () + { + const text = new PIXI.Text(); + + expect(text.text).to.equal(' '); + }); + + it('should prevent setting \'\'', function () + { + const text = new PIXI.Text(''); + + expect(text.text).to.equal(' '); + }); + }); }); diff --git a/test/core/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/TilingSprite.js b/test/core/TilingSprite.js index 2e449a7..7694792 100644 --- a/test/core/TilingSprite.js +++ b/test/core/TilingSprite.js @@ -24,4 +24,25 @@ expect(bounds.height).to.equal(600); }); }); + + it('checks if tilingSprite contains a point', function () + { + const texture = new PIXI.Texture(new PIXI.BaseTexture()); + const tilingSprite = new PIXI.extras.TilingSprite(texture, 200, 300); + + expect(tilingSprite.containsPoint(new PIXI.Point(1, 1))).to.equal(true); + expect(tilingSprite.containsPoint(new PIXI.Point(300, 400))).to.equal(false); + }); + + it('gets and sets height and width correctly', function () + { + const texture = new PIXI.Texture(new PIXI.BaseTexture()); + const tilingSprite = new PIXI.extras.TilingSprite(texture, 200, 300); + + tilingSprite.width = 400; + tilingSprite.height = 600; + + expect(tilingSprite.width).to.equal(400); + expect(tilingSprite.height).to.equal(600); + }); }); diff --git a/test/core/WebGLRenderer.js b/test/core/WebGLRenderer.js new file mode 100644 index 0000000..3d6cf98 --- /dev/null +++ b/test/core/WebGLRenderer.js @@ -0,0 +1,21 @@ +'use strict'; + +const withGL = require('../withGL'); + +describe('PIXI.WebGLRenderer', function () +{ + it('setting option legacy should disable VAOs and SPRITE_MAX_TEXTURES', withGL(function () + { + const renderer = new PIXI.WebGLRenderer(1, 1, { legacy: true }); + + 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/getLocalBounds.js b/test/core/getLocalBounds.js index d160a56..309da0e 100644 --- a/test/core/getLocalBounds.js +++ b/test/core/getLocalBounds.js @@ -196,4 +196,13 @@ expect(bounds.width).to.equal(100); expect(bounds.height).to.equal(100); }); + + it('should register correct local-bounds with a Text', function () + { + const text = new PIXI.Text('hello'); + const bounds = text.getLocalBounds(); + + expect(bounds.width).to.not.equal(0); + expect(bounds.height).to.not.equal(0); + }); }); diff --git a/test/core/index.js b/test/core/index.js index 2a056a2..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'); @@ -23,4 +24,7 @@ require('./Circle'); require('./Graphics'); 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 1271fef..71ade15 100644 --- a/package.json +++ b/package.json @@ -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/scripts/release.js b/scripts/release.js index c4c8e36..156620c 100644 --- a/scripts/release.js +++ b/scripts/release.js @@ -12,7 +12,8 @@ 'dist/**/*', 'lib/**/*', 'src/**/*', - 'scripts/**/*', + 'scripts/*', + 'scripts/renders/*', 'test/**/*', '*.json', '*.md', diff --git a/scripts/renders/client.js b/scripts/renders/client.js index 16c9565..6faee34 100755 --- a/scripts/renders/client.js +++ b/scripts/renders/client.js @@ -1,6 +1,6 @@ 'use strict'; -require('../../bin/pixi'); +require('../../dist/pixi'); PIXI.utils.skipHello(); const fs = require('fs'); 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 bc730db..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. @@ -33,8 +33,11 @@ * need to call toDataUrl on the webgl context * @param {number} [options.resolution=1] - The resolution / device pixel ratio of the renderer, retina would be 2 * @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 @@ -43,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. */ @@ -73,7 +99,7 @@ */ stop() { - this.ticker.stop(); + this._ticker.stop(); } /** @@ -81,7 +107,7 @@ */ start() { - this.ticker.start(); + this._ticker.start(); } /** @@ -95,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 index 81e076b..78b17e6 100644 --- a/src/core/Shader.js +++ b/src/core/Shader.js @@ -1,9 +1,7 @@ import { GLShader } from 'pixi-gl-core'; import settings from './settings'; -const { PRECISION } = settings; - -function checkPrecision(src) +function checkPrecision(src, def) { if (src instanceof Array) { @@ -11,14 +9,14 @@ { const copy = src.slice(0); - copy.unshift(`precision ${PRECISION} float;`); + copy.unshift(`precision ${def} float;`); return copy; } } else if (src.substring(0, 9) !== 'precision') { - return `precision ${PRECISION} float;\n${src}`; + return `precision ${def} float;\n${src}`; } return src; @@ -42,6 +40,7 @@ */ constructor(gl, vertexSrc, fragmentSrc) { - super(gl, checkPrecision(vertexSrc), checkPrecision(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 3425b41..f9235e4 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -181,7 +181,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 b3792ef..17fde8d 100644 --- a/src/core/display/Container.js +++ b/src/core/display/Container.js @@ -73,13 +73,14 @@ } child.parent = this; - - // ensure a transform will be recalculated.. - this.transform._parentID = -1; - this._boundsID++; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.push(child); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(this.children.length - 1); child.emit('added', this); @@ -108,9 +109,14 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.splice(index, 0, child); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(index); child.emit('added', this); @@ -174,6 +180,7 @@ removeItems(this.children, currentIndex, 1); // remove from old position this.children.splice(index, 0, child); // add at new position + this.onChildrenChange(index); } @@ -220,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! @@ -244,9 +252,14 @@ { const child = this.getChildAt(index); + // ensure child transform will be recalculated.. child.parent = null; + child.transform._parentID = -1; removeItems(this.children, index, 1); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(index); child.emit('removed', this); @@ -275,8 +288,14 @@ for (let i = 0; i < removed.length; ++i) { removed[i].parent = null; + if (removed[i].transform) + { + removed[i].transform._parentID = -1; + } } + this._boundsID++; + this.onChildrenChange(beginIndex); for (let i = 0; i < removed.length; ++i) diff --git a/src/core/display/DisplayObject.js b/src/core/display/DisplayObject.js index 3affe36..4e6c77d 100644 --- a/src/core/display/DisplayObject.js +++ b/src/core/display/DisplayObject.js @@ -109,7 +109,7 @@ /** * The original, cached mask of the object * - * @member {PIXI.Rectangle} + * @member {PIXI.Graphics|PIXI.Sprite} * @private */ this._mask = null; diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index 95f2af4..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 ); @@ -1065,10 +1061,15 @@ canvasRenderer = new CanvasRenderer(); } - tempMatrix.tx = -bounds.x; - tempMatrix.ty = -bounds.y; + this.transform.updateLocalTransform(); + this.transform.localTransform.copy(tempMatrix); - canvasRenderer.render(this, canvasBuffer, false, tempMatrix); + tempMatrix.invert(); + + tempMatrix.tx -= bounds.x; + tempMatrix.ty -= bounds.y; + + canvasRenderer.render(this, canvasBuffer, true, tempMatrix); const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode); 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 e67680f..867dde2 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/buildCircle.js b/src/core/graphics/webgl/utils/buildCircle.js index 0400d13..abf5d02 100644 --- a/src/core/graphics/webgl/utils/buildCircle.js +++ b/src/core/graphics/webgl/utils/buildCircle.js @@ -33,6 +33,11 @@ height = circleData.height; } + if (width === 0 || height === 0) + { + return; + } + const totalSegs = Math.floor(30 * Math.sqrt(circleData.radius)) || Math.floor(15 * Math.sqrt(circleData.width + circleData.height)); diff --git a/src/core/graphics/webgl/utils/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 90155f9..f03fe15 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -30,6 +30,7 @@ export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; export { default as CanvasGraphicsRenderer } from './graphics/canvas/CanvasGraphicsRenderer'; +export { default as Spritesheet } from './textures/Spritesheet'; export { default as Texture } from './textures/Texture'; export { default as BaseTexture } from './textures/BaseTexture'; export { default as RenderTexture } from './textures/RenderTexture'; diff --git a/src/core/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 ccce680..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); } /** @@ -162,6 +162,9 @@ if (transform) { transform.copy(tempWt); + + // lets not forget to flag the parent transform as dirty... + this._tempDisplayObjectParent.transform._worldID = -1; } else { @@ -169,6 +172,7 @@ } displayObject.parent = this._tempDisplayObjectParent; + displayObject.updateTransform(); displayObject.parent = cacheParent; // displayObject.hitArea = //TODO add a temp hit area @@ -279,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/canvas/utils/CanvasRenderTarget.js b/src/core/renderers/canvas/utils/CanvasRenderTarget.js index 3de0aef..b409891 100644 --- a/src/core/renderers/canvas/utils/CanvasRenderTarget.js +++ b/src/core/renderers/canvas/utils/CanvasRenderTarget.js @@ -1,5 +1,4 @@ import settings from '../../../settings'; -const { RESOLUTION } = settings; /** * Creates a Canvas element of the given size. @@ -30,7 +29,7 @@ */ this.context = this.canvas.getContext('2d'); - this.resolution = resolution || RESOLUTION; + this.resolution = resolution || settings.RESOLUTION; this.resize(width, height); } 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 ce0e1c4..788ed6c 100644 --- a/src/core/renderers/webgl/WebGLRenderer.js +++ b/src/core/renderers/webgl/WebGLRenderer.js @@ -30,8 +30,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 @@ -49,10 +49,19 @@ * enable this if you need to call toDataUrl on the webgl context. * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when * rendering, stopping pixel interpolation. + * @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; + + if (this.legacy) + { + glCore.VertexArrayObject.FORCE_NATIVE = true; + } /** * The type of this renderer as a standardised const @@ -229,7 +238,7 @@ this.emit('context', gl); // setup the width/height properties and gl viewport - this.resize(this.width, this.height); + this.resize(this.screen.width, this.screen.height); } /** @@ -323,16 +332,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) { @@ -376,6 +385,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 @@ -440,9 +469,10 @@ * Changes the current shader to the one given in parameter * * @param {PIXI.Shader} shader - the new shader + * @param {boolean} [autoProject=true] - Whether automatically set the projection matrix * @return {PIXI.WebGLRenderer} Returns itself. */ - bindShader(shader) + bindShader(shader, autoProject) { // TODO cache if (this._activeShader !== shader) @@ -450,8 +480,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 bbab3b3..944f41d 100644 --- a/src/core/renderers/webgl/filters/Filter.js +++ b/src/core/renderers/webgl/filters/Filter.js @@ -1,6 +1,7 @@ import extractUniformsFromSrc from './extractUniformsFromSrc'; import { uid } from '../../../utils'; import { BLEND_MODES } from '../../../const'; +import settings from '../../../settings'; const SOURCE_KEY_MAP = {}; @@ -10,7 +11,7 @@ * @memberof PIXI * @extends PIXI.Shader */ -class Filter +export default class Filter { /** * @param {string} [vertexSrc] - The source of the vertex shader. @@ -35,8 +36,6 @@ this.blendMode = BLEND_MODES.NORMAL; - // pull out the vertex and shader uniforms if they are not specified.. - // currently this does not extract structs only default types this.uniformData = uniforms || extractUniformsFromSrc(this.vertexSrc, this.fragmentSrc, 'projectionMatrix|uSampler'); /** @@ -80,7 +79,7 @@ * * @member {number} */ - this.resolution = 1; + this.resolution = settings.RESOLUTION; /** * If enabled is true the filter is applied, if false it will not. @@ -97,8 +96,11 @@ * @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 { // --- // // this.uniforms.filterMatrix = filterManager.calculateSpriteMatrix(tempMatrix, window.panda ); @@ -170,5 +172,3 @@ ].join('\n'); } } - -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 dc7072a..6c7422d 100644 --- a/src/core/renderers/webgl/managers/FilterManager.js +++ b/src/core/renderers/webgl/managers/FilterManager.js @@ -154,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 @@ -176,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; @@ -184,7 +184,7 @@ flop = t; } - filters[i].apply(this, flip, lastState.renderTarget, true); + filters[i].apply(this, flip, lastState.renderTarget, false, currentState); this.freePotRenderTarget(flip); this.freePotRenderTarget(flop); @@ -291,9 +291,13 @@ let textureCount = 1; let currentState; - if (shader.uniforms.data.filterArea) + // 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; @@ -306,9 +310,9 @@ // use this to clamp displaced texture coords so they belong to filterArea // see displacementFilter fragment shader for an example - if (shader.uniforms.data.filterClamp) + if (shader.uniforms.filterClamp) { - currentState = this.filterData.stack[this.filterData.index]; + currentState = currentState || this.filterData.stack[this.filterData.index]; const filterClamp = shader.uniforms.filterClamp; @@ -485,7 +489,7 @@ */ destroy() { - this.shaderCache = []; + this.shaderCache = {}; this.emptyPool(); } diff --git a/src/core/renderers/webgl/utils/RenderTarget.js b/src/core/renderers/webgl/utils/RenderTarget.js index afe51ab..6d2467f 100644 --- a/src/core/renderers/webgl/utils/RenderTarget.js +++ b/src/core/renderers/webgl/utils/RenderTarget.js @@ -124,7 +124,7 @@ * @default PIXI.settings.SCALE_MODE * @see PIXI.SCALE_MODES */ - this.scaleMode = scaleMode || settings.SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; /** * Whether this object is the root element or not 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 36ea5bb..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; @@ -346,8 +346,8 @@ /** * Gets the local bounds of the sprite object. * - * @param {Rectangle} rect - The output rectangle. - * @return {Rectangle} The bounds. + * @param {PIXI.Rectangle} rect - The output rectangle. + * @return {PIXI.Rectangle} The bounds. */ getLocalBounds(rect) { @@ -440,7 +440,7 @@ * * @static * @param {number|string|PIXI.BaseTexture|HTMLCanvasElement|HTMLVideoElement} source Source to create texture from - * @return {PIXI.Texture} 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 e77b39a..90b08c0 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++) @@ -103,11 +102,18 @@ { const gl = this.renderer.gl; - // step 1: first check max textures the GPU can handle. - this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); + if (this.renderer.legacy) + { + this.MAX_TEXTURES = 1; + } + else + { + // step 1: first check max textures the GPU can handle. + this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); - // step 2: check the maximum number of if statements the shader can have too.. - this.MAX_TEXTURES = checkMaxIfStatmentsInShader(this.MAX_TEXTURES, gl); + // step 2: check the maximum number of if statements the shader can have too.. + this.MAX_TEXTURES = checkMaxIfStatmentsInShader(this.MAX_TEXTURES, gl); + } const shader = this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); @@ -130,8 +136,12 @@ .addIndex(this.indexBuffer) .addAttribute(this.vertexBuffers[i], shader.attributes.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) .addAttribute(this.vertexBuffers[i], shader.attributes.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) - .addAttribute(this.vertexBuffers[i], shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4) - .addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + .addAttribute(this.vertexBuffers[i], shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); + + if (shader.attributes.aTextureId) + { + this.vaos[i].addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + } /* eslint-enable max-len */ } @@ -378,8 +388,12 @@ .addIndex(this.indexBuffer) .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) - .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4) - .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); + + if (this.shader.attributes.aTextureId) + { + this.vaos[this.vertexCount].addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + } /* eslint-enable max-len */ } diff --git a/src/core/text/Text.js b/src/core/text/Text.js index 013625a..cec8c32 100644 --- a/src/core/text/Text.js +++ b/src/core/text/Text.js @@ -6,6 +6,7 @@ import { TEXT_GRADIENT } from '../const'; import settings from '../settings'; import TextStyle from './TextStyle'; +import trimCanvas from '../utils/trimCanvas'; const defaultDestroyOptions = { texture: true, @@ -57,7 +58,7 @@ /** * The canvas 2d context that everything is drawn with - * @member {HTMLCanvasElement} + * @member {CanvasRenderingContext2D} */ this.context = this.canvas.getContext('2d'); @@ -189,10 +190,12 @@ if (style.dropShadow) { + this.context.shadowBlur = style.dropShadowBlur; + this.context.globalAlpha = style.dropShadowAlpha; + if (style.dropShadowBlur > 0) { this.context.shadowColor = style.dropShadowColor; - this.context.shadowBlur = style.dropShadowBlur; } else { @@ -237,6 +240,10 @@ } } + // reset the shadow blur and alpha that was set by the drop shadow, for the regular text + this.context.shadowBlur = 0; + this.context.globalAlpha = 1; + // set canvas text styles this.context.fillStyle = this._generateFillStyle(style, lines); @@ -326,6 +333,15 @@ */ updateTexture() { + if (this._style.trim) + { + const trimmed = trimCanvas(this.canvas); + + this.canvas.width = trimmed.width; + this.canvas.height = trimmed.height; + this.context.putImageData(trimmed.data, 0, 0); + } + const texture = this._texture; const style = this._style; @@ -475,6 +491,19 @@ } /** + * Gets the local bounds of the text object. + * + * @param {Rectangle} rect - The output rectangle. + * @return {Rectangle} The bounds. + */ + getLocalBounds(rect) + { + this.updateText(true); + + return super.getLocalBounds.call(this, rect); + } + + /** * calculates the bounds of the Text as a rectangle. The bounds calculation takes the worldTransform into account. */ _calculateBounds() @@ -525,6 +554,29 @@ const width = this.canvas.width / this.resolution; const height = this.canvas.height / this.resolution; + // make a copy of the style settings, so we can manipulate them later + const fill = style.fill.slice(); + const fillGradientStops = style.fillGradientStops.slice(); + + // wanting to evenly distribute the fills. So an array of 4 colours should give fills of 0.25, 0.5 and 0.75 + if (!fillGradientStops.length) + { + const lengthPlus1 = fill.length + 1; + + for (let i = 1; i < lengthPlus1; ++i) + { + fillGradientStops.push(i / lengthPlus1); + } + } + + // stop the bleeding of the last gradient on the line above to the top gradient of the this line + // by hard defining the first gradient colour at point 0, and last gradient colour at point 1 + fill.unshift(style.fill[0]); + fillGradientStops.unshift(0); + + fill.push(style.fill[style.fill.length - 1]); + fillGradientStops.push(1); + if (style.fillGradientType === TEXT_GRADIENT.LINEAR_VERTICAL) { // start the gradient at the top center of the canvas, and end at the bottom middle of the canvas @@ -532,15 +584,22 @@ // we need to repeat the gradient so that each individual line of text has the same vertical gradient effect // ['#FF0000', '#00FF00', '#0000FF'] over 2 lines would create stops at 0.125, 0.25, 0.375, 0.625, 0.75, 0.875 - totalIterations = (style.fill.length + 1) * lines.length; + totalIterations = (fill.length + 1) * lines.length; currentIteration = 0; for (let i = 0; i < lines.length; i++) { currentIteration += 1; - for (let j = 0; j < style.fill.length; j++) + for (let j = 0; j < fill.length; j++) { - stop = (currentIteration / totalIterations); - gradient.addColorStop(stop, style.fill[j]); + if (fillGradientStops[j]) + { + stop = (fillGradientStops[j] / lines.length) + (i / lines.length); + } + else + { + stop = currentIteration / totalIterations; + } + gradient.addColorStop(stop, fill[j]); currentIteration++; } } @@ -552,13 +611,20 @@ // can just evenly space out the gradients in this case, as multiple lines makes no difference // to an even left to right gradient - totalIterations = style.fill.length + 1; + totalIterations = fill.length + 1; currentIteration = 1; - for (let i = 0; i < style.fill.length; i++) + for (let i = 0; i < fill.length; i++) { - stop = currentIteration / totalIterations; - gradient.addColorStop(stop, style.fill[i]); + if (fillGradientStops[i]) + { + stop = fillGradientStops[i]; + } + else + { + stop = currentIteration / totalIterations; + } + gradient.addColorStop(stop, fill[i]); currentIteration++; } } @@ -569,7 +635,7 @@ /** * Destroys this text object. * Note* Unlike a Sprite, a Text object will automatically destroy its baseTexture and texture as - * the majorety of the time the texture will not be shared with any other Sprites. + * the majority of the time the texture will not be shared with any other Sprites. * * @param {object|boolean} [options] - Options parameter. A boolean will act as if all options * have been set to that value @@ -680,7 +746,7 @@ set text(text) // eslint-disable-line require-jsdoc { - text = String(text || ' '); + text = String(text === '' || text === null || text === undefined ? ' ' : text); if (this._text === text) { diff --git a/src/core/text/TextStyle.js b/src/core/text/TextStyle.js index 3b25528..890950b 100644 --- a/src/core/text/TextStyle.js +++ b/src/core/text/TextStyle.js @@ -8,12 +8,14 @@ align: 'left', breakWords: false, dropShadow: false, + dropShadowAlpha: 1, dropShadowAngle: Math.PI / 6, dropShadowBlur: 0, dropShadowColor: '#000000', dropShadowDistance: 5, fill: 'black', fillGradientType: TEXT_GRADIENT.LINEAR_VERTICAL, + fillGradientStops: [], fontFamily: 'Arial', fontSize: 26, fontStyle: 'normal', @@ -27,6 +29,7 @@ stroke: 'black', strokeThickness: 0, textBaseline: 'alphabetic', + trim: false, wordWrap: false, wordWrapWidth: 100, }; @@ -47,6 +50,7 @@ * @param {boolean} [style.breakWords=false] - Indicates if lines can be wrapped within words, it * needs wordWrap to be set to true * @param {boolean} [style.dropShadow=false] - Set a drop shadow for the text + * @param {number} [style.dropShadowAlpha=1] - Set alpha for the drop shadow * @param {number} [style.dropShadowAngle=Math.PI/6] - Set a angle of the drop shadow * @param {number} [style.dropShadowBlur=0] - Set a shadow blur radius * @param {string} [style.dropShadowColor='#000000'] - A fill style to be used on the dropshadow e.g 'red', '#00FF00' @@ -55,8 +59,10 @@ * fillstyle that will be used on the text e.g 'red', '#00FF00'. Can be an array to create a gradient * eg ['#000000','#FFFFFF'] * {@link https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/fillStyle|MDN} - * @param {number} [style.fillGradientType=PIXI.TEXT_GRADIENT.LINEAR_VERTICAL] - If fills styles are - * supplied, this can change the type/direction of the gradient. See {@link PIXI.TEXT_GRADIENT} for possible values + * @param {number} [style.fillGradientType=PIXI.TEXT_GRADIENT.LINEAR_VERTICAL] - If fill is an array of colours + * to create a gradient, this can change the type/direction of the gradient. See {@link PIXI.TEXT_GRADIENT} + * @param {number[]} [style.fillGradientStops] - If fill is an array of colours to create a gradient, this array can set + * the stop points (numbers between 0 and 1) for the color, overriding the default behaviour of evenly spacing them. * @param {string|string[]} [style.fontFamily='Arial'] - The font family * @param {number|string} [style.fontSize=26] - The font size (as a number it converts to px, but as a string, * equivalents are '26px','20pt','160%' or '1.6em') @@ -76,6 +82,7 @@ * e.g 'blue', '#FCFF00' * @param {number} [style.strokeThickness=0] - A number that represents the thickness of the stroke. * Default is 0 (no stroke) + * @param {boolean} [style.trim=false] - Trim transparent borders * @param {string} [style.textBaseline='alphabetic'] - The baseline of the text that is rendered. * @param {boolean} [style.wordWrap=false] - Indicates if word wrap should be used * @param {number} [style.wordWrapWidth=100] - The width at which text will wrap, it needs wordWrap to be set to true @@ -152,6 +159,19 @@ } } + get dropShadowAlpha() + { + return this._dropShadowAlpha; + } + set dropShadowAlpha(dropShadowAlpha) + { + if (this._dropShadowAlpha !== dropShadowAlpha) + { + this._dropShadowAlpha = dropShadowAlpha; + this.styleID++; + } + } + get dropShadowAngle() { return this._dropShadowAngle; @@ -232,6 +252,19 @@ } } + get fillGradientStops() + { + return this._fillGradientStops; + } + set fillGradientStops(fillGradientStops) + { + if (!areArraysEqual(this._fillGradientStops,fillGradientStops)) + { + this._fillGradientStops = fillGradientStops; + this.styleID++; + } + } + get fontFamily() { return this._fontFamily; @@ -402,6 +435,19 @@ } } + get trim() + { + return this._trim; + } + set trim(trim) + { + if (this._trim !== trim) + { + this._trim = trim; + this.styleID++; + } + } + get wordWrap() { return this._wordWrap; @@ -475,3 +521,34 @@ return color; } } + +/** + * Utility function to convert hexadecimal colors to strings, and simply return the color if it's a string. + * This version can also convert array of colors + * + * @param {Array} array1 First array to compare + * @param {Array} array2 Second array to compare + * @return {boolean} Do the arrays contain the same values in the same order + */ +function areArraysEqual(array1, array2) +{ + if (!Array.isArray(array1) || !Array.isArray(array2)) + { + return false; + } + + if (array1.length !== array2.length) + { + return false; + } + + for (let i = 0; i < array1.length; ++i) + { + if (array1[i] !== array2[i]) + { + return false; + } + } + + return true; +} diff --git a/src/core/textures/BaseRenderTexture.js b/src/core/textures/BaseRenderTexture.js index 5d50146..aa727f0 100644 --- a/src/core/textures/BaseRenderTexture.js +++ b/src/core/textures/BaseRenderTexture.js @@ -1,8 +1,6 @@ import BaseTexture from './BaseTexture'; import settings from '../settings'; -const { RESOLUTION, SCALE_MODE } = settings; - /** * A BaseRenderTexture is a special texture that allows any Pixi display object to be rendered to it. * @@ -54,7 +52,7 @@ { super(null, scaleMode); - this.resolution = resolution || RESOLUTION; + this.resolution = resolution || settings.RESOLUTION; this.width = width; this.height = height; @@ -62,7 +60,7 @@ this.realWidth = this.width * this.resolution; this.realHeight = this.height * this.resolution; - this.scaleMode = scaleMode || SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; this.hasLoaded = true; /** diff --git a/src/core/textures/BaseTexture.js b/src/core/textures/BaseTexture.js index 676dca3..aeccf39 100644 --- a/src/core/textures/BaseTexture.js +++ b/src/core/textures/BaseTexture.js @@ -77,7 +77,7 @@ * @default PIXI.settings.SCALE_MODE * @see PIXI.SCALE_MODES */ - this.scaleMode = scaleMode || settings.SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; /** * Set to true once the base texture has successfully loaded. @@ -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/RenderTexture.js b/src/core/textures/RenderTexture.js index 4c5034f..b3c6297 100644 --- a/src/core/textures/RenderTexture.js +++ b/src/core/textures/RenderTexture.js @@ -54,8 +54,8 @@ /* eslint-disable prefer-rest-params, no-console */ const width = arguments[1]; const height = arguments[2]; - const scaleMode = arguments[3] || 0; - const resolution = arguments[4] || 1; + const scaleMode = arguments[3]; + const resolution = arguments[4]; // we have an old render texture.. console.warn(`Please use RenderTexture.create(${width}, ${height}) instead of the ctor directly.`); 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 ca31456..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 @@ -459,7 +497,9 @@ if (frame.x + frame.width > this.baseTexture.width || frame.y + frame.height > this.baseTexture.height) { - throw new Error(`Texture Error: frame does not fit inside the base Texture dimensions ${this}`); + throw new Error('Texture Error: frame does not fit inside the base Texture dimensions: ' + + `X: ${frame.x} + ${frame.width} > ${this.baseTexture.width} ` + + `Y: ${frame.y} + ${frame.height} > ${this.baseTexture.height}`); } // this.valid = frame && frame.width && frame.height && this.baseTexture.source && this.baseTexture.hasLoaded; @@ -520,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. @@ -528,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 2a7497b..99e585a 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)); } /** @@ -360,3 +362,43 @@ * @private */ export const BaseTextureCache = {}; + +/** + * 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/core/utils/trimCanvas.js b/src/core/utils/trimCanvas.js new file mode 100644 index 0000000..5af6de0 --- /dev/null +++ b/src/core/utils/trimCanvas.js @@ -0,0 +1,83 @@ +/** + * Trim transparent borders from a canvas + * + * @memberof PIXI + * @function trimCanvas + * @private + * @param {HTMLCanvasElement} canvas - the canvas to trim + * @returns {object} Trim data + */ +export default function trimCanvas(canvas) +{ + // https://gist.github.com/remy/784508 + + let width = canvas.width; + let height = canvas.height; + + const context = canvas.getContext('2d'); + const imageData = context.getImageData(0, 0, width, height); + const pixels = imageData.data; + const len = pixels.length; + + const bound = { + top: null, + left: null, + right: null, + bottom: null, + }; + let i; + let x; + let y; + + for (i = 0; i < len; i += 4) + { + if (pixels[i + 3] !== 0) + { + x = (i / 4) % width; + y = ~~((i / 4) / width); + + if (bound.top === null) + { + bound.top = y; + } + + if (bound.left === null) + { + bound.left = x; + } + else if (x < bound.left) + { + bound.left = x; + } + + if (bound.right === null) + { + bound.right = x + 1; + } + else if (bound.right < x) + { + bound.right = x + 1; + } + + if (bound.bottom === null) + { + bound.bottom = y; + } + else if (bound.bottom < y) + { + bound.bottom = y; + } + } + } + + width = bound.right - bound.left; + height = bound.bottom - bound.top + 1; + + const data = context.getImageData(bound.left, bound.top, width, height); + + return { + height, + width, + data, + }; +} diff --git a/src/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/extras/TilingSprite.js b/src/extras/TilingSprite.js index f90d248..fe82451 100644 --- a/src/extras/TilingSprite.js +++ b/src/extras/TilingSprite.js @@ -220,20 +220,33 @@ transform.tx * resolution, transform.ty * resolution); - // TODO - this should be rolled into the setTransform above.. - context.scale(this.tileScale.x / baseTextureResolution, this.tileScale.y / baseTextureResolution); - - context.translate(modX + (this.anchor.x * -this._width), - modY + (this.anchor.y * -this._height)); - renderer.setBlendMode(this.blendMode); // fill the pattern! context.fillStyle = this._canvasPattern; - context.fillRect(-modX, - -modY, - this._width / this.tileScale.x * baseTextureResolution, - this._height / this.tileScale.y * baseTextureResolution); + + // TODO - this should be rolled into the setTransform above.. + context.scale(this.tileScale.x / baseTextureResolution, this.tileScale.y / baseTextureResolution); + + const anchorX = this.anchor.x * -this._width; + const anchorY = this.anchor.y * -this._height; + + if (this.uvRespectAnchor) + { + context.translate(modX, modY); + + context.fillRect(-modX + anchorX, -modY + anchorY, + this._width / this.tileScale.x * baseTextureResolution, + this._height / this.tileScale.y * baseTextureResolution); + } + else + { + context.translate(modX + anchorX, modY + anchorY); + + context.fillRect(-modX, -modY, + this._width / this.tileScale.x * baseTextureResolution, + this._height / this.tileScale.y * baseTextureResolution); + } } /** diff --git a/src/extras/cacheAsBitmap.js b/src/extras/cacheAsBitmap.js index 18043a7..5dd5640 100644 --- a/src/extras/cacheAsBitmap.js +++ b/src/extras/cacheAsBitmap.js @@ -41,6 +41,9 @@ * provide a performance benefit for complex static displayObjects. * To remove simply set this property to 'false' * + * IMPORTANT GOTCHA - make sure that all your textures are preloaded BEFORE setting this property to true + * as it will take a snapshot of what is currently there. If the textures have not loaded then they will not appear. + * * @member {boolean} * @memberof PIXI.DisplayObject# */ diff --git a/src/filters/blur/BlurFilter.js b/src/filters/blur/BlurFilter.js index 340fc58..1575db9 100644 --- a/src/filters/blur/BlurFilter.js +++ b/src/filters/blur/BlurFilter.js @@ -24,10 +24,9 @@ this.blurXFilter = new BlurXFilter(strength, quality, resolution, kernelSize); this.blurYFilter = new BlurYFilter(strength, quality, resolution, kernelSize); - this.resolution = 1; this.padding = 0; - this.resolution = resolution || 1; + this.resolution = resolution || core.settings.RESOLUTION; this.quality = quality || 4; this.blur = strength || 8; } diff --git a/src/filters/blur/BlurXFilter.js b/src/filters/blur/BlurXFilter.js index def99c3..0f48889 100644 --- a/src/filters/blur/BlurXFilter.js +++ b/src/filters/blur/BlurXFilter.js @@ -31,7 +31,7 @@ fragSrc ); - this.resolution = resolution || 1; + this.resolution = resolution || core.settings.RESOLUTION; this._quality = 0; diff --git a/src/filters/blur/BlurYFilter.js b/src/filters/blur/BlurYFilter.js index 638e0f0..6d489c7 100644 --- a/src/filters/blur/BlurYFilter.js +++ b/src/filters/blur/BlurYFilter.js @@ -31,7 +31,7 @@ fragSrc ); - this.resolution = resolution || 1; + this.resolution = resolution || core.settings.RESOLUTION; this._quality = 0; diff --git a/src/filters/colormatrix/ColorMatrixFilter.js b/src/filters/colormatrix/ColorMatrixFilter.js index 8509505..679f8ec 100644 --- a/src/filters/colormatrix/ColorMatrixFilter.js +++ b/src/filters/colormatrix/ColorMatrixFilter.js @@ -75,28 +75,28 @@ out[1] = (a[0] * b[1]) + (a[1] * b[6]) + (a[2] * b[11]) + (a[3] * b[16]); out[2] = (a[0] * b[2]) + (a[1] * b[7]) + (a[2] * b[12]) + (a[3] * b[17]); out[3] = (a[0] * b[3]) + (a[1] * b[8]) + (a[2] * b[13]) + (a[3] * b[18]); - out[4] = (a[0] * b[4]) + (a[1] * b[9]) + (a[2] * b[14]) + (a[3] * b[19]); + out[4] = (a[0] * b[4]) + (a[1] * b[9]) + (a[2] * b[14]) + (a[3] * b[19]) + a[4]; // Green Channel out[5] = (a[5] * b[0]) + (a[6] * b[5]) + (a[7] * b[10]) + (a[8] * b[15]); out[6] = (a[5] * b[1]) + (a[6] * b[6]) + (a[7] * b[11]) + (a[8] * b[16]); out[7] = (a[5] * b[2]) + (a[6] * b[7]) + (a[7] * b[12]) + (a[8] * b[17]); out[8] = (a[5] * b[3]) + (a[6] * b[8]) + (a[7] * b[13]) + (a[8] * b[18]); - out[9] = (a[5] * b[4]) + (a[6] * b[9]) + (a[7] * b[14]) + (a[8] * b[19]); + out[9] = (a[5] * b[4]) + (a[6] * b[9]) + (a[7] * b[14]) + (a[8] * b[19]) + a[9]; // Blue Channel out[10] = (a[10] * b[0]) + (a[11] * b[5]) + (a[12] * b[10]) + (a[13] * b[15]); out[11] = (a[10] * b[1]) + (a[11] * b[6]) + (a[12] * b[11]) + (a[13] * b[16]); out[12] = (a[10] * b[2]) + (a[11] * b[7]) + (a[12] * b[12]) + (a[13] * b[17]); out[13] = (a[10] * b[3]) + (a[11] * b[8]) + (a[12] * b[13]) + (a[13] * b[18]); - out[14] = (a[10] * b[4]) + (a[11] * b[9]) + (a[12] * b[14]) + (a[13] * b[19]); + out[14] = (a[10] * b[4]) + (a[11] * b[9]) + (a[12] * b[14]) + (a[13] * b[19]) + a[14]; // Alpha Channel out[15] = (a[15] * b[0]) + (a[16] * b[5]) + (a[17] * b[10]) + (a[18] * b[15]); out[16] = (a[15] * b[1]) + (a[16] * b[6]) + (a[17] * b[11]) + (a[18] * b[16]); out[17] = (a[15] * b[2]) + (a[16] * b[7]) + (a[17] * b[12]) + (a[18] * b[17]); out[18] = (a[15] * b[3]) + (a[16] * b[8]) + (a[17] * b[13]) + (a[18] * b[18]); - out[19] = (a[15] * b[4]) + (a[16] * b[9]) + (a[17] * b[14]) + (a[18] * b[19]); + out[19] = (a[15] * b[4]) + (a[16] * b[9]) + (a[17] * b[14]) + (a[18] * b[19]) + a[19]; return out; } diff --git a/src/filters/colormatrix/colorMatrix.frag b/src/filters/colormatrix/colorMatrix.frag index 3aaf2b3..c73c0e9 100644 --- a/src/filters/colormatrix/colorMatrix.frag +++ b/src/filters/colormatrix/colorMatrix.frag @@ -4,32 +4,38 @@ void main(void) { - vec4 c = texture2D(uSampler, vTextureCoord); + // Un-premultiply alpha before applying the color matrix. See issue #3539. + if (c.a > 0.0) { + c.rgb /= c.a; + } + vec4 result; + result.r = (m[0] * c.r); + result.r += (m[1] * c.g); + result.r += (m[2] * c.b); + result.r += (m[3] * c.a); + result.r += m[4]; - gl_FragColor.r = (m[0] * c.r); - gl_FragColor.r += (m[1] * c.g); - gl_FragColor.r += (m[2] * c.b); - gl_FragColor.r += (m[3] * c.a); - gl_FragColor.r += m[4] * c.a; + result.g = (m[5] * c.r); + result.g += (m[6] * c.g); + result.g += (m[7] * c.b); + result.g += (m[8] * c.a); + result.g += m[9]; - gl_FragColor.g = (m[5] * c.r); - gl_FragColor.g += (m[6] * c.g); - gl_FragColor.g += (m[7] * c.b); - gl_FragColor.g += (m[8] * c.a); - gl_FragColor.g += m[9] * c.a; + result.b = (m[10] * c.r); + result.b += (m[11] * c.g); + result.b += (m[12] * c.b); + result.b += (m[13] * c.a); + result.b += m[14]; - gl_FragColor.b = (m[10] * c.r); - gl_FragColor.b += (m[11] * c.g); - gl_FragColor.b += (m[12] * c.b); - gl_FragColor.b += (m[13] * c.a); - gl_FragColor.b += m[14] * c.a; + result.a = (m[15] * c.r); + result.a += (m[16] * c.g); + result.a += (m[17] * c.b); + result.a += (m[18] * c.a); + result.a += m[19]; - gl_FragColor.a = (m[15] * c.r); - gl_FragColor.a += (m[16] * c.g); - gl_FragColor.a += (m[17] * c.b); - gl_FragColor.a += (m[18] * c.a); - gl_FragColor.a += m[19] * c.a; + // Premultiply alpha again. + result.rgb *= result.a; -// gl_FragColor = vec4(m[0]); + gl_FragColor = result; } diff --git a/src/filters/displacement/DisplacementFilter.js b/src/filters/displacement/DisplacementFilter.js index 112de0b..d5355c3 100644 --- a/src/filters/displacement/DisplacementFilter.js +++ b/src/filters/displacement/DisplacementFilter.js @@ -36,7 +36,7 @@ this.maskMatrix = maskMatrix; this.uniforms.mapSampler = sprite.texture; - this.uniforms.filterMatrix = maskMatrix.toArray(true); + this.uniforms.filterMatrix = maskMatrix; this.uniforms.scale = { x: 1, y: 1 }; if (scale === null || scale === undefined) diff --git a/src/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/InteractionData.js b/src/interaction/InteractionData.js index 02d53a3..818da35 100644 --- a/src/interaction/InteractionData.js +++ b/src/interaction/InteractionData.js @@ -21,9 +21,9 @@ this.global = new core.Point(); /** - * The target Sprite that was interacted with + * The target DisplayObject that was interacted with * - * @member {PIXI.Sprite} + * @member {PIXI.DisplayObject} */ this.target = null; @@ -33,6 +33,13 @@ * @member {Event} */ this.originalEvent = null; + + /** + * Unique identifier for this interaction + * + * @member {number} + */ + this.identifier = null; } /** diff --git a/src/interaction/InteractionEvent.js b/src/interaction/InteractionEvent.js index 75b2966..dc554f9 100644 --- a/src/interaction/InteractionEvent.js +++ b/src/interaction/InteractionEvent.js @@ -58,7 +58,7 @@ } /** - * Prevents event from reaching any objects other than the current object. + * Resets the event. * * @private */ diff --git a/src/interaction/InteractionManager.js b/src/interaction/InteractionManager.js index 7d51134..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/src/prepare/BasePrepare.js b/src/prepare/BasePrepare.js index a2005f5..09eb5e5 100644 --- a/src/prepare/BasePrepare.js +++ b/src/prepare/BasePrepare.js @@ -20,7 +20,7 @@ * * @abstract * @class - * @memberof PIXI + * @memberof PIXI.prepare */ export default class BasePrepare { @@ -107,9 +107,9 @@ /** * Upload all the textures and graphics to the GPU. * - * @param {Function|PIXI.DisplayObject|PIXI.Container} item - Either - * the container or display object to search for items to upload or - * the callback function, if items have been added using `prepare.add`. + * @param {Function|PIXI.DisplayObject|PIXI.Container|PIXI.BaseTexture|PIXI.Texture|PIXI.Graphics|PIXI.Text} item - + * Either the container or display object to search for items to upload, the items to upload themselves, + * or the callback function, if items have been added using `prepare.add`. * @param {Function} [done] - Optional callback when all queued uploads have completed */ upload(item, done) @@ -236,7 +236,8 @@ /** * Manually add an item to the uploading queue. * - * @param {PIXI.DisplayObject|PIXI.Container|*} item - Object to add to the queue + * @param {PIXI.DisplayObject|PIXI.Container|PIXI.BaseTexture|PIXI.Texture|PIXI.Graphics|PIXI.Text|*} item - Object to + * add to the queue * @return {PIXI.CanvasPrepare} Instance of plugin for chaining. */ add(item) diff --git a/src/prepare/canvas/CanvasPrepare.js b/src/prepare/canvas/CanvasPrepare.js index 4b99d22..0224bb5 100644 --- a/src/prepare/canvas/CanvasPrepare.js +++ b/src/prepare/canvas/CanvasPrepare.js @@ -12,7 +12,8 @@ * An instance of this class is automatically created by default, and can be found at renderer.plugins.prepare * * @class - * @memberof PIXI + * @extends PIXI.prepare.BasePrepare + * @memberof PIXI.prepare */ export default class CanvasPrepare extends BasePrepare { diff --git a/src/prepare/webgl/WebGLPrepare.js b/src/prepare/webgl/WebGLPrepare.js index 1ef7467..eb7023f 100644 --- a/src/prepare/webgl/WebGLPrepare.js +++ b/src/prepare/webgl/WebGLPrepare.js @@ -7,7 +7,8 @@ * An instance of this class is automatically created by default, and can be found at renderer.plugins.prepare * * @class - * @memberof PIXI + * @extends PIXI.prepare.BasePrepare + * @memberof PIXI.prepare */ export default class WebGLPrepare extends BasePrepare { diff --git a/test/core/Circle.js b/test/core/Circle.js index 183f98f..f621992 100644 --- a/test/core/Circle.js +++ b/test/core/Circle.js @@ -53,6 +53,10 @@ expect(circ1.contains(10, 16)).to.be.false; expect(circ1.contains(11, 15)).to.be.false; expect(circ1.contains(0, 0)).to.be.false; + + const circ2 = new PIXI.Circle(10, 10, 0); + + expect(circ2.contains(10, 10)).to.be.false; }); it('should return framing rectangle', function () diff --git a/test/core/Container.js b/test/core/Container.js index 8a10b40..cefde54 100644 --- a/test/core/Container.js +++ b/test/core/Container.js @@ -1,5 +1,39 @@ 'use strict'; +function testAddChild(fn) +{ + return function () + { + fn(function (container, obj) + { + container.addChild(obj); + }); + fn(function (container, obj) + { + container.addChildAt(obj); + }); + }; +} + +function testRemoveChild(fn) +{ + return function () + { + 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 +104,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 +279,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/Ellipse.js b/test/core/Ellipse.js index f2e8234..3f47442 100644 --- a/test/core/Ellipse.js +++ b/test/core/Ellipse.js @@ -56,6 +56,10 @@ expect(ellipse1.contains(10, 16)).to.be.false; expect(ellipse1.contains(11, 15)).to.be.false; expect(ellipse1.contains(0, 0)).to.be.false; + + const ellipse2 = new PIXI.Ellipse(10, 10, 0, 0); + + expect(ellipse2.contains(10, 10)).to.be.false; }); it('should return framing rectangle', function () diff --git a/test/core/Graphics.js b/test/core/Graphics.js index 2a64946..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 () @@ -127,6 +130,16 @@ expect(graphics.containsPoint(point)).to.be.false; }); + + it('should return false when no fill', function () + { + const point = new PIXI.Point(1, 1); + const graphics = new PIXI.Graphics(); + + graphics.drawRect(0, 0, 10, 10); + + expect(graphics.containsPoint(point)).to.be.false; + }); }); describe('arc', function () @@ -208,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/Matrix.js b/test/core/Matrix.js index 9d2694b..76bd57c 100644 --- a/test/core/Matrix.js +++ b/test/core/Matrix.js @@ -107,10 +107,60 @@ expect(m1.ty).to.equal(m2.ty); }); + it('should prepend matrix', function () + { + const m1 = new PIXI.Matrix(); + const m2 = new PIXI.Matrix(); + + m2.set(2, 3, 4, 5, 100, 200); + m1.prepend(m2); + + expect(m1.a).to.equal(m2.a); + expect(m1.b).to.equal(m2.b); + expect(m1.c).to.equal(m2.c); + expect(m1.d).to.equal(m2.d); + expect(m1.tx).to.equal(m2.tx); + expect(m1.ty).to.equal(m2.ty); + + const m3 = new PIXI.Matrix(); + const m4 = new PIXI.Matrix(); + + m3.prepend(m4); + + expect(m3.a).to.equal(m4.a); + expect(m3.b).to.equal(m4.b); + expect(m3.c).to.equal(m4.c); + expect(m3.d).to.equal(m4.d); + expect(m3.tx).to.equal(m4.tx); + expect(m3.ty).to.equal(m4.ty); + }); + it('should get IDENTITY and TEMP_MATRIX', function () { expect(PIXI.Matrix.IDENTITY instanceof PIXI.Matrix).to.be.true; expect(PIXI.Matrix.TEMP_MATRIX instanceof PIXI.Matrix).to.be.true; }); -}); + it('should reset matrix to default when identity() is called', function () + { + const matrix = new PIXI.Matrix(); + + matrix.set(2, 3, 4, 5, 100, 200); + + expect(matrix.a).to.equal(2); + expect(matrix.b).to.equal(3); + expect(matrix.c).to.equal(4); + expect(matrix.d).to.equal(5); + expect(matrix.tx).to.equal(100); + expect(matrix.ty).to.equal(200); + + matrix.identity(); + + expect(matrix.a).to.equal(1); + expect(matrix.b).to.equal(0); + expect(matrix.c).to.equal(0); + expect(matrix.d).to.equal(1); + expect(matrix.tx).to.equal(0); + expect(matrix.ty).to.equal(0); + }); +}); 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/Text.js b/test/core/Text.js index b8ce561..5799ad4 100644 --- a/test/core/Text.js +++ b/test/core/Text.js @@ -112,4 +112,42 @@ expect(text.width).to.equal(300); }); }); + + describe('text', function () + { + it('should convert numbers into strings', function () + { + const text = new PIXI.Text(2); + + expect(text.text).to.equal('2'); + }); + + it('should not change 0 to \'\'', function () + { + const text = new PIXI.Text(0); + + expect(text.text).to.equal('0'); + }); + + it('should prevent setting null', function () + { + const text = new PIXI.Text(null); + + expect(text.text).to.equal(' '); + }); + + it('should prevent setting undefined', function () + { + const text = new PIXI.Text(); + + expect(text.text).to.equal(' '); + }); + + it('should prevent setting \'\'', function () + { + const text = new PIXI.Text(''); + + expect(text.text).to.equal(' '); + }); + }); }); diff --git a/test/core/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/TilingSprite.js b/test/core/TilingSprite.js index 2e449a7..7694792 100644 --- a/test/core/TilingSprite.js +++ b/test/core/TilingSprite.js @@ -24,4 +24,25 @@ expect(bounds.height).to.equal(600); }); }); + + it('checks if tilingSprite contains a point', function () + { + const texture = new PIXI.Texture(new PIXI.BaseTexture()); + const tilingSprite = new PIXI.extras.TilingSprite(texture, 200, 300); + + expect(tilingSprite.containsPoint(new PIXI.Point(1, 1))).to.equal(true); + expect(tilingSprite.containsPoint(new PIXI.Point(300, 400))).to.equal(false); + }); + + it('gets and sets height and width correctly', function () + { + const texture = new PIXI.Texture(new PIXI.BaseTexture()); + const tilingSprite = new PIXI.extras.TilingSprite(texture, 200, 300); + + tilingSprite.width = 400; + tilingSprite.height = 600; + + expect(tilingSprite.width).to.equal(400); + expect(tilingSprite.height).to.equal(600); + }); }); diff --git a/test/core/WebGLRenderer.js b/test/core/WebGLRenderer.js new file mode 100644 index 0000000..3d6cf98 --- /dev/null +++ b/test/core/WebGLRenderer.js @@ -0,0 +1,21 @@ +'use strict'; + +const withGL = require('../withGL'); + +describe('PIXI.WebGLRenderer', function () +{ + it('setting option legacy should disable VAOs and SPRITE_MAX_TEXTURES', withGL(function () + { + const renderer = new PIXI.WebGLRenderer(1, 1, { legacy: true }); + + 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/getLocalBounds.js b/test/core/getLocalBounds.js index d160a56..309da0e 100644 --- a/test/core/getLocalBounds.js +++ b/test/core/getLocalBounds.js @@ -196,4 +196,13 @@ expect(bounds.width).to.equal(100); expect(bounds.height).to.equal(100); }); + + it('should register correct local-bounds with a Text', function () + { + const text = new PIXI.Text('hello'); + const bounds = text.getLocalBounds(); + + expect(bounds.width).to.not.equal(0); + expect(bounds.height).to.not.equal(0); + }); }); diff --git a/test/core/index.js b/test/core/index.js index 2a056a2..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'); @@ -23,4 +24,7 @@ require('./Circle'); require('./Graphics'); 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 1271fef..71ade15 100644 --- a/package.json +++ b/package.json @@ -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/scripts/release.js b/scripts/release.js index c4c8e36..156620c 100644 --- a/scripts/release.js +++ b/scripts/release.js @@ -12,7 +12,8 @@ 'dist/**/*', 'lib/**/*', 'src/**/*', - 'scripts/**/*', + 'scripts/*', + 'scripts/renders/*', 'test/**/*', '*.json', '*.md', diff --git a/scripts/renders/client.js b/scripts/renders/client.js index 16c9565..6faee34 100755 --- a/scripts/renders/client.js +++ b/scripts/renders/client.js @@ -1,6 +1,6 @@ 'use strict'; -require('../../bin/pixi'); +require('../../dist/pixi'); PIXI.utils.skipHello(); const fs = require('fs'); 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 bc730db..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. @@ -33,8 +33,11 @@ * need to call toDataUrl on the webgl context * @param {number} [options.resolution=1] - The resolution / device pixel ratio of the renderer, retina would be 2 * @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 @@ -43,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. */ @@ -73,7 +99,7 @@ */ stop() { - this.ticker.stop(); + this._ticker.stop(); } /** @@ -81,7 +107,7 @@ */ start() { - this.ticker.start(); + this._ticker.start(); } /** @@ -95,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 index 81e076b..78b17e6 100644 --- a/src/core/Shader.js +++ b/src/core/Shader.js @@ -1,9 +1,7 @@ import { GLShader } from 'pixi-gl-core'; import settings from './settings'; -const { PRECISION } = settings; - -function checkPrecision(src) +function checkPrecision(src, def) { if (src instanceof Array) { @@ -11,14 +9,14 @@ { const copy = src.slice(0); - copy.unshift(`precision ${PRECISION} float;`); + copy.unshift(`precision ${def} float;`); return copy; } } else if (src.substring(0, 9) !== 'precision') { - return `precision ${PRECISION} float;\n${src}`; + return `precision ${def} float;\n${src}`; } return src; @@ -42,6 +40,7 @@ */ constructor(gl, vertexSrc, fragmentSrc) { - super(gl, checkPrecision(vertexSrc), checkPrecision(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 3425b41..f9235e4 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -181,7 +181,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 b3792ef..17fde8d 100644 --- a/src/core/display/Container.js +++ b/src/core/display/Container.js @@ -73,13 +73,14 @@ } child.parent = this; - - // ensure a transform will be recalculated.. - this.transform._parentID = -1; - this._boundsID++; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.push(child); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(this.children.length - 1); child.emit('added', this); @@ -108,9 +109,14 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.splice(index, 0, child); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(index); child.emit('added', this); @@ -174,6 +180,7 @@ removeItems(this.children, currentIndex, 1); // remove from old position this.children.splice(index, 0, child); // add at new position + this.onChildrenChange(index); } @@ -220,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! @@ -244,9 +252,14 @@ { const child = this.getChildAt(index); + // ensure child transform will be recalculated.. child.parent = null; + child.transform._parentID = -1; removeItems(this.children, index, 1); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(index); child.emit('removed', this); @@ -275,8 +288,14 @@ for (let i = 0; i < removed.length; ++i) { removed[i].parent = null; + if (removed[i].transform) + { + removed[i].transform._parentID = -1; + } } + this._boundsID++; + this.onChildrenChange(beginIndex); for (let i = 0; i < removed.length; ++i) diff --git a/src/core/display/DisplayObject.js b/src/core/display/DisplayObject.js index 3affe36..4e6c77d 100644 --- a/src/core/display/DisplayObject.js +++ b/src/core/display/DisplayObject.js @@ -109,7 +109,7 @@ /** * The original, cached mask of the object * - * @member {PIXI.Rectangle} + * @member {PIXI.Graphics|PIXI.Sprite} * @private */ this._mask = null; diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index 95f2af4..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 ); @@ -1065,10 +1061,15 @@ canvasRenderer = new CanvasRenderer(); } - tempMatrix.tx = -bounds.x; - tempMatrix.ty = -bounds.y; + this.transform.updateLocalTransform(); + this.transform.localTransform.copy(tempMatrix); - canvasRenderer.render(this, canvasBuffer, false, tempMatrix); + tempMatrix.invert(); + + tempMatrix.tx -= bounds.x; + tempMatrix.ty -= bounds.y; + + canvasRenderer.render(this, canvasBuffer, true, tempMatrix); const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode); 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 e67680f..867dde2 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/buildCircle.js b/src/core/graphics/webgl/utils/buildCircle.js index 0400d13..abf5d02 100644 --- a/src/core/graphics/webgl/utils/buildCircle.js +++ b/src/core/graphics/webgl/utils/buildCircle.js @@ -33,6 +33,11 @@ height = circleData.height; } + if (width === 0 || height === 0) + { + return; + } + const totalSegs = Math.floor(30 * Math.sqrt(circleData.radius)) || Math.floor(15 * Math.sqrt(circleData.width + circleData.height)); diff --git a/src/core/graphics/webgl/utils/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 90155f9..f03fe15 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -30,6 +30,7 @@ export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; export { default as CanvasGraphicsRenderer } from './graphics/canvas/CanvasGraphicsRenderer'; +export { default as Spritesheet } from './textures/Spritesheet'; export { default as Texture } from './textures/Texture'; export { default as BaseTexture } from './textures/BaseTexture'; export { default as RenderTexture } from './textures/RenderTexture'; diff --git a/src/core/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 ccce680..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); } /** @@ -162,6 +162,9 @@ if (transform) { transform.copy(tempWt); + + // lets not forget to flag the parent transform as dirty... + this._tempDisplayObjectParent.transform._worldID = -1; } else { @@ -169,6 +172,7 @@ } displayObject.parent = this._tempDisplayObjectParent; + displayObject.updateTransform(); displayObject.parent = cacheParent; // displayObject.hitArea = //TODO add a temp hit area @@ -279,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/canvas/utils/CanvasRenderTarget.js b/src/core/renderers/canvas/utils/CanvasRenderTarget.js index 3de0aef..b409891 100644 --- a/src/core/renderers/canvas/utils/CanvasRenderTarget.js +++ b/src/core/renderers/canvas/utils/CanvasRenderTarget.js @@ -1,5 +1,4 @@ import settings from '../../../settings'; -const { RESOLUTION } = settings; /** * Creates a Canvas element of the given size. @@ -30,7 +29,7 @@ */ this.context = this.canvas.getContext('2d'); - this.resolution = resolution || RESOLUTION; + this.resolution = resolution || settings.RESOLUTION; this.resize(width, height); } 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 ce0e1c4..788ed6c 100644 --- a/src/core/renderers/webgl/WebGLRenderer.js +++ b/src/core/renderers/webgl/WebGLRenderer.js @@ -30,8 +30,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 @@ -49,10 +49,19 @@ * enable this if you need to call toDataUrl on the webgl context. * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when * rendering, stopping pixel interpolation. + * @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; + + if (this.legacy) + { + glCore.VertexArrayObject.FORCE_NATIVE = true; + } /** * The type of this renderer as a standardised const @@ -229,7 +238,7 @@ this.emit('context', gl); // setup the width/height properties and gl viewport - this.resize(this.width, this.height); + this.resize(this.screen.width, this.screen.height); } /** @@ -323,16 +332,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) { @@ -376,6 +385,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 @@ -440,9 +469,10 @@ * Changes the current shader to the one given in parameter * * @param {PIXI.Shader} shader - the new shader + * @param {boolean} [autoProject=true] - Whether automatically set the projection matrix * @return {PIXI.WebGLRenderer} Returns itself. */ - bindShader(shader) + bindShader(shader, autoProject) { // TODO cache if (this._activeShader !== shader) @@ -450,8 +480,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 bbab3b3..944f41d 100644 --- a/src/core/renderers/webgl/filters/Filter.js +++ b/src/core/renderers/webgl/filters/Filter.js @@ -1,6 +1,7 @@ import extractUniformsFromSrc from './extractUniformsFromSrc'; import { uid } from '../../../utils'; import { BLEND_MODES } from '../../../const'; +import settings from '../../../settings'; const SOURCE_KEY_MAP = {}; @@ -10,7 +11,7 @@ * @memberof PIXI * @extends PIXI.Shader */ -class Filter +export default class Filter { /** * @param {string} [vertexSrc] - The source of the vertex shader. @@ -35,8 +36,6 @@ this.blendMode = BLEND_MODES.NORMAL; - // pull out the vertex and shader uniforms if they are not specified.. - // currently this does not extract structs only default types this.uniformData = uniforms || extractUniformsFromSrc(this.vertexSrc, this.fragmentSrc, 'projectionMatrix|uSampler'); /** @@ -80,7 +79,7 @@ * * @member {number} */ - this.resolution = 1; + this.resolution = settings.RESOLUTION; /** * If enabled is true the filter is applied, if false it will not. @@ -97,8 +96,11 @@ * @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 { // --- // // this.uniforms.filterMatrix = filterManager.calculateSpriteMatrix(tempMatrix, window.panda ); @@ -170,5 +172,3 @@ ].join('\n'); } } - -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 dc7072a..6c7422d 100644 --- a/src/core/renderers/webgl/managers/FilterManager.js +++ b/src/core/renderers/webgl/managers/FilterManager.js @@ -154,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 @@ -176,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; @@ -184,7 +184,7 @@ flop = t; } - filters[i].apply(this, flip, lastState.renderTarget, true); + filters[i].apply(this, flip, lastState.renderTarget, false, currentState); this.freePotRenderTarget(flip); this.freePotRenderTarget(flop); @@ -291,9 +291,13 @@ let textureCount = 1; let currentState; - if (shader.uniforms.data.filterArea) + // 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; @@ -306,9 +310,9 @@ // use this to clamp displaced texture coords so they belong to filterArea // see displacementFilter fragment shader for an example - if (shader.uniforms.data.filterClamp) + if (shader.uniforms.filterClamp) { - currentState = this.filterData.stack[this.filterData.index]; + currentState = currentState || this.filterData.stack[this.filterData.index]; const filterClamp = shader.uniforms.filterClamp; @@ -485,7 +489,7 @@ */ destroy() { - this.shaderCache = []; + this.shaderCache = {}; this.emptyPool(); } diff --git a/src/core/renderers/webgl/utils/RenderTarget.js b/src/core/renderers/webgl/utils/RenderTarget.js index afe51ab..6d2467f 100644 --- a/src/core/renderers/webgl/utils/RenderTarget.js +++ b/src/core/renderers/webgl/utils/RenderTarget.js @@ -124,7 +124,7 @@ * @default PIXI.settings.SCALE_MODE * @see PIXI.SCALE_MODES */ - this.scaleMode = scaleMode || settings.SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; /** * Whether this object is the root element or not 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 36ea5bb..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; @@ -346,8 +346,8 @@ /** * Gets the local bounds of the sprite object. * - * @param {Rectangle} rect - The output rectangle. - * @return {Rectangle} The bounds. + * @param {PIXI.Rectangle} rect - The output rectangle. + * @return {PIXI.Rectangle} The bounds. */ getLocalBounds(rect) { @@ -440,7 +440,7 @@ * * @static * @param {number|string|PIXI.BaseTexture|HTMLCanvasElement|HTMLVideoElement} source Source to create texture from - * @return {PIXI.Texture} 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 e77b39a..90b08c0 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++) @@ -103,11 +102,18 @@ { const gl = this.renderer.gl; - // step 1: first check max textures the GPU can handle. - this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); + if (this.renderer.legacy) + { + this.MAX_TEXTURES = 1; + } + else + { + // step 1: first check max textures the GPU can handle. + this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); - // step 2: check the maximum number of if statements the shader can have too.. - this.MAX_TEXTURES = checkMaxIfStatmentsInShader(this.MAX_TEXTURES, gl); + // step 2: check the maximum number of if statements the shader can have too.. + this.MAX_TEXTURES = checkMaxIfStatmentsInShader(this.MAX_TEXTURES, gl); + } const shader = this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); @@ -130,8 +136,12 @@ .addIndex(this.indexBuffer) .addAttribute(this.vertexBuffers[i], shader.attributes.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) .addAttribute(this.vertexBuffers[i], shader.attributes.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) - .addAttribute(this.vertexBuffers[i], shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4) - .addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + .addAttribute(this.vertexBuffers[i], shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); + + if (shader.attributes.aTextureId) + { + this.vaos[i].addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + } /* eslint-enable max-len */ } @@ -378,8 +388,12 @@ .addIndex(this.indexBuffer) .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) - .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4) - .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); + + if (this.shader.attributes.aTextureId) + { + this.vaos[this.vertexCount].addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + } /* eslint-enable max-len */ } diff --git a/src/core/text/Text.js b/src/core/text/Text.js index 013625a..cec8c32 100644 --- a/src/core/text/Text.js +++ b/src/core/text/Text.js @@ -6,6 +6,7 @@ import { TEXT_GRADIENT } from '../const'; import settings from '../settings'; import TextStyle from './TextStyle'; +import trimCanvas from '../utils/trimCanvas'; const defaultDestroyOptions = { texture: true, @@ -57,7 +58,7 @@ /** * The canvas 2d context that everything is drawn with - * @member {HTMLCanvasElement} + * @member {CanvasRenderingContext2D} */ this.context = this.canvas.getContext('2d'); @@ -189,10 +190,12 @@ if (style.dropShadow) { + this.context.shadowBlur = style.dropShadowBlur; + this.context.globalAlpha = style.dropShadowAlpha; + if (style.dropShadowBlur > 0) { this.context.shadowColor = style.dropShadowColor; - this.context.shadowBlur = style.dropShadowBlur; } else { @@ -237,6 +240,10 @@ } } + // reset the shadow blur and alpha that was set by the drop shadow, for the regular text + this.context.shadowBlur = 0; + this.context.globalAlpha = 1; + // set canvas text styles this.context.fillStyle = this._generateFillStyle(style, lines); @@ -326,6 +333,15 @@ */ updateTexture() { + if (this._style.trim) + { + const trimmed = trimCanvas(this.canvas); + + this.canvas.width = trimmed.width; + this.canvas.height = trimmed.height; + this.context.putImageData(trimmed.data, 0, 0); + } + const texture = this._texture; const style = this._style; @@ -475,6 +491,19 @@ } /** + * Gets the local bounds of the text object. + * + * @param {Rectangle} rect - The output rectangle. + * @return {Rectangle} The bounds. + */ + getLocalBounds(rect) + { + this.updateText(true); + + return super.getLocalBounds.call(this, rect); + } + + /** * calculates the bounds of the Text as a rectangle. The bounds calculation takes the worldTransform into account. */ _calculateBounds() @@ -525,6 +554,29 @@ const width = this.canvas.width / this.resolution; const height = this.canvas.height / this.resolution; + // make a copy of the style settings, so we can manipulate them later + const fill = style.fill.slice(); + const fillGradientStops = style.fillGradientStops.slice(); + + // wanting to evenly distribute the fills. So an array of 4 colours should give fills of 0.25, 0.5 and 0.75 + if (!fillGradientStops.length) + { + const lengthPlus1 = fill.length + 1; + + for (let i = 1; i < lengthPlus1; ++i) + { + fillGradientStops.push(i / lengthPlus1); + } + } + + // stop the bleeding of the last gradient on the line above to the top gradient of the this line + // by hard defining the first gradient colour at point 0, and last gradient colour at point 1 + fill.unshift(style.fill[0]); + fillGradientStops.unshift(0); + + fill.push(style.fill[style.fill.length - 1]); + fillGradientStops.push(1); + if (style.fillGradientType === TEXT_GRADIENT.LINEAR_VERTICAL) { // start the gradient at the top center of the canvas, and end at the bottom middle of the canvas @@ -532,15 +584,22 @@ // we need to repeat the gradient so that each individual line of text has the same vertical gradient effect // ['#FF0000', '#00FF00', '#0000FF'] over 2 lines would create stops at 0.125, 0.25, 0.375, 0.625, 0.75, 0.875 - totalIterations = (style.fill.length + 1) * lines.length; + totalIterations = (fill.length + 1) * lines.length; currentIteration = 0; for (let i = 0; i < lines.length; i++) { currentIteration += 1; - for (let j = 0; j < style.fill.length; j++) + for (let j = 0; j < fill.length; j++) { - stop = (currentIteration / totalIterations); - gradient.addColorStop(stop, style.fill[j]); + if (fillGradientStops[j]) + { + stop = (fillGradientStops[j] / lines.length) + (i / lines.length); + } + else + { + stop = currentIteration / totalIterations; + } + gradient.addColorStop(stop, fill[j]); currentIteration++; } } @@ -552,13 +611,20 @@ // can just evenly space out the gradients in this case, as multiple lines makes no difference // to an even left to right gradient - totalIterations = style.fill.length + 1; + totalIterations = fill.length + 1; currentIteration = 1; - for (let i = 0; i < style.fill.length; i++) + for (let i = 0; i < fill.length; i++) { - stop = currentIteration / totalIterations; - gradient.addColorStop(stop, style.fill[i]); + if (fillGradientStops[i]) + { + stop = fillGradientStops[i]; + } + else + { + stop = currentIteration / totalIterations; + } + gradient.addColorStop(stop, fill[i]); currentIteration++; } } @@ -569,7 +635,7 @@ /** * Destroys this text object. * Note* Unlike a Sprite, a Text object will automatically destroy its baseTexture and texture as - * the majorety of the time the texture will not be shared with any other Sprites. + * the majority of the time the texture will not be shared with any other Sprites. * * @param {object|boolean} [options] - Options parameter. A boolean will act as if all options * have been set to that value @@ -680,7 +746,7 @@ set text(text) // eslint-disable-line require-jsdoc { - text = String(text || ' '); + text = String(text === '' || text === null || text === undefined ? ' ' : text); if (this._text === text) { diff --git a/src/core/text/TextStyle.js b/src/core/text/TextStyle.js index 3b25528..890950b 100644 --- a/src/core/text/TextStyle.js +++ b/src/core/text/TextStyle.js @@ -8,12 +8,14 @@ align: 'left', breakWords: false, dropShadow: false, + dropShadowAlpha: 1, dropShadowAngle: Math.PI / 6, dropShadowBlur: 0, dropShadowColor: '#000000', dropShadowDistance: 5, fill: 'black', fillGradientType: TEXT_GRADIENT.LINEAR_VERTICAL, + fillGradientStops: [], fontFamily: 'Arial', fontSize: 26, fontStyle: 'normal', @@ -27,6 +29,7 @@ stroke: 'black', strokeThickness: 0, textBaseline: 'alphabetic', + trim: false, wordWrap: false, wordWrapWidth: 100, }; @@ -47,6 +50,7 @@ * @param {boolean} [style.breakWords=false] - Indicates if lines can be wrapped within words, it * needs wordWrap to be set to true * @param {boolean} [style.dropShadow=false] - Set a drop shadow for the text + * @param {number} [style.dropShadowAlpha=1] - Set alpha for the drop shadow * @param {number} [style.dropShadowAngle=Math.PI/6] - Set a angle of the drop shadow * @param {number} [style.dropShadowBlur=0] - Set a shadow blur radius * @param {string} [style.dropShadowColor='#000000'] - A fill style to be used on the dropshadow e.g 'red', '#00FF00' @@ -55,8 +59,10 @@ * fillstyle that will be used on the text e.g 'red', '#00FF00'. Can be an array to create a gradient * eg ['#000000','#FFFFFF'] * {@link https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/fillStyle|MDN} - * @param {number} [style.fillGradientType=PIXI.TEXT_GRADIENT.LINEAR_VERTICAL] - If fills styles are - * supplied, this can change the type/direction of the gradient. See {@link PIXI.TEXT_GRADIENT} for possible values + * @param {number} [style.fillGradientType=PIXI.TEXT_GRADIENT.LINEAR_VERTICAL] - If fill is an array of colours + * to create a gradient, this can change the type/direction of the gradient. See {@link PIXI.TEXT_GRADIENT} + * @param {number[]} [style.fillGradientStops] - If fill is an array of colours to create a gradient, this array can set + * the stop points (numbers between 0 and 1) for the color, overriding the default behaviour of evenly spacing them. * @param {string|string[]} [style.fontFamily='Arial'] - The font family * @param {number|string} [style.fontSize=26] - The font size (as a number it converts to px, but as a string, * equivalents are '26px','20pt','160%' or '1.6em') @@ -76,6 +82,7 @@ * e.g 'blue', '#FCFF00' * @param {number} [style.strokeThickness=0] - A number that represents the thickness of the stroke. * Default is 0 (no stroke) + * @param {boolean} [style.trim=false] - Trim transparent borders * @param {string} [style.textBaseline='alphabetic'] - The baseline of the text that is rendered. * @param {boolean} [style.wordWrap=false] - Indicates if word wrap should be used * @param {number} [style.wordWrapWidth=100] - The width at which text will wrap, it needs wordWrap to be set to true @@ -152,6 +159,19 @@ } } + get dropShadowAlpha() + { + return this._dropShadowAlpha; + } + set dropShadowAlpha(dropShadowAlpha) + { + if (this._dropShadowAlpha !== dropShadowAlpha) + { + this._dropShadowAlpha = dropShadowAlpha; + this.styleID++; + } + } + get dropShadowAngle() { return this._dropShadowAngle; @@ -232,6 +252,19 @@ } } + get fillGradientStops() + { + return this._fillGradientStops; + } + set fillGradientStops(fillGradientStops) + { + if (!areArraysEqual(this._fillGradientStops,fillGradientStops)) + { + this._fillGradientStops = fillGradientStops; + this.styleID++; + } + } + get fontFamily() { return this._fontFamily; @@ -402,6 +435,19 @@ } } + get trim() + { + return this._trim; + } + set trim(trim) + { + if (this._trim !== trim) + { + this._trim = trim; + this.styleID++; + } + } + get wordWrap() { return this._wordWrap; @@ -475,3 +521,34 @@ return color; } } + +/** + * Utility function to convert hexadecimal colors to strings, and simply return the color if it's a string. + * This version can also convert array of colors + * + * @param {Array} array1 First array to compare + * @param {Array} array2 Second array to compare + * @return {boolean} Do the arrays contain the same values in the same order + */ +function areArraysEqual(array1, array2) +{ + if (!Array.isArray(array1) || !Array.isArray(array2)) + { + return false; + } + + if (array1.length !== array2.length) + { + return false; + } + + for (let i = 0; i < array1.length; ++i) + { + if (array1[i] !== array2[i]) + { + return false; + } + } + + return true; +} diff --git a/src/core/textures/BaseRenderTexture.js b/src/core/textures/BaseRenderTexture.js index 5d50146..aa727f0 100644 --- a/src/core/textures/BaseRenderTexture.js +++ b/src/core/textures/BaseRenderTexture.js @@ -1,8 +1,6 @@ import BaseTexture from './BaseTexture'; import settings from '../settings'; -const { RESOLUTION, SCALE_MODE } = settings; - /** * A BaseRenderTexture is a special texture that allows any Pixi display object to be rendered to it. * @@ -54,7 +52,7 @@ { super(null, scaleMode); - this.resolution = resolution || RESOLUTION; + this.resolution = resolution || settings.RESOLUTION; this.width = width; this.height = height; @@ -62,7 +60,7 @@ this.realWidth = this.width * this.resolution; this.realHeight = this.height * this.resolution; - this.scaleMode = scaleMode || SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; this.hasLoaded = true; /** diff --git a/src/core/textures/BaseTexture.js b/src/core/textures/BaseTexture.js index 676dca3..aeccf39 100644 --- a/src/core/textures/BaseTexture.js +++ b/src/core/textures/BaseTexture.js @@ -77,7 +77,7 @@ * @default PIXI.settings.SCALE_MODE * @see PIXI.SCALE_MODES */ - this.scaleMode = scaleMode || settings.SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; /** * Set to true once the base texture has successfully loaded. @@ -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/RenderTexture.js b/src/core/textures/RenderTexture.js index 4c5034f..b3c6297 100644 --- a/src/core/textures/RenderTexture.js +++ b/src/core/textures/RenderTexture.js @@ -54,8 +54,8 @@ /* eslint-disable prefer-rest-params, no-console */ const width = arguments[1]; const height = arguments[2]; - const scaleMode = arguments[3] || 0; - const resolution = arguments[4] || 1; + const scaleMode = arguments[3]; + const resolution = arguments[4]; // we have an old render texture.. console.warn(`Please use RenderTexture.create(${width}, ${height}) instead of the ctor directly.`); 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 ca31456..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 @@ -459,7 +497,9 @@ if (frame.x + frame.width > this.baseTexture.width || frame.y + frame.height > this.baseTexture.height) { - throw new Error(`Texture Error: frame does not fit inside the base Texture dimensions ${this}`); + throw new Error('Texture Error: frame does not fit inside the base Texture dimensions: ' + + `X: ${frame.x} + ${frame.width} > ${this.baseTexture.width} ` + + `Y: ${frame.y} + ${frame.height} > ${this.baseTexture.height}`); } // this.valid = frame && frame.width && frame.height && this.baseTexture.source && this.baseTexture.hasLoaded; @@ -520,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. @@ -528,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 2a7497b..99e585a 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)); } /** @@ -360,3 +362,43 @@ * @private */ export const BaseTextureCache = {}; + +/** + * 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/core/utils/trimCanvas.js b/src/core/utils/trimCanvas.js new file mode 100644 index 0000000..5af6de0 --- /dev/null +++ b/src/core/utils/trimCanvas.js @@ -0,0 +1,83 @@ +/** + * Trim transparent borders from a canvas + * + * @memberof PIXI + * @function trimCanvas + * @private + * @param {HTMLCanvasElement} canvas - the canvas to trim + * @returns {object} Trim data + */ +export default function trimCanvas(canvas) +{ + // https://gist.github.com/remy/784508 + + let width = canvas.width; + let height = canvas.height; + + const context = canvas.getContext('2d'); + const imageData = context.getImageData(0, 0, width, height); + const pixels = imageData.data; + const len = pixels.length; + + const bound = { + top: null, + left: null, + right: null, + bottom: null, + }; + let i; + let x; + let y; + + for (i = 0; i < len; i += 4) + { + if (pixels[i + 3] !== 0) + { + x = (i / 4) % width; + y = ~~((i / 4) / width); + + if (bound.top === null) + { + bound.top = y; + } + + if (bound.left === null) + { + bound.left = x; + } + else if (x < bound.left) + { + bound.left = x; + } + + if (bound.right === null) + { + bound.right = x + 1; + } + else if (bound.right < x) + { + bound.right = x + 1; + } + + if (bound.bottom === null) + { + bound.bottom = y; + } + else if (bound.bottom < y) + { + bound.bottom = y; + } + } + } + + width = bound.right - bound.left; + height = bound.bottom - bound.top + 1; + + const data = context.getImageData(bound.left, bound.top, width, height); + + return { + height, + width, + data, + }; +} diff --git a/src/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/extras/TilingSprite.js b/src/extras/TilingSprite.js index f90d248..fe82451 100644 --- a/src/extras/TilingSprite.js +++ b/src/extras/TilingSprite.js @@ -220,20 +220,33 @@ transform.tx * resolution, transform.ty * resolution); - // TODO - this should be rolled into the setTransform above.. - context.scale(this.tileScale.x / baseTextureResolution, this.tileScale.y / baseTextureResolution); - - context.translate(modX + (this.anchor.x * -this._width), - modY + (this.anchor.y * -this._height)); - renderer.setBlendMode(this.blendMode); // fill the pattern! context.fillStyle = this._canvasPattern; - context.fillRect(-modX, - -modY, - this._width / this.tileScale.x * baseTextureResolution, - this._height / this.tileScale.y * baseTextureResolution); + + // TODO - this should be rolled into the setTransform above.. + context.scale(this.tileScale.x / baseTextureResolution, this.tileScale.y / baseTextureResolution); + + const anchorX = this.anchor.x * -this._width; + const anchorY = this.anchor.y * -this._height; + + if (this.uvRespectAnchor) + { + context.translate(modX, modY); + + context.fillRect(-modX + anchorX, -modY + anchorY, + this._width / this.tileScale.x * baseTextureResolution, + this._height / this.tileScale.y * baseTextureResolution); + } + else + { + context.translate(modX + anchorX, modY + anchorY); + + context.fillRect(-modX, -modY, + this._width / this.tileScale.x * baseTextureResolution, + this._height / this.tileScale.y * baseTextureResolution); + } } /** diff --git a/src/extras/cacheAsBitmap.js b/src/extras/cacheAsBitmap.js index 18043a7..5dd5640 100644 --- a/src/extras/cacheAsBitmap.js +++ b/src/extras/cacheAsBitmap.js @@ -41,6 +41,9 @@ * provide a performance benefit for complex static displayObjects. * To remove simply set this property to 'false' * + * IMPORTANT GOTCHA - make sure that all your textures are preloaded BEFORE setting this property to true + * as it will take a snapshot of what is currently there. If the textures have not loaded then they will not appear. + * * @member {boolean} * @memberof PIXI.DisplayObject# */ diff --git a/src/filters/blur/BlurFilter.js b/src/filters/blur/BlurFilter.js index 340fc58..1575db9 100644 --- a/src/filters/blur/BlurFilter.js +++ b/src/filters/blur/BlurFilter.js @@ -24,10 +24,9 @@ this.blurXFilter = new BlurXFilter(strength, quality, resolution, kernelSize); this.blurYFilter = new BlurYFilter(strength, quality, resolution, kernelSize); - this.resolution = 1; this.padding = 0; - this.resolution = resolution || 1; + this.resolution = resolution || core.settings.RESOLUTION; this.quality = quality || 4; this.blur = strength || 8; } diff --git a/src/filters/blur/BlurXFilter.js b/src/filters/blur/BlurXFilter.js index def99c3..0f48889 100644 --- a/src/filters/blur/BlurXFilter.js +++ b/src/filters/blur/BlurXFilter.js @@ -31,7 +31,7 @@ fragSrc ); - this.resolution = resolution || 1; + this.resolution = resolution || core.settings.RESOLUTION; this._quality = 0; diff --git a/src/filters/blur/BlurYFilter.js b/src/filters/blur/BlurYFilter.js index 638e0f0..6d489c7 100644 --- a/src/filters/blur/BlurYFilter.js +++ b/src/filters/blur/BlurYFilter.js @@ -31,7 +31,7 @@ fragSrc ); - this.resolution = resolution || 1; + this.resolution = resolution || core.settings.RESOLUTION; this._quality = 0; diff --git a/src/filters/colormatrix/ColorMatrixFilter.js b/src/filters/colormatrix/ColorMatrixFilter.js index 8509505..679f8ec 100644 --- a/src/filters/colormatrix/ColorMatrixFilter.js +++ b/src/filters/colormatrix/ColorMatrixFilter.js @@ -75,28 +75,28 @@ out[1] = (a[0] * b[1]) + (a[1] * b[6]) + (a[2] * b[11]) + (a[3] * b[16]); out[2] = (a[0] * b[2]) + (a[1] * b[7]) + (a[2] * b[12]) + (a[3] * b[17]); out[3] = (a[0] * b[3]) + (a[1] * b[8]) + (a[2] * b[13]) + (a[3] * b[18]); - out[4] = (a[0] * b[4]) + (a[1] * b[9]) + (a[2] * b[14]) + (a[3] * b[19]); + out[4] = (a[0] * b[4]) + (a[1] * b[9]) + (a[2] * b[14]) + (a[3] * b[19]) + a[4]; // Green Channel out[5] = (a[5] * b[0]) + (a[6] * b[5]) + (a[7] * b[10]) + (a[8] * b[15]); out[6] = (a[5] * b[1]) + (a[6] * b[6]) + (a[7] * b[11]) + (a[8] * b[16]); out[7] = (a[5] * b[2]) + (a[6] * b[7]) + (a[7] * b[12]) + (a[8] * b[17]); out[8] = (a[5] * b[3]) + (a[6] * b[8]) + (a[7] * b[13]) + (a[8] * b[18]); - out[9] = (a[5] * b[4]) + (a[6] * b[9]) + (a[7] * b[14]) + (a[8] * b[19]); + out[9] = (a[5] * b[4]) + (a[6] * b[9]) + (a[7] * b[14]) + (a[8] * b[19]) + a[9]; // Blue Channel out[10] = (a[10] * b[0]) + (a[11] * b[5]) + (a[12] * b[10]) + (a[13] * b[15]); out[11] = (a[10] * b[1]) + (a[11] * b[6]) + (a[12] * b[11]) + (a[13] * b[16]); out[12] = (a[10] * b[2]) + (a[11] * b[7]) + (a[12] * b[12]) + (a[13] * b[17]); out[13] = (a[10] * b[3]) + (a[11] * b[8]) + (a[12] * b[13]) + (a[13] * b[18]); - out[14] = (a[10] * b[4]) + (a[11] * b[9]) + (a[12] * b[14]) + (a[13] * b[19]); + out[14] = (a[10] * b[4]) + (a[11] * b[9]) + (a[12] * b[14]) + (a[13] * b[19]) + a[14]; // Alpha Channel out[15] = (a[15] * b[0]) + (a[16] * b[5]) + (a[17] * b[10]) + (a[18] * b[15]); out[16] = (a[15] * b[1]) + (a[16] * b[6]) + (a[17] * b[11]) + (a[18] * b[16]); out[17] = (a[15] * b[2]) + (a[16] * b[7]) + (a[17] * b[12]) + (a[18] * b[17]); out[18] = (a[15] * b[3]) + (a[16] * b[8]) + (a[17] * b[13]) + (a[18] * b[18]); - out[19] = (a[15] * b[4]) + (a[16] * b[9]) + (a[17] * b[14]) + (a[18] * b[19]); + out[19] = (a[15] * b[4]) + (a[16] * b[9]) + (a[17] * b[14]) + (a[18] * b[19]) + a[19]; return out; } diff --git a/src/filters/colormatrix/colorMatrix.frag b/src/filters/colormatrix/colorMatrix.frag index 3aaf2b3..c73c0e9 100644 --- a/src/filters/colormatrix/colorMatrix.frag +++ b/src/filters/colormatrix/colorMatrix.frag @@ -4,32 +4,38 @@ void main(void) { - vec4 c = texture2D(uSampler, vTextureCoord); + // Un-premultiply alpha before applying the color matrix. See issue #3539. + if (c.a > 0.0) { + c.rgb /= c.a; + } + vec4 result; + result.r = (m[0] * c.r); + result.r += (m[1] * c.g); + result.r += (m[2] * c.b); + result.r += (m[3] * c.a); + result.r += m[4]; - gl_FragColor.r = (m[0] * c.r); - gl_FragColor.r += (m[1] * c.g); - gl_FragColor.r += (m[2] * c.b); - gl_FragColor.r += (m[3] * c.a); - gl_FragColor.r += m[4] * c.a; + result.g = (m[5] * c.r); + result.g += (m[6] * c.g); + result.g += (m[7] * c.b); + result.g += (m[8] * c.a); + result.g += m[9]; - gl_FragColor.g = (m[5] * c.r); - gl_FragColor.g += (m[6] * c.g); - gl_FragColor.g += (m[7] * c.b); - gl_FragColor.g += (m[8] * c.a); - gl_FragColor.g += m[9] * c.a; + result.b = (m[10] * c.r); + result.b += (m[11] * c.g); + result.b += (m[12] * c.b); + result.b += (m[13] * c.a); + result.b += m[14]; - gl_FragColor.b = (m[10] * c.r); - gl_FragColor.b += (m[11] * c.g); - gl_FragColor.b += (m[12] * c.b); - gl_FragColor.b += (m[13] * c.a); - gl_FragColor.b += m[14] * c.a; + result.a = (m[15] * c.r); + result.a += (m[16] * c.g); + result.a += (m[17] * c.b); + result.a += (m[18] * c.a); + result.a += m[19]; - gl_FragColor.a = (m[15] * c.r); - gl_FragColor.a += (m[16] * c.g); - gl_FragColor.a += (m[17] * c.b); - gl_FragColor.a += (m[18] * c.a); - gl_FragColor.a += m[19] * c.a; + // Premultiply alpha again. + result.rgb *= result.a; -// gl_FragColor = vec4(m[0]); + gl_FragColor = result; } diff --git a/src/filters/displacement/DisplacementFilter.js b/src/filters/displacement/DisplacementFilter.js index 112de0b..d5355c3 100644 --- a/src/filters/displacement/DisplacementFilter.js +++ b/src/filters/displacement/DisplacementFilter.js @@ -36,7 +36,7 @@ this.maskMatrix = maskMatrix; this.uniforms.mapSampler = sprite.texture; - this.uniforms.filterMatrix = maskMatrix.toArray(true); + this.uniforms.filterMatrix = maskMatrix; this.uniforms.scale = { x: 1, y: 1 }; if (scale === null || scale === undefined) diff --git a/src/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/InteractionData.js b/src/interaction/InteractionData.js index 02d53a3..818da35 100644 --- a/src/interaction/InteractionData.js +++ b/src/interaction/InteractionData.js @@ -21,9 +21,9 @@ this.global = new core.Point(); /** - * The target Sprite that was interacted with + * The target DisplayObject that was interacted with * - * @member {PIXI.Sprite} + * @member {PIXI.DisplayObject} */ this.target = null; @@ -33,6 +33,13 @@ * @member {Event} */ this.originalEvent = null; + + /** + * Unique identifier for this interaction + * + * @member {number} + */ + this.identifier = null; } /** diff --git a/src/interaction/InteractionEvent.js b/src/interaction/InteractionEvent.js index 75b2966..dc554f9 100644 --- a/src/interaction/InteractionEvent.js +++ b/src/interaction/InteractionEvent.js @@ -58,7 +58,7 @@ } /** - * Prevents event from reaching any objects other than the current object. + * Resets the event. * * @private */ diff --git a/src/interaction/InteractionManager.js b/src/interaction/InteractionManager.js index 7d51134..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/src/prepare/BasePrepare.js b/src/prepare/BasePrepare.js index a2005f5..09eb5e5 100644 --- a/src/prepare/BasePrepare.js +++ b/src/prepare/BasePrepare.js @@ -20,7 +20,7 @@ * * @abstract * @class - * @memberof PIXI + * @memberof PIXI.prepare */ export default class BasePrepare { @@ -107,9 +107,9 @@ /** * Upload all the textures and graphics to the GPU. * - * @param {Function|PIXI.DisplayObject|PIXI.Container} item - Either - * the container or display object to search for items to upload or - * the callback function, if items have been added using `prepare.add`. + * @param {Function|PIXI.DisplayObject|PIXI.Container|PIXI.BaseTexture|PIXI.Texture|PIXI.Graphics|PIXI.Text} item - + * Either the container or display object to search for items to upload, the items to upload themselves, + * or the callback function, if items have been added using `prepare.add`. * @param {Function} [done] - Optional callback when all queued uploads have completed */ upload(item, done) @@ -236,7 +236,8 @@ /** * Manually add an item to the uploading queue. * - * @param {PIXI.DisplayObject|PIXI.Container|*} item - Object to add to the queue + * @param {PIXI.DisplayObject|PIXI.Container|PIXI.BaseTexture|PIXI.Texture|PIXI.Graphics|PIXI.Text|*} item - Object to + * add to the queue * @return {PIXI.CanvasPrepare} Instance of plugin for chaining. */ add(item) diff --git a/src/prepare/canvas/CanvasPrepare.js b/src/prepare/canvas/CanvasPrepare.js index 4b99d22..0224bb5 100644 --- a/src/prepare/canvas/CanvasPrepare.js +++ b/src/prepare/canvas/CanvasPrepare.js @@ -12,7 +12,8 @@ * An instance of this class is automatically created by default, and can be found at renderer.plugins.prepare * * @class - * @memberof PIXI + * @extends PIXI.prepare.BasePrepare + * @memberof PIXI.prepare */ export default class CanvasPrepare extends BasePrepare { diff --git a/src/prepare/webgl/WebGLPrepare.js b/src/prepare/webgl/WebGLPrepare.js index 1ef7467..eb7023f 100644 --- a/src/prepare/webgl/WebGLPrepare.js +++ b/src/prepare/webgl/WebGLPrepare.js @@ -7,7 +7,8 @@ * An instance of this class is automatically created by default, and can be found at renderer.plugins.prepare * * @class - * @memberof PIXI + * @extends PIXI.prepare.BasePrepare + * @memberof PIXI.prepare */ export default class WebGLPrepare extends BasePrepare { diff --git a/test/core/Circle.js b/test/core/Circle.js index 183f98f..f621992 100644 --- a/test/core/Circle.js +++ b/test/core/Circle.js @@ -53,6 +53,10 @@ expect(circ1.contains(10, 16)).to.be.false; expect(circ1.contains(11, 15)).to.be.false; expect(circ1.contains(0, 0)).to.be.false; + + const circ2 = new PIXI.Circle(10, 10, 0); + + expect(circ2.contains(10, 10)).to.be.false; }); it('should return framing rectangle', function () diff --git a/test/core/Container.js b/test/core/Container.js index 8a10b40..cefde54 100644 --- a/test/core/Container.js +++ b/test/core/Container.js @@ -1,5 +1,39 @@ 'use strict'; +function testAddChild(fn) +{ + return function () + { + fn(function (container, obj) + { + container.addChild(obj); + }); + fn(function (container, obj) + { + container.addChildAt(obj); + }); + }; +} + +function testRemoveChild(fn) +{ + return function () + { + 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 +104,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 +279,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/Ellipse.js b/test/core/Ellipse.js index f2e8234..3f47442 100644 --- a/test/core/Ellipse.js +++ b/test/core/Ellipse.js @@ -56,6 +56,10 @@ expect(ellipse1.contains(10, 16)).to.be.false; expect(ellipse1.contains(11, 15)).to.be.false; expect(ellipse1.contains(0, 0)).to.be.false; + + const ellipse2 = new PIXI.Ellipse(10, 10, 0, 0); + + expect(ellipse2.contains(10, 10)).to.be.false; }); it('should return framing rectangle', function () diff --git a/test/core/Graphics.js b/test/core/Graphics.js index 2a64946..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 () @@ -127,6 +130,16 @@ expect(graphics.containsPoint(point)).to.be.false; }); + + it('should return false when no fill', function () + { + const point = new PIXI.Point(1, 1); + const graphics = new PIXI.Graphics(); + + graphics.drawRect(0, 0, 10, 10); + + expect(graphics.containsPoint(point)).to.be.false; + }); }); describe('arc', function () @@ -208,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/Matrix.js b/test/core/Matrix.js index 9d2694b..76bd57c 100644 --- a/test/core/Matrix.js +++ b/test/core/Matrix.js @@ -107,10 +107,60 @@ expect(m1.ty).to.equal(m2.ty); }); + it('should prepend matrix', function () + { + const m1 = new PIXI.Matrix(); + const m2 = new PIXI.Matrix(); + + m2.set(2, 3, 4, 5, 100, 200); + m1.prepend(m2); + + expect(m1.a).to.equal(m2.a); + expect(m1.b).to.equal(m2.b); + expect(m1.c).to.equal(m2.c); + expect(m1.d).to.equal(m2.d); + expect(m1.tx).to.equal(m2.tx); + expect(m1.ty).to.equal(m2.ty); + + const m3 = new PIXI.Matrix(); + const m4 = new PIXI.Matrix(); + + m3.prepend(m4); + + expect(m3.a).to.equal(m4.a); + expect(m3.b).to.equal(m4.b); + expect(m3.c).to.equal(m4.c); + expect(m3.d).to.equal(m4.d); + expect(m3.tx).to.equal(m4.tx); + expect(m3.ty).to.equal(m4.ty); + }); + it('should get IDENTITY and TEMP_MATRIX', function () { expect(PIXI.Matrix.IDENTITY instanceof PIXI.Matrix).to.be.true; expect(PIXI.Matrix.TEMP_MATRIX instanceof PIXI.Matrix).to.be.true; }); -}); + it('should reset matrix to default when identity() is called', function () + { + const matrix = new PIXI.Matrix(); + + matrix.set(2, 3, 4, 5, 100, 200); + + expect(matrix.a).to.equal(2); + expect(matrix.b).to.equal(3); + expect(matrix.c).to.equal(4); + expect(matrix.d).to.equal(5); + expect(matrix.tx).to.equal(100); + expect(matrix.ty).to.equal(200); + + matrix.identity(); + + expect(matrix.a).to.equal(1); + expect(matrix.b).to.equal(0); + expect(matrix.c).to.equal(0); + expect(matrix.d).to.equal(1); + expect(matrix.tx).to.equal(0); + expect(matrix.ty).to.equal(0); + }); +}); 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/Text.js b/test/core/Text.js index b8ce561..5799ad4 100644 --- a/test/core/Text.js +++ b/test/core/Text.js @@ -112,4 +112,42 @@ expect(text.width).to.equal(300); }); }); + + describe('text', function () + { + it('should convert numbers into strings', function () + { + const text = new PIXI.Text(2); + + expect(text.text).to.equal('2'); + }); + + it('should not change 0 to \'\'', function () + { + const text = new PIXI.Text(0); + + expect(text.text).to.equal('0'); + }); + + it('should prevent setting null', function () + { + const text = new PIXI.Text(null); + + expect(text.text).to.equal(' '); + }); + + it('should prevent setting undefined', function () + { + const text = new PIXI.Text(); + + expect(text.text).to.equal(' '); + }); + + it('should prevent setting \'\'', function () + { + const text = new PIXI.Text(''); + + expect(text.text).to.equal(' '); + }); + }); }); diff --git a/test/core/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/TilingSprite.js b/test/core/TilingSprite.js index 2e449a7..7694792 100644 --- a/test/core/TilingSprite.js +++ b/test/core/TilingSprite.js @@ -24,4 +24,25 @@ expect(bounds.height).to.equal(600); }); }); + + it('checks if tilingSprite contains a point', function () + { + const texture = new PIXI.Texture(new PIXI.BaseTexture()); + const tilingSprite = new PIXI.extras.TilingSprite(texture, 200, 300); + + expect(tilingSprite.containsPoint(new PIXI.Point(1, 1))).to.equal(true); + expect(tilingSprite.containsPoint(new PIXI.Point(300, 400))).to.equal(false); + }); + + it('gets and sets height and width correctly', function () + { + const texture = new PIXI.Texture(new PIXI.BaseTexture()); + const tilingSprite = new PIXI.extras.TilingSprite(texture, 200, 300); + + tilingSprite.width = 400; + tilingSprite.height = 600; + + expect(tilingSprite.width).to.equal(400); + expect(tilingSprite.height).to.equal(600); + }); }); diff --git a/test/core/WebGLRenderer.js b/test/core/WebGLRenderer.js new file mode 100644 index 0000000..3d6cf98 --- /dev/null +++ b/test/core/WebGLRenderer.js @@ -0,0 +1,21 @@ +'use strict'; + +const withGL = require('../withGL'); + +describe('PIXI.WebGLRenderer', function () +{ + it('setting option legacy should disable VAOs and SPRITE_MAX_TEXTURES', withGL(function () + { + const renderer = new PIXI.WebGLRenderer(1, 1, { legacy: true }); + + 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/getLocalBounds.js b/test/core/getLocalBounds.js index d160a56..309da0e 100644 --- a/test/core/getLocalBounds.js +++ b/test/core/getLocalBounds.js @@ -196,4 +196,13 @@ expect(bounds.width).to.equal(100); expect(bounds.height).to.equal(100); }); + + it('should register correct local-bounds with a Text', function () + { + const text = new PIXI.Text('hello'); + const bounds = text.getLocalBounds(); + + expect(bounds.width).to.not.equal(0); + expect(bounds.height).to.not.equal(0); + }); }); diff --git a/test/core/index.js b/test/core/index.js index 2a056a2..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'); @@ -23,4 +24,7 @@ require('./Circle'); require('./Graphics'); 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 1271fef..71ade15 100644 --- a/package.json +++ b/package.json @@ -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/scripts/release.js b/scripts/release.js index c4c8e36..156620c 100644 --- a/scripts/release.js +++ b/scripts/release.js @@ -12,7 +12,8 @@ 'dist/**/*', 'lib/**/*', 'src/**/*', - 'scripts/**/*', + 'scripts/*', + 'scripts/renders/*', 'test/**/*', '*.json', '*.md', diff --git a/scripts/renders/client.js b/scripts/renders/client.js index 16c9565..6faee34 100755 --- a/scripts/renders/client.js +++ b/scripts/renders/client.js @@ -1,6 +1,6 @@ 'use strict'; -require('../../bin/pixi'); +require('../../dist/pixi'); PIXI.utils.skipHello(); const fs = require('fs'); 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 bc730db..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. @@ -33,8 +33,11 @@ * need to call toDataUrl on the webgl context * @param {number} [options.resolution=1] - The resolution / device pixel ratio of the renderer, retina would be 2 * @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 @@ -43,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. */ @@ -73,7 +99,7 @@ */ stop() { - this.ticker.stop(); + this._ticker.stop(); } /** @@ -81,7 +107,7 @@ */ start() { - this.ticker.start(); + this._ticker.start(); } /** @@ -95,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 index 81e076b..78b17e6 100644 --- a/src/core/Shader.js +++ b/src/core/Shader.js @@ -1,9 +1,7 @@ import { GLShader } from 'pixi-gl-core'; import settings from './settings'; -const { PRECISION } = settings; - -function checkPrecision(src) +function checkPrecision(src, def) { if (src instanceof Array) { @@ -11,14 +9,14 @@ { const copy = src.slice(0); - copy.unshift(`precision ${PRECISION} float;`); + copy.unshift(`precision ${def} float;`); return copy; } } else if (src.substring(0, 9) !== 'precision') { - return `precision ${PRECISION} float;\n${src}`; + return `precision ${def} float;\n${src}`; } return src; @@ -42,6 +40,7 @@ */ constructor(gl, vertexSrc, fragmentSrc) { - super(gl, checkPrecision(vertexSrc), checkPrecision(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 3425b41..f9235e4 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -181,7 +181,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 b3792ef..17fde8d 100644 --- a/src/core/display/Container.js +++ b/src/core/display/Container.js @@ -73,13 +73,14 @@ } child.parent = this; - - // ensure a transform will be recalculated.. - this.transform._parentID = -1; - this._boundsID++; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.push(child); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(this.children.length - 1); child.emit('added', this); @@ -108,9 +109,14 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.splice(index, 0, child); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(index); child.emit('added', this); @@ -174,6 +180,7 @@ removeItems(this.children, currentIndex, 1); // remove from old position this.children.splice(index, 0, child); // add at new position + this.onChildrenChange(index); } @@ -220,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! @@ -244,9 +252,14 @@ { const child = this.getChildAt(index); + // ensure child transform will be recalculated.. child.parent = null; + child.transform._parentID = -1; removeItems(this.children, index, 1); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(index); child.emit('removed', this); @@ -275,8 +288,14 @@ for (let i = 0; i < removed.length; ++i) { removed[i].parent = null; + if (removed[i].transform) + { + removed[i].transform._parentID = -1; + } } + this._boundsID++; + this.onChildrenChange(beginIndex); for (let i = 0; i < removed.length; ++i) diff --git a/src/core/display/DisplayObject.js b/src/core/display/DisplayObject.js index 3affe36..4e6c77d 100644 --- a/src/core/display/DisplayObject.js +++ b/src/core/display/DisplayObject.js @@ -109,7 +109,7 @@ /** * The original, cached mask of the object * - * @member {PIXI.Rectangle} + * @member {PIXI.Graphics|PIXI.Sprite} * @private */ this._mask = null; diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index 95f2af4..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 ); @@ -1065,10 +1061,15 @@ canvasRenderer = new CanvasRenderer(); } - tempMatrix.tx = -bounds.x; - tempMatrix.ty = -bounds.y; + this.transform.updateLocalTransform(); + this.transform.localTransform.copy(tempMatrix); - canvasRenderer.render(this, canvasBuffer, false, tempMatrix); + tempMatrix.invert(); + + tempMatrix.tx -= bounds.x; + tempMatrix.ty -= bounds.y; + + canvasRenderer.render(this, canvasBuffer, true, tempMatrix); const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode); 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 e67680f..867dde2 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/buildCircle.js b/src/core/graphics/webgl/utils/buildCircle.js index 0400d13..abf5d02 100644 --- a/src/core/graphics/webgl/utils/buildCircle.js +++ b/src/core/graphics/webgl/utils/buildCircle.js @@ -33,6 +33,11 @@ height = circleData.height; } + if (width === 0 || height === 0) + { + return; + } + const totalSegs = Math.floor(30 * Math.sqrt(circleData.radius)) || Math.floor(15 * Math.sqrt(circleData.width + circleData.height)); diff --git a/src/core/graphics/webgl/utils/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 90155f9..f03fe15 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -30,6 +30,7 @@ export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; export { default as CanvasGraphicsRenderer } from './graphics/canvas/CanvasGraphicsRenderer'; +export { default as Spritesheet } from './textures/Spritesheet'; export { default as Texture } from './textures/Texture'; export { default as BaseTexture } from './textures/BaseTexture'; export { default as RenderTexture } from './textures/RenderTexture'; diff --git a/src/core/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 ccce680..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); } /** @@ -162,6 +162,9 @@ if (transform) { transform.copy(tempWt); + + // lets not forget to flag the parent transform as dirty... + this._tempDisplayObjectParent.transform._worldID = -1; } else { @@ -169,6 +172,7 @@ } displayObject.parent = this._tempDisplayObjectParent; + displayObject.updateTransform(); displayObject.parent = cacheParent; // displayObject.hitArea = //TODO add a temp hit area @@ -279,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/canvas/utils/CanvasRenderTarget.js b/src/core/renderers/canvas/utils/CanvasRenderTarget.js index 3de0aef..b409891 100644 --- a/src/core/renderers/canvas/utils/CanvasRenderTarget.js +++ b/src/core/renderers/canvas/utils/CanvasRenderTarget.js @@ -1,5 +1,4 @@ import settings from '../../../settings'; -const { RESOLUTION } = settings; /** * Creates a Canvas element of the given size. @@ -30,7 +29,7 @@ */ this.context = this.canvas.getContext('2d'); - this.resolution = resolution || RESOLUTION; + this.resolution = resolution || settings.RESOLUTION; this.resize(width, height); } 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 ce0e1c4..788ed6c 100644 --- a/src/core/renderers/webgl/WebGLRenderer.js +++ b/src/core/renderers/webgl/WebGLRenderer.js @@ -30,8 +30,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 @@ -49,10 +49,19 @@ * enable this if you need to call toDataUrl on the webgl context. * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when * rendering, stopping pixel interpolation. + * @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; + + if (this.legacy) + { + glCore.VertexArrayObject.FORCE_NATIVE = true; + } /** * The type of this renderer as a standardised const @@ -229,7 +238,7 @@ this.emit('context', gl); // setup the width/height properties and gl viewport - this.resize(this.width, this.height); + this.resize(this.screen.width, this.screen.height); } /** @@ -323,16 +332,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) { @@ -376,6 +385,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 @@ -440,9 +469,10 @@ * Changes the current shader to the one given in parameter * * @param {PIXI.Shader} shader - the new shader + * @param {boolean} [autoProject=true] - Whether automatically set the projection matrix * @return {PIXI.WebGLRenderer} Returns itself. */ - bindShader(shader) + bindShader(shader, autoProject) { // TODO cache if (this._activeShader !== shader) @@ -450,8 +480,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 bbab3b3..944f41d 100644 --- a/src/core/renderers/webgl/filters/Filter.js +++ b/src/core/renderers/webgl/filters/Filter.js @@ -1,6 +1,7 @@ import extractUniformsFromSrc from './extractUniformsFromSrc'; import { uid } from '../../../utils'; import { BLEND_MODES } from '../../../const'; +import settings from '../../../settings'; const SOURCE_KEY_MAP = {}; @@ -10,7 +11,7 @@ * @memberof PIXI * @extends PIXI.Shader */ -class Filter +export default class Filter { /** * @param {string} [vertexSrc] - The source of the vertex shader. @@ -35,8 +36,6 @@ this.blendMode = BLEND_MODES.NORMAL; - // pull out the vertex and shader uniforms if they are not specified.. - // currently this does not extract structs only default types this.uniformData = uniforms || extractUniformsFromSrc(this.vertexSrc, this.fragmentSrc, 'projectionMatrix|uSampler'); /** @@ -80,7 +79,7 @@ * * @member {number} */ - this.resolution = 1; + this.resolution = settings.RESOLUTION; /** * If enabled is true the filter is applied, if false it will not. @@ -97,8 +96,11 @@ * @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 { // --- // // this.uniforms.filterMatrix = filterManager.calculateSpriteMatrix(tempMatrix, window.panda ); @@ -170,5 +172,3 @@ ].join('\n'); } } - -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 dc7072a..6c7422d 100644 --- a/src/core/renderers/webgl/managers/FilterManager.js +++ b/src/core/renderers/webgl/managers/FilterManager.js @@ -154,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 @@ -176,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; @@ -184,7 +184,7 @@ flop = t; } - filters[i].apply(this, flip, lastState.renderTarget, true); + filters[i].apply(this, flip, lastState.renderTarget, false, currentState); this.freePotRenderTarget(flip); this.freePotRenderTarget(flop); @@ -291,9 +291,13 @@ let textureCount = 1; let currentState; - if (shader.uniforms.data.filterArea) + // 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; @@ -306,9 +310,9 @@ // use this to clamp displaced texture coords so they belong to filterArea // see displacementFilter fragment shader for an example - if (shader.uniforms.data.filterClamp) + if (shader.uniforms.filterClamp) { - currentState = this.filterData.stack[this.filterData.index]; + currentState = currentState || this.filterData.stack[this.filterData.index]; const filterClamp = shader.uniforms.filterClamp; @@ -485,7 +489,7 @@ */ destroy() { - this.shaderCache = []; + this.shaderCache = {}; this.emptyPool(); } diff --git a/src/core/renderers/webgl/utils/RenderTarget.js b/src/core/renderers/webgl/utils/RenderTarget.js index afe51ab..6d2467f 100644 --- a/src/core/renderers/webgl/utils/RenderTarget.js +++ b/src/core/renderers/webgl/utils/RenderTarget.js @@ -124,7 +124,7 @@ * @default PIXI.settings.SCALE_MODE * @see PIXI.SCALE_MODES */ - this.scaleMode = scaleMode || settings.SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; /** * Whether this object is the root element or not 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 36ea5bb..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; @@ -346,8 +346,8 @@ /** * Gets the local bounds of the sprite object. * - * @param {Rectangle} rect - The output rectangle. - * @return {Rectangle} The bounds. + * @param {PIXI.Rectangle} rect - The output rectangle. + * @return {PIXI.Rectangle} The bounds. */ getLocalBounds(rect) { @@ -440,7 +440,7 @@ * * @static * @param {number|string|PIXI.BaseTexture|HTMLCanvasElement|HTMLVideoElement} source Source to create texture from - * @return {PIXI.Texture} 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 e77b39a..90b08c0 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++) @@ -103,11 +102,18 @@ { const gl = this.renderer.gl; - // step 1: first check max textures the GPU can handle. - this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); + if (this.renderer.legacy) + { + this.MAX_TEXTURES = 1; + } + else + { + // step 1: first check max textures the GPU can handle. + this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); - // step 2: check the maximum number of if statements the shader can have too.. - this.MAX_TEXTURES = checkMaxIfStatmentsInShader(this.MAX_TEXTURES, gl); + // step 2: check the maximum number of if statements the shader can have too.. + this.MAX_TEXTURES = checkMaxIfStatmentsInShader(this.MAX_TEXTURES, gl); + } const shader = this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); @@ -130,8 +136,12 @@ .addIndex(this.indexBuffer) .addAttribute(this.vertexBuffers[i], shader.attributes.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) .addAttribute(this.vertexBuffers[i], shader.attributes.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) - .addAttribute(this.vertexBuffers[i], shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4) - .addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + .addAttribute(this.vertexBuffers[i], shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); + + if (shader.attributes.aTextureId) + { + this.vaos[i].addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + } /* eslint-enable max-len */ } @@ -378,8 +388,12 @@ .addIndex(this.indexBuffer) .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) - .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4) - .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); + + if (this.shader.attributes.aTextureId) + { + this.vaos[this.vertexCount].addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + } /* eslint-enable max-len */ } diff --git a/src/core/text/Text.js b/src/core/text/Text.js index 013625a..cec8c32 100644 --- a/src/core/text/Text.js +++ b/src/core/text/Text.js @@ -6,6 +6,7 @@ import { TEXT_GRADIENT } from '../const'; import settings from '../settings'; import TextStyle from './TextStyle'; +import trimCanvas from '../utils/trimCanvas'; const defaultDestroyOptions = { texture: true, @@ -57,7 +58,7 @@ /** * The canvas 2d context that everything is drawn with - * @member {HTMLCanvasElement} + * @member {CanvasRenderingContext2D} */ this.context = this.canvas.getContext('2d'); @@ -189,10 +190,12 @@ if (style.dropShadow) { + this.context.shadowBlur = style.dropShadowBlur; + this.context.globalAlpha = style.dropShadowAlpha; + if (style.dropShadowBlur > 0) { this.context.shadowColor = style.dropShadowColor; - this.context.shadowBlur = style.dropShadowBlur; } else { @@ -237,6 +240,10 @@ } } + // reset the shadow blur and alpha that was set by the drop shadow, for the regular text + this.context.shadowBlur = 0; + this.context.globalAlpha = 1; + // set canvas text styles this.context.fillStyle = this._generateFillStyle(style, lines); @@ -326,6 +333,15 @@ */ updateTexture() { + if (this._style.trim) + { + const trimmed = trimCanvas(this.canvas); + + this.canvas.width = trimmed.width; + this.canvas.height = trimmed.height; + this.context.putImageData(trimmed.data, 0, 0); + } + const texture = this._texture; const style = this._style; @@ -475,6 +491,19 @@ } /** + * Gets the local bounds of the text object. + * + * @param {Rectangle} rect - The output rectangle. + * @return {Rectangle} The bounds. + */ + getLocalBounds(rect) + { + this.updateText(true); + + return super.getLocalBounds.call(this, rect); + } + + /** * calculates the bounds of the Text as a rectangle. The bounds calculation takes the worldTransform into account. */ _calculateBounds() @@ -525,6 +554,29 @@ const width = this.canvas.width / this.resolution; const height = this.canvas.height / this.resolution; + // make a copy of the style settings, so we can manipulate them later + const fill = style.fill.slice(); + const fillGradientStops = style.fillGradientStops.slice(); + + // wanting to evenly distribute the fills. So an array of 4 colours should give fills of 0.25, 0.5 and 0.75 + if (!fillGradientStops.length) + { + const lengthPlus1 = fill.length + 1; + + for (let i = 1; i < lengthPlus1; ++i) + { + fillGradientStops.push(i / lengthPlus1); + } + } + + // stop the bleeding of the last gradient on the line above to the top gradient of the this line + // by hard defining the first gradient colour at point 0, and last gradient colour at point 1 + fill.unshift(style.fill[0]); + fillGradientStops.unshift(0); + + fill.push(style.fill[style.fill.length - 1]); + fillGradientStops.push(1); + if (style.fillGradientType === TEXT_GRADIENT.LINEAR_VERTICAL) { // start the gradient at the top center of the canvas, and end at the bottom middle of the canvas @@ -532,15 +584,22 @@ // we need to repeat the gradient so that each individual line of text has the same vertical gradient effect // ['#FF0000', '#00FF00', '#0000FF'] over 2 lines would create stops at 0.125, 0.25, 0.375, 0.625, 0.75, 0.875 - totalIterations = (style.fill.length + 1) * lines.length; + totalIterations = (fill.length + 1) * lines.length; currentIteration = 0; for (let i = 0; i < lines.length; i++) { currentIteration += 1; - for (let j = 0; j < style.fill.length; j++) + for (let j = 0; j < fill.length; j++) { - stop = (currentIteration / totalIterations); - gradient.addColorStop(stop, style.fill[j]); + if (fillGradientStops[j]) + { + stop = (fillGradientStops[j] / lines.length) + (i / lines.length); + } + else + { + stop = currentIteration / totalIterations; + } + gradient.addColorStop(stop, fill[j]); currentIteration++; } } @@ -552,13 +611,20 @@ // can just evenly space out the gradients in this case, as multiple lines makes no difference // to an even left to right gradient - totalIterations = style.fill.length + 1; + totalIterations = fill.length + 1; currentIteration = 1; - for (let i = 0; i < style.fill.length; i++) + for (let i = 0; i < fill.length; i++) { - stop = currentIteration / totalIterations; - gradient.addColorStop(stop, style.fill[i]); + if (fillGradientStops[i]) + { + stop = fillGradientStops[i]; + } + else + { + stop = currentIteration / totalIterations; + } + gradient.addColorStop(stop, fill[i]); currentIteration++; } } @@ -569,7 +635,7 @@ /** * Destroys this text object. * Note* Unlike a Sprite, a Text object will automatically destroy its baseTexture and texture as - * the majorety of the time the texture will not be shared with any other Sprites. + * the majority of the time the texture will not be shared with any other Sprites. * * @param {object|boolean} [options] - Options parameter. A boolean will act as if all options * have been set to that value @@ -680,7 +746,7 @@ set text(text) // eslint-disable-line require-jsdoc { - text = String(text || ' '); + text = String(text === '' || text === null || text === undefined ? ' ' : text); if (this._text === text) { diff --git a/src/core/text/TextStyle.js b/src/core/text/TextStyle.js index 3b25528..890950b 100644 --- a/src/core/text/TextStyle.js +++ b/src/core/text/TextStyle.js @@ -8,12 +8,14 @@ align: 'left', breakWords: false, dropShadow: false, + dropShadowAlpha: 1, dropShadowAngle: Math.PI / 6, dropShadowBlur: 0, dropShadowColor: '#000000', dropShadowDistance: 5, fill: 'black', fillGradientType: TEXT_GRADIENT.LINEAR_VERTICAL, + fillGradientStops: [], fontFamily: 'Arial', fontSize: 26, fontStyle: 'normal', @@ -27,6 +29,7 @@ stroke: 'black', strokeThickness: 0, textBaseline: 'alphabetic', + trim: false, wordWrap: false, wordWrapWidth: 100, }; @@ -47,6 +50,7 @@ * @param {boolean} [style.breakWords=false] - Indicates if lines can be wrapped within words, it * needs wordWrap to be set to true * @param {boolean} [style.dropShadow=false] - Set a drop shadow for the text + * @param {number} [style.dropShadowAlpha=1] - Set alpha for the drop shadow * @param {number} [style.dropShadowAngle=Math.PI/6] - Set a angle of the drop shadow * @param {number} [style.dropShadowBlur=0] - Set a shadow blur radius * @param {string} [style.dropShadowColor='#000000'] - A fill style to be used on the dropshadow e.g 'red', '#00FF00' @@ -55,8 +59,10 @@ * fillstyle that will be used on the text e.g 'red', '#00FF00'. Can be an array to create a gradient * eg ['#000000','#FFFFFF'] * {@link https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/fillStyle|MDN} - * @param {number} [style.fillGradientType=PIXI.TEXT_GRADIENT.LINEAR_VERTICAL] - If fills styles are - * supplied, this can change the type/direction of the gradient. See {@link PIXI.TEXT_GRADIENT} for possible values + * @param {number} [style.fillGradientType=PIXI.TEXT_GRADIENT.LINEAR_VERTICAL] - If fill is an array of colours + * to create a gradient, this can change the type/direction of the gradient. See {@link PIXI.TEXT_GRADIENT} + * @param {number[]} [style.fillGradientStops] - If fill is an array of colours to create a gradient, this array can set + * the stop points (numbers between 0 and 1) for the color, overriding the default behaviour of evenly spacing them. * @param {string|string[]} [style.fontFamily='Arial'] - The font family * @param {number|string} [style.fontSize=26] - The font size (as a number it converts to px, but as a string, * equivalents are '26px','20pt','160%' or '1.6em') @@ -76,6 +82,7 @@ * e.g 'blue', '#FCFF00' * @param {number} [style.strokeThickness=0] - A number that represents the thickness of the stroke. * Default is 0 (no stroke) + * @param {boolean} [style.trim=false] - Trim transparent borders * @param {string} [style.textBaseline='alphabetic'] - The baseline of the text that is rendered. * @param {boolean} [style.wordWrap=false] - Indicates if word wrap should be used * @param {number} [style.wordWrapWidth=100] - The width at which text will wrap, it needs wordWrap to be set to true @@ -152,6 +159,19 @@ } } + get dropShadowAlpha() + { + return this._dropShadowAlpha; + } + set dropShadowAlpha(dropShadowAlpha) + { + if (this._dropShadowAlpha !== dropShadowAlpha) + { + this._dropShadowAlpha = dropShadowAlpha; + this.styleID++; + } + } + get dropShadowAngle() { return this._dropShadowAngle; @@ -232,6 +252,19 @@ } } + get fillGradientStops() + { + return this._fillGradientStops; + } + set fillGradientStops(fillGradientStops) + { + if (!areArraysEqual(this._fillGradientStops,fillGradientStops)) + { + this._fillGradientStops = fillGradientStops; + this.styleID++; + } + } + get fontFamily() { return this._fontFamily; @@ -402,6 +435,19 @@ } } + get trim() + { + return this._trim; + } + set trim(trim) + { + if (this._trim !== trim) + { + this._trim = trim; + this.styleID++; + } + } + get wordWrap() { return this._wordWrap; @@ -475,3 +521,34 @@ return color; } } + +/** + * Utility function to convert hexadecimal colors to strings, and simply return the color if it's a string. + * This version can also convert array of colors + * + * @param {Array} array1 First array to compare + * @param {Array} array2 Second array to compare + * @return {boolean} Do the arrays contain the same values in the same order + */ +function areArraysEqual(array1, array2) +{ + if (!Array.isArray(array1) || !Array.isArray(array2)) + { + return false; + } + + if (array1.length !== array2.length) + { + return false; + } + + for (let i = 0; i < array1.length; ++i) + { + if (array1[i] !== array2[i]) + { + return false; + } + } + + return true; +} diff --git a/src/core/textures/BaseRenderTexture.js b/src/core/textures/BaseRenderTexture.js index 5d50146..aa727f0 100644 --- a/src/core/textures/BaseRenderTexture.js +++ b/src/core/textures/BaseRenderTexture.js @@ -1,8 +1,6 @@ import BaseTexture from './BaseTexture'; import settings from '../settings'; -const { RESOLUTION, SCALE_MODE } = settings; - /** * A BaseRenderTexture is a special texture that allows any Pixi display object to be rendered to it. * @@ -54,7 +52,7 @@ { super(null, scaleMode); - this.resolution = resolution || RESOLUTION; + this.resolution = resolution || settings.RESOLUTION; this.width = width; this.height = height; @@ -62,7 +60,7 @@ this.realWidth = this.width * this.resolution; this.realHeight = this.height * this.resolution; - this.scaleMode = scaleMode || SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; this.hasLoaded = true; /** diff --git a/src/core/textures/BaseTexture.js b/src/core/textures/BaseTexture.js index 676dca3..aeccf39 100644 --- a/src/core/textures/BaseTexture.js +++ b/src/core/textures/BaseTexture.js @@ -77,7 +77,7 @@ * @default PIXI.settings.SCALE_MODE * @see PIXI.SCALE_MODES */ - this.scaleMode = scaleMode || settings.SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; /** * Set to true once the base texture has successfully loaded. @@ -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/RenderTexture.js b/src/core/textures/RenderTexture.js index 4c5034f..b3c6297 100644 --- a/src/core/textures/RenderTexture.js +++ b/src/core/textures/RenderTexture.js @@ -54,8 +54,8 @@ /* eslint-disable prefer-rest-params, no-console */ const width = arguments[1]; const height = arguments[2]; - const scaleMode = arguments[3] || 0; - const resolution = arguments[4] || 1; + const scaleMode = arguments[3]; + const resolution = arguments[4]; // we have an old render texture.. console.warn(`Please use RenderTexture.create(${width}, ${height}) instead of the ctor directly.`); 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 ca31456..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 @@ -459,7 +497,9 @@ if (frame.x + frame.width > this.baseTexture.width || frame.y + frame.height > this.baseTexture.height) { - throw new Error(`Texture Error: frame does not fit inside the base Texture dimensions ${this}`); + throw new Error('Texture Error: frame does not fit inside the base Texture dimensions: ' + + `X: ${frame.x} + ${frame.width} > ${this.baseTexture.width} ` + + `Y: ${frame.y} + ${frame.height} > ${this.baseTexture.height}`); } // this.valid = frame && frame.width && frame.height && this.baseTexture.source && this.baseTexture.hasLoaded; @@ -520,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. @@ -528,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 2a7497b..99e585a 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)); } /** @@ -360,3 +362,43 @@ * @private */ export const BaseTextureCache = {}; + +/** + * 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/core/utils/trimCanvas.js b/src/core/utils/trimCanvas.js new file mode 100644 index 0000000..5af6de0 --- /dev/null +++ b/src/core/utils/trimCanvas.js @@ -0,0 +1,83 @@ +/** + * Trim transparent borders from a canvas + * + * @memberof PIXI + * @function trimCanvas + * @private + * @param {HTMLCanvasElement} canvas - the canvas to trim + * @returns {object} Trim data + */ +export default function trimCanvas(canvas) +{ + // https://gist.github.com/remy/784508 + + let width = canvas.width; + let height = canvas.height; + + const context = canvas.getContext('2d'); + const imageData = context.getImageData(0, 0, width, height); + const pixels = imageData.data; + const len = pixels.length; + + const bound = { + top: null, + left: null, + right: null, + bottom: null, + }; + let i; + let x; + let y; + + for (i = 0; i < len; i += 4) + { + if (pixels[i + 3] !== 0) + { + x = (i / 4) % width; + y = ~~((i / 4) / width); + + if (bound.top === null) + { + bound.top = y; + } + + if (bound.left === null) + { + bound.left = x; + } + else if (x < bound.left) + { + bound.left = x; + } + + if (bound.right === null) + { + bound.right = x + 1; + } + else if (bound.right < x) + { + bound.right = x + 1; + } + + if (bound.bottom === null) + { + bound.bottom = y; + } + else if (bound.bottom < y) + { + bound.bottom = y; + } + } + } + + width = bound.right - bound.left; + height = bound.bottom - bound.top + 1; + + const data = context.getImageData(bound.left, bound.top, width, height); + + return { + height, + width, + data, + }; +} diff --git a/src/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/extras/TilingSprite.js b/src/extras/TilingSprite.js index f90d248..fe82451 100644 --- a/src/extras/TilingSprite.js +++ b/src/extras/TilingSprite.js @@ -220,20 +220,33 @@ transform.tx * resolution, transform.ty * resolution); - // TODO - this should be rolled into the setTransform above.. - context.scale(this.tileScale.x / baseTextureResolution, this.tileScale.y / baseTextureResolution); - - context.translate(modX + (this.anchor.x * -this._width), - modY + (this.anchor.y * -this._height)); - renderer.setBlendMode(this.blendMode); // fill the pattern! context.fillStyle = this._canvasPattern; - context.fillRect(-modX, - -modY, - this._width / this.tileScale.x * baseTextureResolution, - this._height / this.tileScale.y * baseTextureResolution); + + // TODO - this should be rolled into the setTransform above.. + context.scale(this.tileScale.x / baseTextureResolution, this.tileScale.y / baseTextureResolution); + + const anchorX = this.anchor.x * -this._width; + const anchorY = this.anchor.y * -this._height; + + if (this.uvRespectAnchor) + { + context.translate(modX, modY); + + context.fillRect(-modX + anchorX, -modY + anchorY, + this._width / this.tileScale.x * baseTextureResolution, + this._height / this.tileScale.y * baseTextureResolution); + } + else + { + context.translate(modX + anchorX, modY + anchorY); + + context.fillRect(-modX, -modY, + this._width / this.tileScale.x * baseTextureResolution, + this._height / this.tileScale.y * baseTextureResolution); + } } /** diff --git a/src/extras/cacheAsBitmap.js b/src/extras/cacheAsBitmap.js index 18043a7..5dd5640 100644 --- a/src/extras/cacheAsBitmap.js +++ b/src/extras/cacheAsBitmap.js @@ -41,6 +41,9 @@ * provide a performance benefit for complex static displayObjects. * To remove simply set this property to 'false' * + * IMPORTANT GOTCHA - make sure that all your textures are preloaded BEFORE setting this property to true + * as it will take a snapshot of what is currently there. If the textures have not loaded then they will not appear. + * * @member {boolean} * @memberof PIXI.DisplayObject# */ diff --git a/src/filters/blur/BlurFilter.js b/src/filters/blur/BlurFilter.js index 340fc58..1575db9 100644 --- a/src/filters/blur/BlurFilter.js +++ b/src/filters/blur/BlurFilter.js @@ -24,10 +24,9 @@ this.blurXFilter = new BlurXFilter(strength, quality, resolution, kernelSize); this.blurYFilter = new BlurYFilter(strength, quality, resolution, kernelSize); - this.resolution = 1; this.padding = 0; - this.resolution = resolution || 1; + this.resolution = resolution || core.settings.RESOLUTION; this.quality = quality || 4; this.blur = strength || 8; } diff --git a/src/filters/blur/BlurXFilter.js b/src/filters/blur/BlurXFilter.js index def99c3..0f48889 100644 --- a/src/filters/blur/BlurXFilter.js +++ b/src/filters/blur/BlurXFilter.js @@ -31,7 +31,7 @@ fragSrc ); - this.resolution = resolution || 1; + this.resolution = resolution || core.settings.RESOLUTION; this._quality = 0; diff --git a/src/filters/blur/BlurYFilter.js b/src/filters/blur/BlurYFilter.js index 638e0f0..6d489c7 100644 --- a/src/filters/blur/BlurYFilter.js +++ b/src/filters/blur/BlurYFilter.js @@ -31,7 +31,7 @@ fragSrc ); - this.resolution = resolution || 1; + this.resolution = resolution || core.settings.RESOLUTION; this._quality = 0; diff --git a/src/filters/colormatrix/ColorMatrixFilter.js b/src/filters/colormatrix/ColorMatrixFilter.js index 8509505..679f8ec 100644 --- a/src/filters/colormatrix/ColorMatrixFilter.js +++ b/src/filters/colormatrix/ColorMatrixFilter.js @@ -75,28 +75,28 @@ out[1] = (a[0] * b[1]) + (a[1] * b[6]) + (a[2] * b[11]) + (a[3] * b[16]); out[2] = (a[0] * b[2]) + (a[1] * b[7]) + (a[2] * b[12]) + (a[3] * b[17]); out[3] = (a[0] * b[3]) + (a[1] * b[8]) + (a[2] * b[13]) + (a[3] * b[18]); - out[4] = (a[0] * b[4]) + (a[1] * b[9]) + (a[2] * b[14]) + (a[3] * b[19]); + out[4] = (a[0] * b[4]) + (a[1] * b[9]) + (a[2] * b[14]) + (a[3] * b[19]) + a[4]; // Green Channel out[5] = (a[5] * b[0]) + (a[6] * b[5]) + (a[7] * b[10]) + (a[8] * b[15]); out[6] = (a[5] * b[1]) + (a[6] * b[6]) + (a[7] * b[11]) + (a[8] * b[16]); out[7] = (a[5] * b[2]) + (a[6] * b[7]) + (a[7] * b[12]) + (a[8] * b[17]); out[8] = (a[5] * b[3]) + (a[6] * b[8]) + (a[7] * b[13]) + (a[8] * b[18]); - out[9] = (a[5] * b[4]) + (a[6] * b[9]) + (a[7] * b[14]) + (a[8] * b[19]); + out[9] = (a[5] * b[4]) + (a[6] * b[9]) + (a[7] * b[14]) + (a[8] * b[19]) + a[9]; // Blue Channel out[10] = (a[10] * b[0]) + (a[11] * b[5]) + (a[12] * b[10]) + (a[13] * b[15]); out[11] = (a[10] * b[1]) + (a[11] * b[6]) + (a[12] * b[11]) + (a[13] * b[16]); out[12] = (a[10] * b[2]) + (a[11] * b[7]) + (a[12] * b[12]) + (a[13] * b[17]); out[13] = (a[10] * b[3]) + (a[11] * b[8]) + (a[12] * b[13]) + (a[13] * b[18]); - out[14] = (a[10] * b[4]) + (a[11] * b[9]) + (a[12] * b[14]) + (a[13] * b[19]); + out[14] = (a[10] * b[4]) + (a[11] * b[9]) + (a[12] * b[14]) + (a[13] * b[19]) + a[14]; // Alpha Channel out[15] = (a[15] * b[0]) + (a[16] * b[5]) + (a[17] * b[10]) + (a[18] * b[15]); out[16] = (a[15] * b[1]) + (a[16] * b[6]) + (a[17] * b[11]) + (a[18] * b[16]); out[17] = (a[15] * b[2]) + (a[16] * b[7]) + (a[17] * b[12]) + (a[18] * b[17]); out[18] = (a[15] * b[3]) + (a[16] * b[8]) + (a[17] * b[13]) + (a[18] * b[18]); - out[19] = (a[15] * b[4]) + (a[16] * b[9]) + (a[17] * b[14]) + (a[18] * b[19]); + out[19] = (a[15] * b[4]) + (a[16] * b[9]) + (a[17] * b[14]) + (a[18] * b[19]) + a[19]; return out; } diff --git a/src/filters/colormatrix/colorMatrix.frag b/src/filters/colormatrix/colorMatrix.frag index 3aaf2b3..c73c0e9 100644 --- a/src/filters/colormatrix/colorMatrix.frag +++ b/src/filters/colormatrix/colorMatrix.frag @@ -4,32 +4,38 @@ void main(void) { - vec4 c = texture2D(uSampler, vTextureCoord); + // Un-premultiply alpha before applying the color matrix. See issue #3539. + if (c.a > 0.0) { + c.rgb /= c.a; + } + vec4 result; + result.r = (m[0] * c.r); + result.r += (m[1] * c.g); + result.r += (m[2] * c.b); + result.r += (m[3] * c.a); + result.r += m[4]; - gl_FragColor.r = (m[0] * c.r); - gl_FragColor.r += (m[1] * c.g); - gl_FragColor.r += (m[2] * c.b); - gl_FragColor.r += (m[3] * c.a); - gl_FragColor.r += m[4] * c.a; + result.g = (m[5] * c.r); + result.g += (m[6] * c.g); + result.g += (m[7] * c.b); + result.g += (m[8] * c.a); + result.g += m[9]; - gl_FragColor.g = (m[5] * c.r); - gl_FragColor.g += (m[6] * c.g); - gl_FragColor.g += (m[7] * c.b); - gl_FragColor.g += (m[8] * c.a); - gl_FragColor.g += m[9] * c.a; + result.b = (m[10] * c.r); + result.b += (m[11] * c.g); + result.b += (m[12] * c.b); + result.b += (m[13] * c.a); + result.b += m[14]; - gl_FragColor.b = (m[10] * c.r); - gl_FragColor.b += (m[11] * c.g); - gl_FragColor.b += (m[12] * c.b); - gl_FragColor.b += (m[13] * c.a); - gl_FragColor.b += m[14] * c.a; + result.a = (m[15] * c.r); + result.a += (m[16] * c.g); + result.a += (m[17] * c.b); + result.a += (m[18] * c.a); + result.a += m[19]; - gl_FragColor.a = (m[15] * c.r); - gl_FragColor.a += (m[16] * c.g); - gl_FragColor.a += (m[17] * c.b); - gl_FragColor.a += (m[18] * c.a); - gl_FragColor.a += m[19] * c.a; + // Premultiply alpha again. + result.rgb *= result.a; -// gl_FragColor = vec4(m[0]); + gl_FragColor = result; } diff --git a/src/filters/displacement/DisplacementFilter.js b/src/filters/displacement/DisplacementFilter.js index 112de0b..d5355c3 100644 --- a/src/filters/displacement/DisplacementFilter.js +++ b/src/filters/displacement/DisplacementFilter.js @@ -36,7 +36,7 @@ this.maskMatrix = maskMatrix; this.uniforms.mapSampler = sprite.texture; - this.uniforms.filterMatrix = maskMatrix.toArray(true); + this.uniforms.filterMatrix = maskMatrix; this.uniforms.scale = { x: 1, y: 1 }; if (scale === null || scale === undefined) diff --git a/src/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/InteractionData.js b/src/interaction/InteractionData.js index 02d53a3..818da35 100644 --- a/src/interaction/InteractionData.js +++ b/src/interaction/InteractionData.js @@ -21,9 +21,9 @@ this.global = new core.Point(); /** - * The target Sprite that was interacted with + * The target DisplayObject that was interacted with * - * @member {PIXI.Sprite} + * @member {PIXI.DisplayObject} */ this.target = null; @@ -33,6 +33,13 @@ * @member {Event} */ this.originalEvent = null; + + /** + * Unique identifier for this interaction + * + * @member {number} + */ + this.identifier = null; } /** diff --git a/src/interaction/InteractionEvent.js b/src/interaction/InteractionEvent.js index 75b2966..dc554f9 100644 --- a/src/interaction/InteractionEvent.js +++ b/src/interaction/InteractionEvent.js @@ -58,7 +58,7 @@ } /** - * Prevents event from reaching any objects other than the current object. + * Resets the event. * * @private */ diff --git a/src/interaction/InteractionManager.js b/src/interaction/InteractionManager.js index 7d51134..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/src/prepare/BasePrepare.js b/src/prepare/BasePrepare.js index a2005f5..09eb5e5 100644 --- a/src/prepare/BasePrepare.js +++ b/src/prepare/BasePrepare.js @@ -20,7 +20,7 @@ * * @abstract * @class - * @memberof PIXI + * @memberof PIXI.prepare */ export default class BasePrepare { @@ -107,9 +107,9 @@ /** * Upload all the textures and graphics to the GPU. * - * @param {Function|PIXI.DisplayObject|PIXI.Container} item - Either - * the container or display object to search for items to upload or - * the callback function, if items have been added using `prepare.add`. + * @param {Function|PIXI.DisplayObject|PIXI.Container|PIXI.BaseTexture|PIXI.Texture|PIXI.Graphics|PIXI.Text} item - + * Either the container or display object to search for items to upload, the items to upload themselves, + * or the callback function, if items have been added using `prepare.add`. * @param {Function} [done] - Optional callback when all queued uploads have completed */ upload(item, done) @@ -236,7 +236,8 @@ /** * Manually add an item to the uploading queue. * - * @param {PIXI.DisplayObject|PIXI.Container|*} item - Object to add to the queue + * @param {PIXI.DisplayObject|PIXI.Container|PIXI.BaseTexture|PIXI.Texture|PIXI.Graphics|PIXI.Text|*} item - Object to + * add to the queue * @return {PIXI.CanvasPrepare} Instance of plugin for chaining. */ add(item) diff --git a/src/prepare/canvas/CanvasPrepare.js b/src/prepare/canvas/CanvasPrepare.js index 4b99d22..0224bb5 100644 --- a/src/prepare/canvas/CanvasPrepare.js +++ b/src/prepare/canvas/CanvasPrepare.js @@ -12,7 +12,8 @@ * An instance of this class is automatically created by default, and can be found at renderer.plugins.prepare * * @class - * @memberof PIXI + * @extends PIXI.prepare.BasePrepare + * @memberof PIXI.prepare */ export default class CanvasPrepare extends BasePrepare { diff --git a/src/prepare/webgl/WebGLPrepare.js b/src/prepare/webgl/WebGLPrepare.js index 1ef7467..eb7023f 100644 --- a/src/prepare/webgl/WebGLPrepare.js +++ b/src/prepare/webgl/WebGLPrepare.js @@ -7,7 +7,8 @@ * An instance of this class is automatically created by default, and can be found at renderer.plugins.prepare * * @class - * @memberof PIXI + * @extends PIXI.prepare.BasePrepare + * @memberof PIXI.prepare */ export default class WebGLPrepare extends BasePrepare { diff --git a/test/core/Circle.js b/test/core/Circle.js index 183f98f..f621992 100644 --- a/test/core/Circle.js +++ b/test/core/Circle.js @@ -53,6 +53,10 @@ expect(circ1.contains(10, 16)).to.be.false; expect(circ1.contains(11, 15)).to.be.false; expect(circ1.contains(0, 0)).to.be.false; + + const circ2 = new PIXI.Circle(10, 10, 0); + + expect(circ2.contains(10, 10)).to.be.false; }); it('should return framing rectangle', function () diff --git a/test/core/Container.js b/test/core/Container.js index 8a10b40..cefde54 100644 --- a/test/core/Container.js +++ b/test/core/Container.js @@ -1,5 +1,39 @@ 'use strict'; +function testAddChild(fn) +{ + return function () + { + fn(function (container, obj) + { + container.addChild(obj); + }); + fn(function (container, obj) + { + container.addChildAt(obj); + }); + }; +} + +function testRemoveChild(fn) +{ + return function () + { + 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 +104,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 +279,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/Ellipse.js b/test/core/Ellipse.js index f2e8234..3f47442 100644 --- a/test/core/Ellipse.js +++ b/test/core/Ellipse.js @@ -56,6 +56,10 @@ expect(ellipse1.contains(10, 16)).to.be.false; expect(ellipse1.contains(11, 15)).to.be.false; expect(ellipse1.contains(0, 0)).to.be.false; + + const ellipse2 = new PIXI.Ellipse(10, 10, 0, 0); + + expect(ellipse2.contains(10, 10)).to.be.false; }); it('should return framing rectangle', function () diff --git a/test/core/Graphics.js b/test/core/Graphics.js index 2a64946..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 () @@ -127,6 +130,16 @@ expect(graphics.containsPoint(point)).to.be.false; }); + + it('should return false when no fill', function () + { + const point = new PIXI.Point(1, 1); + const graphics = new PIXI.Graphics(); + + graphics.drawRect(0, 0, 10, 10); + + expect(graphics.containsPoint(point)).to.be.false; + }); }); describe('arc', function () @@ -208,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/Matrix.js b/test/core/Matrix.js index 9d2694b..76bd57c 100644 --- a/test/core/Matrix.js +++ b/test/core/Matrix.js @@ -107,10 +107,60 @@ expect(m1.ty).to.equal(m2.ty); }); + it('should prepend matrix', function () + { + const m1 = new PIXI.Matrix(); + const m2 = new PIXI.Matrix(); + + m2.set(2, 3, 4, 5, 100, 200); + m1.prepend(m2); + + expect(m1.a).to.equal(m2.a); + expect(m1.b).to.equal(m2.b); + expect(m1.c).to.equal(m2.c); + expect(m1.d).to.equal(m2.d); + expect(m1.tx).to.equal(m2.tx); + expect(m1.ty).to.equal(m2.ty); + + const m3 = new PIXI.Matrix(); + const m4 = new PIXI.Matrix(); + + m3.prepend(m4); + + expect(m3.a).to.equal(m4.a); + expect(m3.b).to.equal(m4.b); + expect(m3.c).to.equal(m4.c); + expect(m3.d).to.equal(m4.d); + expect(m3.tx).to.equal(m4.tx); + expect(m3.ty).to.equal(m4.ty); + }); + it('should get IDENTITY and TEMP_MATRIX', function () { expect(PIXI.Matrix.IDENTITY instanceof PIXI.Matrix).to.be.true; expect(PIXI.Matrix.TEMP_MATRIX instanceof PIXI.Matrix).to.be.true; }); -}); + it('should reset matrix to default when identity() is called', function () + { + const matrix = new PIXI.Matrix(); + + matrix.set(2, 3, 4, 5, 100, 200); + + expect(matrix.a).to.equal(2); + expect(matrix.b).to.equal(3); + expect(matrix.c).to.equal(4); + expect(matrix.d).to.equal(5); + expect(matrix.tx).to.equal(100); + expect(matrix.ty).to.equal(200); + + matrix.identity(); + + expect(matrix.a).to.equal(1); + expect(matrix.b).to.equal(0); + expect(matrix.c).to.equal(0); + expect(matrix.d).to.equal(1); + expect(matrix.tx).to.equal(0); + expect(matrix.ty).to.equal(0); + }); +}); 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/Text.js b/test/core/Text.js index b8ce561..5799ad4 100644 --- a/test/core/Text.js +++ b/test/core/Text.js @@ -112,4 +112,42 @@ expect(text.width).to.equal(300); }); }); + + describe('text', function () + { + it('should convert numbers into strings', function () + { + const text = new PIXI.Text(2); + + expect(text.text).to.equal('2'); + }); + + it('should not change 0 to \'\'', function () + { + const text = new PIXI.Text(0); + + expect(text.text).to.equal('0'); + }); + + it('should prevent setting null', function () + { + const text = new PIXI.Text(null); + + expect(text.text).to.equal(' '); + }); + + it('should prevent setting undefined', function () + { + const text = new PIXI.Text(); + + expect(text.text).to.equal(' '); + }); + + it('should prevent setting \'\'', function () + { + const text = new PIXI.Text(''); + + expect(text.text).to.equal(' '); + }); + }); }); diff --git a/test/core/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/TilingSprite.js b/test/core/TilingSprite.js index 2e449a7..7694792 100644 --- a/test/core/TilingSprite.js +++ b/test/core/TilingSprite.js @@ -24,4 +24,25 @@ expect(bounds.height).to.equal(600); }); }); + + it('checks if tilingSprite contains a point', function () + { + const texture = new PIXI.Texture(new PIXI.BaseTexture()); + const tilingSprite = new PIXI.extras.TilingSprite(texture, 200, 300); + + expect(tilingSprite.containsPoint(new PIXI.Point(1, 1))).to.equal(true); + expect(tilingSprite.containsPoint(new PIXI.Point(300, 400))).to.equal(false); + }); + + it('gets and sets height and width correctly', function () + { + const texture = new PIXI.Texture(new PIXI.BaseTexture()); + const tilingSprite = new PIXI.extras.TilingSprite(texture, 200, 300); + + tilingSprite.width = 400; + tilingSprite.height = 600; + + expect(tilingSprite.width).to.equal(400); + expect(tilingSprite.height).to.equal(600); + }); }); diff --git a/test/core/WebGLRenderer.js b/test/core/WebGLRenderer.js new file mode 100644 index 0000000..3d6cf98 --- /dev/null +++ b/test/core/WebGLRenderer.js @@ -0,0 +1,21 @@ +'use strict'; + +const withGL = require('../withGL'); + +describe('PIXI.WebGLRenderer', function () +{ + it('setting option legacy should disable VAOs and SPRITE_MAX_TEXTURES', withGL(function () + { + const renderer = new PIXI.WebGLRenderer(1, 1, { legacy: true }); + + 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/getLocalBounds.js b/test/core/getLocalBounds.js index d160a56..309da0e 100644 --- a/test/core/getLocalBounds.js +++ b/test/core/getLocalBounds.js @@ -196,4 +196,13 @@ expect(bounds.width).to.equal(100); expect(bounds.height).to.equal(100); }); + + it('should register correct local-bounds with a Text', function () + { + const text = new PIXI.Text('hello'); + const bounds = text.getLocalBounds(); + + expect(bounds.width).to.not.equal(0); + expect(bounds.height).to.not.equal(0); + }); }); diff --git a/test/core/index.js b/test/core/index.js index 2a056a2..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'); @@ -23,4 +24,7 @@ require('./Circle'); require('./Graphics'); 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 1271fef..71ade15 100644 --- a/package.json +++ b/package.json @@ -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/scripts/release.js b/scripts/release.js index c4c8e36..156620c 100644 --- a/scripts/release.js +++ b/scripts/release.js @@ -12,7 +12,8 @@ 'dist/**/*', 'lib/**/*', 'src/**/*', - 'scripts/**/*', + 'scripts/*', + 'scripts/renders/*', 'test/**/*', '*.json', '*.md', diff --git a/scripts/renders/client.js b/scripts/renders/client.js index 16c9565..6faee34 100755 --- a/scripts/renders/client.js +++ b/scripts/renders/client.js @@ -1,6 +1,6 @@ 'use strict'; -require('../../bin/pixi'); +require('../../dist/pixi'); PIXI.utils.skipHello(); const fs = require('fs'); 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 bc730db..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. @@ -33,8 +33,11 @@ * need to call toDataUrl on the webgl context * @param {number} [options.resolution=1] - The resolution / device pixel ratio of the renderer, retina would be 2 * @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 @@ -43,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. */ @@ -73,7 +99,7 @@ */ stop() { - this.ticker.stop(); + this._ticker.stop(); } /** @@ -81,7 +107,7 @@ */ start() { - this.ticker.start(); + this._ticker.start(); } /** @@ -95,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 index 81e076b..78b17e6 100644 --- a/src/core/Shader.js +++ b/src/core/Shader.js @@ -1,9 +1,7 @@ import { GLShader } from 'pixi-gl-core'; import settings from './settings'; -const { PRECISION } = settings; - -function checkPrecision(src) +function checkPrecision(src, def) { if (src instanceof Array) { @@ -11,14 +9,14 @@ { const copy = src.slice(0); - copy.unshift(`precision ${PRECISION} float;`); + copy.unshift(`precision ${def} float;`); return copy; } } else if (src.substring(0, 9) !== 'precision') { - return `precision ${PRECISION} float;\n${src}`; + return `precision ${def} float;\n${src}`; } return src; @@ -42,6 +40,7 @@ */ constructor(gl, vertexSrc, fragmentSrc) { - super(gl, checkPrecision(vertexSrc), checkPrecision(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 3425b41..f9235e4 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -181,7 +181,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 b3792ef..17fde8d 100644 --- a/src/core/display/Container.js +++ b/src/core/display/Container.js @@ -73,13 +73,14 @@ } child.parent = this; - - // ensure a transform will be recalculated.. - this.transform._parentID = -1; - this._boundsID++; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.push(child); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(this.children.length - 1); child.emit('added', this); @@ -108,9 +109,14 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.splice(index, 0, child); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(index); child.emit('added', this); @@ -174,6 +180,7 @@ removeItems(this.children, currentIndex, 1); // remove from old position this.children.splice(index, 0, child); // add at new position + this.onChildrenChange(index); } @@ -220,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! @@ -244,9 +252,14 @@ { const child = this.getChildAt(index); + // ensure child transform will be recalculated.. child.parent = null; + child.transform._parentID = -1; removeItems(this.children, index, 1); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(index); child.emit('removed', this); @@ -275,8 +288,14 @@ for (let i = 0; i < removed.length; ++i) { removed[i].parent = null; + if (removed[i].transform) + { + removed[i].transform._parentID = -1; + } } + this._boundsID++; + this.onChildrenChange(beginIndex); for (let i = 0; i < removed.length; ++i) diff --git a/src/core/display/DisplayObject.js b/src/core/display/DisplayObject.js index 3affe36..4e6c77d 100644 --- a/src/core/display/DisplayObject.js +++ b/src/core/display/DisplayObject.js @@ -109,7 +109,7 @@ /** * The original, cached mask of the object * - * @member {PIXI.Rectangle} + * @member {PIXI.Graphics|PIXI.Sprite} * @private */ this._mask = null; diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index 95f2af4..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 ); @@ -1065,10 +1061,15 @@ canvasRenderer = new CanvasRenderer(); } - tempMatrix.tx = -bounds.x; - tempMatrix.ty = -bounds.y; + this.transform.updateLocalTransform(); + this.transform.localTransform.copy(tempMatrix); - canvasRenderer.render(this, canvasBuffer, false, tempMatrix); + tempMatrix.invert(); + + tempMatrix.tx -= bounds.x; + tempMatrix.ty -= bounds.y; + + canvasRenderer.render(this, canvasBuffer, true, tempMatrix); const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode); 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 e67680f..867dde2 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/buildCircle.js b/src/core/graphics/webgl/utils/buildCircle.js index 0400d13..abf5d02 100644 --- a/src/core/graphics/webgl/utils/buildCircle.js +++ b/src/core/graphics/webgl/utils/buildCircle.js @@ -33,6 +33,11 @@ height = circleData.height; } + if (width === 0 || height === 0) + { + return; + } + const totalSegs = Math.floor(30 * Math.sqrt(circleData.radius)) || Math.floor(15 * Math.sqrt(circleData.width + circleData.height)); diff --git a/src/core/graphics/webgl/utils/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 90155f9..f03fe15 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -30,6 +30,7 @@ export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; export { default as CanvasGraphicsRenderer } from './graphics/canvas/CanvasGraphicsRenderer'; +export { default as Spritesheet } from './textures/Spritesheet'; export { default as Texture } from './textures/Texture'; export { default as BaseTexture } from './textures/BaseTexture'; export { default as RenderTexture } from './textures/RenderTexture'; diff --git a/src/core/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 ccce680..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); } /** @@ -162,6 +162,9 @@ if (transform) { transform.copy(tempWt); + + // lets not forget to flag the parent transform as dirty... + this._tempDisplayObjectParent.transform._worldID = -1; } else { @@ -169,6 +172,7 @@ } displayObject.parent = this._tempDisplayObjectParent; + displayObject.updateTransform(); displayObject.parent = cacheParent; // displayObject.hitArea = //TODO add a temp hit area @@ -279,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/canvas/utils/CanvasRenderTarget.js b/src/core/renderers/canvas/utils/CanvasRenderTarget.js index 3de0aef..b409891 100644 --- a/src/core/renderers/canvas/utils/CanvasRenderTarget.js +++ b/src/core/renderers/canvas/utils/CanvasRenderTarget.js @@ -1,5 +1,4 @@ import settings from '../../../settings'; -const { RESOLUTION } = settings; /** * Creates a Canvas element of the given size. @@ -30,7 +29,7 @@ */ this.context = this.canvas.getContext('2d'); - this.resolution = resolution || RESOLUTION; + this.resolution = resolution || settings.RESOLUTION; this.resize(width, height); } 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 ce0e1c4..788ed6c 100644 --- a/src/core/renderers/webgl/WebGLRenderer.js +++ b/src/core/renderers/webgl/WebGLRenderer.js @@ -30,8 +30,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 @@ -49,10 +49,19 @@ * enable this if you need to call toDataUrl on the webgl context. * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when * rendering, stopping pixel interpolation. + * @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; + + if (this.legacy) + { + glCore.VertexArrayObject.FORCE_NATIVE = true; + } /** * The type of this renderer as a standardised const @@ -229,7 +238,7 @@ this.emit('context', gl); // setup the width/height properties and gl viewport - this.resize(this.width, this.height); + this.resize(this.screen.width, this.screen.height); } /** @@ -323,16 +332,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) { @@ -376,6 +385,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 @@ -440,9 +469,10 @@ * Changes the current shader to the one given in parameter * * @param {PIXI.Shader} shader - the new shader + * @param {boolean} [autoProject=true] - Whether automatically set the projection matrix * @return {PIXI.WebGLRenderer} Returns itself. */ - bindShader(shader) + bindShader(shader, autoProject) { // TODO cache if (this._activeShader !== shader) @@ -450,8 +480,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 bbab3b3..944f41d 100644 --- a/src/core/renderers/webgl/filters/Filter.js +++ b/src/core/renderers/webgl/filters/Filter.js @@ -1,6 +1,7 @@ import extractUniformsFromSrc from './extractUniformsFromSrc'; import { uid } from '../../../utils'; import { BLEND_MODES } from '../../../const'; +import settings from '../../../settings'; const SOURCE_KEY_MAP = {}; @@ -10,7 +11,7 @@ * @memberof PIXI * @extends PIXI.Shader */ -class Filter +export default class Filter { /** * @param {string} [vertexSrc] - The source of the vertex shader. @@ -35,8 +36,6 @@ this.blendMode = BLEND_MODES.NORMAL; - // pull out the vertex and shader uniforms if they are not specified.. - // currently this does not extract structs only default types this.uniformData = uniforms || extractUniformsFromSrc(this.vertexSrc, this.fragmentSrc, 'projectionMatrix|uSampler'); /** @@ -80,7 +79,7 @@ * * @member {number} */ - this.resolution = 1; + this.resolution = settings.RESOLUTION; /** * If enabled is true the filter is applied, if false it will not. @@ -97,8 +96,11 @@ * @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 { // --- // // this.uniforms.filterMatrix = filterManager.calculateSpriteMatrix(tempMatrix, window.panda ); @@ -170,5 +172,3 @@ ].join('\n'); } } - -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 dc7072a..6c7422d 100644 --- a/src/core/renderers/webgl/managers/FilterManager.js +++ b/src/core/renderers/webgl/managers/FilterManager.js @@ -154,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 @@ -176,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; @@ -184,7 +184,7 @@ flop = t; } - filters[i].apply(this, flip, lastState.renderTarget, true); + filters[i].apply(this, flip, lastState.renderTarget, false, currentState); this.freePotRenderTarget(flip); this.freePotRenderTarget(flop); @@ -291,9 +291,13 @@ let textureCount = 1; let currentState; - if (shader.uniforms.data.filterArea) + // 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; @@ -306,9 +310,9 @@ // use this to clamp displaced texture coords so they belong to filterArea // see displacementFilter fragment shader for an example - if (shader.uniforms.data.filterClamp) + if (shader.uniforms.filterClamp) { - currentState = this.filterData.stack[this.filterData.index]; + currentState = currentState || this.filterData.stack[this.filterData.index]; const filterClamp = shader.uniforms.filterClamp; @@ -485,7 +489,7 @@ */ destroy() { - this.shaderCache = []; + this.shaderCache = {}; this.emptyPool(); } diff --git a/src/core/renderers/webgl/utils/RenderTarget.js b/src/core/renderers/webgl/utils/RenderTarget.js index afe51ab..6d2467f 100644 --- a/src/core/renderers/webgl/utils/RenderTarget.js +++ b/src/core/renderers/webgl/utils/RenderTarget.js @@ -124,7 +124,7 @@ * @default PIXI.settings.SCALE_MODE * @see PIXI.SCALE_MODES */ - this.scaleMode = scaleMode || settings.SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; /** * Whether this object is the root element or not 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 36ea5bb..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; @@ -346,8 +346,8 @@ /** * Gets the local bounds of the sprite object. * - * @param {Rectangle} rect - The output rectangle. - * @return {Rectangle} The bounds. + * @param {PIXI.Rectangle} rect - The output rectangle. + * @return {PIXI.Rectangle} The bounds. */ getLocalBounds(rect) { @@ -440,7 +440,7 @@ * * @static * @param {number|string|PIXI.BaseTexture|HTMLCanvasElement|HTMLVideoElement} source Source to create texture from - * @return {PIXI.Texture} 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 e77b39a..90b08c0 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++) @@ -103,11 +102,18 @@ { const gl = this.renderer.gl; - // step 1: first check max textures the GPU can handle. - this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); + if (this.renderer.legacy) + { + this.MAX_TEXTURES = 1; + } + else + { + // step 1: first check max textures the GPU can handle. + this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); - // step 2: check the maximum number of if statements the shader can have too.. - this.MAX_TEXTURES = checkMaxIfStatmentsInShader(this.MAX_TEXTURES, gl); + // step 2: check the maximum number of if statements the shader can have too.. + this.MAX_TEXTURES = checkMaxIfStatmentsInShader(this.MAX_TEXTURES, gl); + } const shader = this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); @@ -130,8 +136,12 @@ .addIndex(this.indexBuffer) .addAttribute(this.vertexBuffers[i], shader.attributes.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) .addAttribute(this.vertexBuffers[i], shader.attributes.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) - .addAttribute(this.vertexBuffers[i], shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4) - .addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + .addAttribute(this.vertexBuffers[i], shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); + + if (shader.attributes.aTextureId) + { + this.vaos[i].addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + } /* eslint-enable max-len */ } @@ -378,8 +388,12 @@ .addIndex(this.indexBuffer) .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) - .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4) - .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); + + if (this.shader.attributes.aTextureId) + { + this.vaos[this.vertexCount].addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + } /* eslint-enable max-len */ } diff --git a/src/core/text/Text.js b/src/core/text/Text.js index 013625a..cec8c32 100644 --- a/src/core/text/Text.js +++ b/src/core/text/Text.js @@ -6,6 +6,7 @@ import { TEXT_GRADIENT } from '../const'; import settings from '../settings'; import TextStyle from './TextStyle'; +import trimCanvas from '../utils/trimCanvas'; const defaultDestroyOptions = { texture: true, @@ -57,7 +58,7 @@ /** * The canvas 2d context that everything is drawn with - * @member {HTMLCanvasElement} + * @member {CanvasRenderingContext2D} */ this.context = this.canvas.getContext('2d'); @@ -189,10 +190,12 @@ if (style.dropShadow) { + this.context.shadowBlur = style.dropShadowBlur; + this.context.globalAlpha = style.dropShadowAlpha; + if (style.dropShadowBlur > 0) { this.context.shadowColor = style.dropShadowColor; - this.context.shadowBlur = style.dropShadowBlur; } else { @@ -237,6 +240,10 @@ } } + // reset the shadow blur and alpha that was set by the drop shadow, for the regular text + this.context.shadowBlur = 0; + this.context.globalAlpha = 1; + // set canvas text styles this.context.fillStyle = this._generateFillStyle(style, lines); @@ -326,6 +333,15 @@ */ updateTexture() { + if (this._style.trim) + { + const trimmed = trimCanvas(this.canvas); + + this.canvas.width = trimmed.width; + this.canvas.height = trimmed.height; + this.context.putImageData(trimmed.data, 0, 0); + } + const texture = this._texture; const style = this._style; @@ -475,6 +491,19 @@ } /** + * Gets the local bounds of the text object. + * + * @param {Rectangle} rect - The output rectangle. + * @return {Rectangle} The bounds. + */ + getLocalBounds(rect) + { + this.updateText(true); + + return super.getLocalBounds.call(this, rect); + } + + /** * calculates the bounds of the Text as a rectangle. The bounds calculation takes the worldTransform into account. */ _calculateBounds() @@ -525,6 +554,29 @@ const width = this.canvas.width / this.resolution; const height = this.canvas.height / this.resolution; + // make a copy of the style settings, so we can manipulate them later + const fill = style.fill.slice(); + const fillGradientStops = style.fillGradientStops.slice(); + + // wanting to evenly distribute the fills. So an array of 4 colours should give fills of 0.25, 0.5 and 0.75 + if (!fillGradientStops.length) + { + const lengthPlus1 = fill.length + 1; + + for (let i = 1; i < lengthPlus1; ++i) + { + fillGradientStops.push(i / lengthPlus1); + } + } + + // stop the bleeding of the last gradient on the line above to the top gradient of the this line + // by hard defining the first gradient colour at point 0, and last gradient colour at point 1 + fill.unshift(style.fill[0]); + fillGradientStops.unshift(0); + + fill.push(style.fill[style.fill.length - 1]); + fillGradientStops.push(1); + if (style.fillGradientType === TEXT_GRADIENT.LINEAR_VERTICAL) { // start the gradient at the top center of the canvas, and end at the bottom middle of the canvas @@ -532,15 +584,22 @@ // we need to repeat the gradient so that each individual line of text has the same vertical gradient effect // ['#FF0000', '#00FF00', '#0000FF'] over 2 lines would create stops at 0.125, 0.25, 0.375, 0.625, 0.75, 0.875 - totalIterations = (style.fill.length + 1) * lines.length; + totalIterations = (fill.length + 1) * lines.length; currentIteration = 0; for (let i = 0; i < lines.length; i++) { currentIteration += 1; - for (let j = 0; j < style.fill.length; j++) + for (let j = 0; j < fill.length; j++) { - stop = (currentIteration / totalIterations); - gradient.addColorStop(stop, style.fill[j]); + if (fillGradientStops[j]) + { + stop = (fillGradientStops[j] / lines.length) + (i / lines.length); + } + else + { + stop = currentIteration / totalIterations; + } + gradient.addColorStop(stop, fill[j]); currentIteration++; } } @@ -552,13 +611,20 @@ // can just evenly space out the gradients in this case, as multiple lines makes no difference // to an even left to right gradient - totalIterations = style.fill.length + 1; + totalIterations = fill.length + 1; currentIteration = 1; - for (let i = 0; i < style.fill.length; i++) + for (let i = 0; i < fill.length; i++) { - stop = currentIteration / totalIterations; - gradient.addColorStop(stop, style.fill[i]); + if (fillGradientStops[i]) + { + stop = fillGradientStops[i]; + } + else + { + stop = currentIteration / totalIterations; + } + gradient.addColorStop(stop, fill[i]); currentIteration++; } } @@ -569,7 +635,7 @@ /** * Destroys this text object. * Note* Unlike a Sprite, a Text object will automatically destroy its baseTexture and texture as - * the majorety of the time the texture will not be shared with any other Sprites. + * the majority of the time the texture will not be shared with any other Sprites. * * @param {object|boolean} [options] - Options parameter. A boolean will act as if all options * have been set to that value @@ -680,7 +746,7 @@ set text(text) // eslint-disable-line require-jsdoc { - text = String(text || ' '); + text = String(text === '' || text === null || text === undefined ? ' ' : text); if (this._text === text) { diff --git a/src/core/text/TextStyle.js b/src/core/text/TextStyle.js index 3b25528..890950b 100644 --- a/src/core/text/TextStyle.js +++ b/src/core/text/TextStyle.js @@ -8,12 +8,14 @@ align: 'left', breakWords: false, dropShadow: false, + dropShadowAlpha: 1, dropShadowAngle: Math.PI / 6, dropShadowBlur: 0, dropShadowColor: '#000000', dropShadowDistance: 5, fill: 'black', fillGradientType: TEXT_GRADIENT.LINEAR_VERTICAL, + fillGradientStops: [], fontFamily: 'Arial', fontSize: 26, fontStyle: 'normal', @@ -27,6 +29,7 @@ stroke: 'black', strokeThickness: 0, textBaseline: 'alphabetic', + trim: false, wordWrap: false, wordWrapWidth: 100, }; @@ -47,6 +50,7 @@ * @param {boolean} [style.breakWords=false] - Indicates if lines can be wrapped within words, it * needs wordWrap to be set to true * @param {boolean} [style.dropShadow=false] - Set a drop shadow for the text + * @param {number} [style.dropShadowAlpha=1] - Set alpha for the drop shadow * @param {number} [style.dropShadowAngle=Math.PI/6] - Set a angle of the drop shadow * @param {number} [style.dropShadowBlur=0] - Set a shadow blur radius * @param {string} [style.dropShadowColor='#000000'] - A fill style to be used on the dropshadow e.g 'red', '#00FF00' @@ -55,8 +59,10 @@ * fillstyle that will be used on the text e.g 'red', '#00FF00'. Can be an array to create a gradient * eg ['#000000','#FFFFFF'] * {@link https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/fillStyle|MDN} - * @param {number} [style.fillGradientType=PIXI.TEXT_GRADIENT.LINEAR_VERTICAL] - If fills styles are - * supplied, this can change the type/direction of the gradient. See {@link PIXI.TEXT_GRADIENT} for possible values + * @param {number} [style.fillGradientType=PIXI.TEXT_GRADIENT.LINEAR_VERTICAL] - If fill is an array of colours + * to create a gradient, this can change the type/direction of the gradient. See {@link PIXI.TEXT_GRADIENT} + * @param {number[]} [style.fillGradientStops] - If fill is an array of colours to create a gradient, this array can set + * the stop points (numbers between 0 and 1) for the color, overriding the default behaviour of evenly spacing them. * @param {string|string[]} [style.fontFamily='Arial'] - The font family * @param {number|string} [style.fontSize=26] - The font size (as a number it converts to px, but as a string, * equivalents are '26px','20pt','160%' or '1.6em') @@ -76,6 +82,7 @@ * e.g 'blue', '#FCFF00' * @param {number} [style.strokeThickness=0] - A number that represents the thickness of the stroke. * Default is 0 (no stroke) + * @param {boolean} [style.trim=false] - Trim transparent borders * @param {string} [style.textBaseline='alphabetic'] - The baseline of the text that is rendered. * @param {boolean} [style.wordWrap=false] - Indicates if word wrap should be used * @param {number} [style.wordWrapWidth=100] - The width at which text will wrap, it needs wordWrap to be set to true @@ -152,6 +159,19 @@ } } + get dropShadowAlpha() + { + return this._dropShadowAlpha; + } + set dropShadowAlpha(dropShadowAlpha) + { + if (this._dropShadowAlpha !== dropShadowAlpha) + { + this._dropShadowAlpha = dropShadowAlpha; + this.styleID++; + } + } + get dropShadowAngle() { return this._dropShadowAngle; @@ -232,6 +252,19 @@ } } + get fillGradientStops() + { + return this._fillGradientStops; + } + set fillGradientStops(fillGradientStops) + { + if (!areArraysEqual(this._fillGradientStops,fillGradientStops)) + { + this._fillGradientStops = fillGradientStops; + this.styleID++; + } + } + get fontFamily() { return this._fontFamily; @@ -402,6 +435,19 @@ } } + get trim() + { + return this._trim; + } + set trim(trim) + { + if (this._trim !== trim) + { + this._trim = trim; + this.styleID++; + } + } + get wordWrap() { return this._wordWrap; @@ -475,3 +521,34 @@ return color; } } + +/** + * Utility function to convert hexadecimal colors to strings, and simply return the color if it's a string. + * This version can also convert array of colors + * + * @param {Array} array1 First array to compare + * @param {Array} array2 Second array to compare + * @return {boolean} Do the arrays contain the same values in the same order + */ +function areArraysEqual(array1, array2) +{ + if (!Array.isArray(array1) || !Array.isArray(array2)) + { + return false; + } + + if (array1.length !== array2.length) + { + return false; + } + + for (let i = 0; i < array1.length; ++i) + { + if (array1[i] !== array2[i]) + { + return false; + } + } + + return true; +} diff --git a/src/core/textures/BaseRenderTexture.js b/src/core/textures/BaseRenderTexture.js index 5d50146..aa727f0 100644 --- a/src/core/textures/BaseRenderTexture.js +++ b/src/core/textures/BaseRenderTexture.js @@ -1,8 +1,6 @@ import BaseTexture from './BaseTexture'; import settings from '../settings'; -const { RESOLUTION, SCALE_MODE } = settings; - /** * A BaseRenderTexture is a special texture that allows any Pixi display object to be rendered to it. * @@ -54,7 +52,7 @@ { super(null, scaleMode); - this.resolution = resolution || RESOLUTION; + this.resolution = resolution || settings.RESOLUTION; this.width = width; this.height = height; @@ -62,7 +60,7 @@ this.realWidth = this.width * this.resolution; this.realHeight = this.height * this.resolution; - this.scaleMode = scaleMode || SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; this.hasLoaded = true; /** diff --git a/src/core/textures/BaseTexture.js b/src/core/textures/BaseTexture.js index 676dca3..aeccf39 100644 --- a/src/core/textures/BaseTexture.js +++ b/src/core/textures/BaseTexture.js @@ -77,7 +77,7 @@ * @default PIXI.settings.SCALE_MODE * @see PIXI.SCALE_MODES */ - this.scaleMode = scaleMode || settings.SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; /** * Set to true once the base texture has successfully loaded. @@ -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/RenderTexture.js b/src/core/textures/RenderTexture.js index 4c5034f..b3c6297 100644 --- a/src/core/textures/RenderTexture.js +++ b/src/core/textures/RenderTexture.js @@ -54,8 +54,8 @@ /* eslint-disable prefer-rest-params, no-console */ const width = arguments[1]; const height = arguments[2]; - const scaleMode = arguments[3] || 0; - const resolution = arguments[4] || 1; + const scaleMode = arguments[3]; + const resolution = arguments[4]; // we have an old render texture.. console.warn(`Please use RenderTexture.create(${width}, ${height}) instead of the ctor directly.`); 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 ca31456..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 @@ -459,7 +497,9 @@ if (frame.x + frame.width > this.baseTexture.width || frame.y + frame.height > this.baseTexture.height) { - throw new Error(`Texture Error: frame does not fit inside the base Texture dimensions ${this}`); + throw new Error('Texture Error: frame does not fit inside the base Texture dimensions: ' + + `X: ${frame.x} + ${frame.width} > ${this.baseTexture.width} ` + + `Y: ${frame.y} + ${frame.height} > ${this.baseTexture.height}`); } // this.valid = frame && frame.width && frame.height && this.baseTexture.source && this.baseTexture.hasLoaded; @@ -520,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. @@ -528,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 2a7497b..99e585a 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)); } /** @@ -360,3 +362,43 @@ * @private */ export const BaseTextureCache = {}; + +/** + * 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/core/utils/trimCanvas.js b/src/core/utils/trimCanvas.js new file mode 100644 index 0000000..5af6de0 --- /dev/null +++ b/src/core/utils/trimCanvas.js @@ -0,0 +1,83 @@ +/** + * Trim transparent borders from a canvas + * + * @memberof PIXI + * @function trimCanvas + * @private + * @param {HTMLCanvasElement} canvas - the canvas to trim + * @returns {object} Trim data + */ +export default function trimCanvas(canvas) +{ + // https://gist.github.com/remy/784508 + + let width = canvas.width; + let height = canvas.height; + + const context = canvas.getContext('2d'); + const imageData = context.getImageData(0, 0, width, height); + const pixels = imageData.data; + const len = pixels.length; + + const bound = { + top: null, + left: null, + right: null, + bottom: null, + }; + let i; + let x; + let y; + + for (i = 0; i < len; i += 4) + { + if (pixels[i + 3] !== 0) + { + x = (i / 4) % width; + y = ~~((i / 4) / width); + + if (bound.top === null) + { + bound.top = y; + } + + if (bound.left === null) + { + bound.left = x; + } + else if (x < bound.left) + { + bound.left = x; + } + + if (bound.right === null) + { + bound.right = x + 1; + } + else if (bound.right < x) + { + bound.right = x + 1; + } + + if (bound.bottom === null) + { + bound.bottom = y; + } + else if (bound.bottom < y) + { + bound.bottom = y; + } + } + } + + width = bound.right - bound.left; + height = bound.bottom - bound.top + 1; + + const data = context.getImageData(bound.left, bound.top, width, height); + + return { + height, + width, + data, + }; +} diff --git a/src/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/extras/TilingSprite.js b/src/extras/TilingSprite.js index f90d248..fe82451 100644 --- a/src/extras/TilingSprite.js +++ b/src/extras/TilingSprite.js @@ -220,20 +220,33 @@ transform.tx * resolution, transform.ty * resolution); - // TODO - this should be rolled into the setTransform above.. - context.scale(this.tileScale.x / baseTextureResolution, this.tileScale.y / baseTextureResolution); - - context.translate(modX + (this.anchor.x * -this._width), - modY + (this.anchor.y * -this._height)); - renderer.setBlendMode(this.blendMode); // fill the pattern! context.fillStyle = this._canvasPattern; - context.fillRect(-modX, - -modY, - this._width / this.tileScale.x * baseTextureResolution, - this._height / this.tileScale.y * baseTextureResolution); + + // TODO - this should be rolled into the setTransform above.. + context.scale(this.tileScale.x / baseTextureResolution, this.tileScale.y / baseTextureResolution); + + const anchorX = this.anchor.x * -this._width; + const anchorY = this.anchor.y * -this._height; + + if (this.uvRespectAnchor) + { + context.translate(modX, modY); + + context.fillRect(-modX + anchorX, -modY + anchorY, + this._width / this.tileScale.x * baseTextureResolution, + this._height / this.tileScale.y * baseTextureResolution); + } + else + { + context.translate(modX + anchorX, modY + anchorY); + + context.fillRect(-modX, -modY, + this._width / this.tileScale.x * baseTextureResolution, + this._height / this.tileScale.y * baseTextureResolution); + } } /** diff --git a/src/extras/cacheAsBitmap.js b/src/extras/cacheAsBitmap.js index 18043a7..5dd5640 100644 --- a/src/extras/cacheAsBitmap.js +++ b/src/extras/cacheAsBitmap.js @@ -41,6 +41,9 @@ * provide a performance benefit for complex static displayObjects. * To remove simply set this property to 'false' * + * IMPORTANT GOTCHA - make sure that all your textures are preloaded BEFORE setting this property to true + * as it will take a snapshot of what is currently there. If the textures have not loaded then they will not appear. + * * @member {boolean} * @memberof PIXI.DisplayObject# */ diff --git a/src/filters/blur/BlurFilter.js b/src/filters/blur/BlurFilter.js index 340fc58..1575db9 100644 --- a/src/filters/blur/BlurFilter.js +++ b/src/filters/blur/BlurFilter.js @@ -24,10 +24,9 @@ this.blurXFilter = new BlurXFilter(strength, quality, resolution, kernelSize); this.blurYFilter = new BlurYFilter(strength, quality, resolution, kernelSize); - this.resolution = 1; this.padding = 0; - this.resolution = resolution || 1; + this.resolution = resolution || core.settings.RESOLUTION; this.quality = quality || 4; this.blur = strength || 8; } diff --git a/src/filters/blur/BlurXFilter.js b/src/filters/blur/BlurXFilter.js index def99c3..0f48889 100644 --- a/src/filters/blur/BlurXFilter.js +++ b/src/filters/blur/BlurXFilter.js @@ -31,7 +31,7 @@ fragSrc ); - this.resolution = resolution || 1; + this.resolution = resolution || core.settings.RESOLUTION; this._quality = 0; diff --git a/src/filters/blur/BlurYFilter.js b/src/filters/blur/BlurYFilter.js index 638e0f0..6d489c7 100644 --- a/src/filters/blur/BlurYFilter.js +++ b/src/filters/blur/BlurYFilter.js @@ -31,7 +31,7 @@ fragSrc ); - this.resolution = resolution || 1; + this.resolution = resolution || core.settings.RESOLUTION; this._quality = 0; diff --git a/src/filters/colormatrix/ColorMatrixFilter.js b/src/filters/colormatrix/ColorMatrixFilter.js index 8509505..679f8ec 100644 --- a/src/filters/colormatrix/ColorMatrixFilter.js +++ b/src/filters/colormatrix/ColorMatrixFilter.js @@ -75,28 +75,28 @@ out[1] = (a[0] * b[1]) + (a[1] * b[6]) + (a[2] * b[11]) + (a[3] * b[16]); out[2] = (a[0] * b[2]) + (a[1] * b[7]) + (a[2] * b[12]) + (a[3] * b[17]); out[3] = (a[0] * b[3]) + (a[1] * b[8]) + (a[2] * b[13]) + (a[3] * b[18]); - out[4] = (a[0] * b[4]) + (a[1] * b[9]) + (a[2] * b[14]) + (a[3] * b[19]); + out[4] = (a[0] * b[4]) + (a[1] * b[9]) + (a[2] * b[14]) + (a[3] * b[19]) + a[4]; // Green Channel out[5] = (a[5] * b[0]) + (a[6] * b[5]) + (a[7] * b[10]) + (a[8] * b[15]); out[6] = (a[5] * b[1]) + (a[6] * b[6]) + (a[7] * b[11]) + (a[8] * b[16]); out[7] = (a[5] * b[2]) + (a[6] * b[7]) + (a[7] * b[12]) + (a[8] * b[17]); out[8] = (a[5] * b[3]) + (a[6] * b[8]) + (a[7] * b[13]) + (a[8] * b[18]); - out[9] = (a[5] * b[4]) + (a[6] * b[9]) + (a[7] * b[14]) + (a[8] * b[19]); + out[9] = (a[5] * b[4]) + (a[6] * b[9]) + (a[7] * b[14]) + (a[8] * b[19]) + a[9]; // Blue Channel out[10] = (a[10] * b[0]) + (a[11] * b[5]) + (a[12] * b[10]) + (a[13] * b[15]); out[11] = (a[10] * b[1]) + (a[11] * b[6]) + (a[12] * b[11]) + (a[13] * b[16]); out[12] = (a[10] * b[2]) + (a[11] * b[7]) + (a[12] * b[12]) + (a[13] * b[17]); out[13] = (a[10] * b[3]) + (a[11] * b[8]) + (a[12] * b[13]) + (a[13] * b[18]); - out[14] = (a[10] * b[4]) + (a[11] * b[9]) + (a[12] * b[14]) + (a[13] * b[19]); + out[14] = (a[10] * b[4]) + (a[11] * b[9]) + (a[12] * b[14]) + (a[13] * b[19]) + a[14]; // Alpha Channel out[15] = (a[15] * b[0]) + (a[16] * b[5]) + (a[17] * b[10]) + (a[18] * b[15]); out[16] = (a[15] * b[1]) + (a[16] * b[6]) + (a[17] * b[11]) + (a[18] * b[16]); out[17] = (a[15] * b[2]) + (a[16] * b[7]) + (a[17] * b[12]) + (a[18] * b[17]); out[18] = (a[15] * b[3]) + (a[16] * b[8]) + (a[17] * b[13]) + (a[18] * b[18]); - out[19] = (a[15] * b[4]) + (a[16] * b[9]) + (a[17] * b[14]) + (a[18] * b[19]); + out[19] = (a[15] * b[4]) + (a[16] * b[9]) + (a[17] * b[14]) + (a[18] * b[19]) + a[19]; return out; } diff --git a/src/filters/colormatrix/colorMatrix.frag b/src/filters/colormatrix/colorMatrix.frag index 3aaf2b3..c73c0e9 100644 --- a/src/filters/colormatrix/colorMatrix.frag +++ b/src/filters/colormatrix/colorMatrix.frag @@ -4,32 +4,38 @@ void main(void) { - vec4 c = texture2D(uSampler, vTextureCoord); + // Un-premultiply alpha before applying the color matrix. See issue #3539. + if (c.a > 0.0) { + c.rgb /= c.a; + } + vec4 result; + result.r = (m[0] * c.r); + result.r += (m[1] * c.g); + result.r += (m[2] * c.b); + result.r += (m[3] * c.a); + result.r += m[4]; - gl_FragColor.r = (m[0] * c.r); - gl_FragColor.r += (m[1] * c.g); - gl_FragColor.r += (m[2] * c.b); - gl_FragColor.r += (m[3] * c.a); - gl_FragColor.r += m[4] * c.a; + result.g = (m[5] * c.r); + result.g += (m[6] * c.g); + result.g += (m[7] * c.b); + result.g += (m[8] * c.a); + result.g += m[9]; - gl_FragColor.g = (m[5] * c.r); - gl_FragColor.g += (m[6] * c.g); - gl_FragColor.g += (m[7] * c.b); - gl_FragColor.g += (m[8] * c.a); - gl_FragColor.g += m[9] * c.a; + result.b = (m[10] * c.r); + result.b += (m[11] * c.g); + result.b += (m[12] * c.b); + result.b += (m[13] * c.a); + result.b += m[14]; - gl_FragColor.b = (m[10] * c.r); - gl_FragColor.b += (m[11] * c.g); - gl_FragColor.b += (m[12] * c.b); - gl_FragColor.b += (m[13] * c.a); - gl_FragColor.b += m[14] * c.a; + result.a = (m[15] * c.r); + result.a += (m[16] * c.g); + result.a += (m[17] * c.b); + result.a += (m[18] * c.a); + result.a += m[19]; - gl_FragColor.a = (m[15] * c.r); - gl_FragColor.a += (m[16] * c.g); - gl_FragColor.a += (m[17] * c.b); - gl_FragColor.a += (m[18] * c.a); - gl_FragColor.a += m[19] * c.a; + // Premultiply alpha again. + result.rgb *= result.a; -// gl_FragColor = vec4(m[0]); + gl_FragColor = result; } diff --git a/src/filters/displacement/DisplacementFilter.js b/src/filters/displacement/DisplacementFilter.js index 112de0b..d5355c3 100644 --- a/src/filters/displacement/DisplacementFilter.js +++ b/src/filters/displacement/DisplacementFilter.js @@ -36,7 +36,7 @@ this.maskMatrix = maskMatrix; this.uniforms.mapSampler = sprite.texture; - this.uniforms.filterMatrix = maskMatrix.toArray(true); + this.uniforms.filterMatrix = maskMatrix; this.uniforms.scale = { x: 1, y: 1 }; if (scale === null || scale === undefined) diff --git a/src/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/InteractionData.js b/src/interaction/InteractionData.js index 02d53a3..818da35 100644 --- a/src/interaction/InteractionData.js +++ b/src/interaction/InteractionData.js @@ -21,9 +21,9 @@ this.global = new core.Point(); /** - * The target Sprite that was interacted with + * The target DisplayObject that was interacted with * - * @member {PIXI.Sprite} + * @member {PIXI.DisplayObject} */ this.target = null; @@ -33,6 +33,13 @@ * @member {Event} */ this.originalEvent = null; + + /** + * Unique identifier for this interaction + * + * @member {number} + */ + this.identifier = null; } /** diff --git a/src/interaction/InteractionEvent.js b/src/interaction/InteractionEvent.js index 75b2966..dc554f9 100644 --- a/src/interaction/InteractionEvent.js +++ b/src/interaction/InteractionEvent.js @@ -58,7 +58,7 @@ } /** - * Prevents event from reaching any objects other than the current object. + * Resets the event. * * @private */ diff --git a/src/interaction/InteractionManager.js b/src/interaction/InteractionManager.js index 7d51134..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/src/prepare/BasePrepare.js b/src/prepare/BasePrepare.js index a2005f5..09eb5e5 100644 --- a/src/prepare/BasePrepare.js +++ b/src/prepare/BasePrepare.js @@ -20,7 +20,7 @@ * * @abstract * @class - * @memberof PIXI + * @memberof PIXI.prepare */ export default class BasePrepare { @@ -107,9 +107,9 @@ /** * Upload all the textures and graphics to the GPU. * - * @param {Function|PIXI.DisplayObject|PIXI.Container} item - Either - * the container or display object to search for items to upload or - * the callback function, if items have been added using `prepare.add`. + * @param {Function|PIXI.DisplayObject|PIXI.Container|PIXI.BaseTexture|PIXI.Texture|PIXI.Graphics|PIXI.Text} item - + * Either the container or display object to search for items to upload, the items to upload themselves, + * or the callback function, if items have been added using `prepare.add`. * @param {Function} [done] - Optional callback when all queued uploads have completed */ upload(item, done) @@ -236,7 +236,8 @@ /** * Manually add an item to the uploading queue. * - * @param {PIXI.DisplayObject|PIXI.Container|*} item - Object to add to the queue + * @param {PIXI.DisplayObject|PIXI.Container|PIXI.BaseTexture|PIXI.Texture|PIXI.Graphics|PIXI.Text|*} item - Object to + * add to the queue * @return {PIXI.CanvasPrepare} Instance of plugin for chaining. */ add(item) diff --git a/src/prepare/canvas/CanvasPrepare.js b/src/prepare/canvas/CanvasPrepare.js index 4b99d22..0224bb5 100644 --- a/src/prepare/canvas/CanvasPrepare.js +++ b/src/prepare/canvas/CanvasPrepare.js @@ -12,7 +12,8 @@ * An instance of this class is automatically created by default, and can be found at renderer.plugins.prepare * * @class - * @memberof PIXI + * @extends PIXI.prepare.BasePrepare + * @memberof PIXI.prepare */ export default class CanvasPrepare extends BasePrepare { diff --git a/src/prepare/webgl/WebGLPrepare.js b/src/prepare/webgl/WebGLPrepare.js index 1ef7467..eb7023f 100644 --- a/src/prepare/webgl/WebGLPrepare.js +++ b/src/prepare/webgl/WebGLPrepare.js @@ -7,7 +7,8 @@ * An instance of this class is automatically created by default, and can be found at renderer.plugins.prepare * * @class - * @memberof PIXI + * @extends PIXI.prepare.BasePrepare + * @memberof PIXI.prepare */ export default class WebGLPrepare extends BasePrepare { diff --git a/test/core/Circle.js b/test/core/Circle.js index 183f98f..f621992 100644 --- a/test/core/Circle.js +++ b/test/core/Circle.js @@ -53,6 +53,10 @@ expect(circ1.contains(10, 16)).to.be.false; expect(circ1.contains(11, 15)).to.be.false; expect(circ1.contains(0, 0)).to.be.false; + + const circ2 = new PIXI.Circle(10, 10, 0); + + expect(circ2.contains(10, 10)).to.be.false; }); it('should return framing rectangle', function () diff --git a/test/core/Container.js b/test/core/Container.js index 8a10b40..cefde54 100644 --- a/test/core/Container.js +++ b/test/core/Container.js @@ -1,5 +1,39 @@ 'use strict'; +function testAddChild(fn) +{ + return function () + { + fn(function (container, obj) + { + container.addChild(obj); + }); + fn(function (container, obj) + { + container.addChildAt(obj); + }); + }; +} + +function testRemoveChild(fn) +{ + return function () + { + 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 +104,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 +279,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/Ellipse.js b/test/core/Ellipse.js index f2e8234..3f47442 100644 --- a/test/core/Ellipse.js +++ b/test/core/Ellipse.js @@ -56,6 +56,10 @@ expect(ellipse1.contains(10, 16)).to.be.false; expect(ellipse1.contains(11, 15)).to.be.false; expect(ellipse1.contains(0, 0)).to.be.false; + + const ellipse2 = new PIXI.Ellipse(10, 10, 0, 0); + + expect(ellipse2.contains(10, 10)).to.be.false; }); it('should return framing rectangle', function () diff --git a/test/core/Graphics.js b/test/core/Graphics.js index 2a64946..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 () @@ -127,6 +130,16 @@ expect(graphics.containsPoint(point)).to.be.false; }); + + it('should return false when no fill', function () + { + const point = new PIXI.Point(1, 1); + const graphics = new PIXI.Graphics(); + + graphics.drawRect(0, 0, 10, 10); + + expect(graphics.containsPoint(point)).to.be.false; + }); }); describe('arc', function () @@ -208,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/Matrix.js b/test/core/Matrix.js index 9d2694b..76bd57c 100644 --- a/test/core/Matrix.js +++ b/test/core/Matrix.js @@ -107,10 +107,60 @@ expect(m1.ty).to.equal(m2.ty); }); + it('should prepend matrix', function () + { + const m1 = new PIXI.Matrix(); + const m2 = new PIXI.Matrix(); + + m2.set(2, 3, 4, 5, 100, 200); + m1.prepend(m2); + + expect(m1.a).to.equal(m2.a); + expect(m1.b).to.equal(m2.b); + expect(m1.c).to.equal(m2.c); + expect(m1.d).to.equal(m2.d); + expect(m1.tx).to.equal(m2.tx); + expect(m1.ty).to.equal(m2.ty); + + const m3 = new PIXI.Matrix(); + const m4 = new PIXI.Matrix(); + + m3.prepend(m4); + + expect(m3.a).to.equal(m4.a); + expect(m3.b).to.equal(m4.b); + expect(m3.c).to.equal(m4.c); + expect(m3.d).to.equal(m4.d); + expect(m3.tx).to.equal(m4.tx); + expect(m3.ty).to.equal(m4.ty); + }); + it('should get IDENTITY and TEMP_MATRIX', function () { expect(PIXI.Matrix.IDENTITY instanceof PIXI.Matrix).to.be.true; expect(PIXI.Matrix.TEMP_MATRIX instanceof PIXI.Matrix).to.be.true; }); -}); + it('should reset matrix to default when identity() is called', function () + { + const matrix = new PIXI.Matrix(); + + matrix.set(2, 3, 4, 5, 100, 200); + + expect(matrix.a).to.equal(2); + expect(matrix.b).to.equal(3); + expect(matrix.c).to.equal(4); + expect(matrix.d).to.equal(5); + expect(matrix.tx).to.equal(100); + expect(matrix.ty).to.equal(200); + + matrix.identity(); + + expect(matrix.a).to.equal(1); + expect(matrix.b).to.equal(0); + expect(matrix.c).to.equal(0); + expect(matrix.d).to.equal(1); + expect(matrix.tx).to.equal(0); + expect(matrix.ty).to.equal(0); + }); +}); 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/Text.js b/test/core/Text.js index b8ce561..5799ad4 100644 --- a/test/core/Text.js +++ b/test/core/Text.js @@ -112,4 +112,42 @@ expect(text.width).to.equal(300); }); }); + + describe('text', function () + { + it('should convert numbers into strings', function () + { + const text = new PIXI.Text(2); + + expect(text.text).to.equal('2'); + }); + + it('should not change 0 to \'\'', function () + { + const text = new PIXI.Text(0); + + expect(text.text).to.equal('0'); + }); + + it('should prevent setting null', function () + { + const text = new PIXI.Text(null); + + expect(text.text).to.equal(' '); + }); + + it('should prevent setting undefined', function () + { + const text = new PIXI.Text(); + + expect(text.text).to.equal(' '); + }); + + it('should prevent setting \'\'', function () + { + const text = new PIXI.Text(''); + + expect(text.text).to.equal(' '); + }); + }); }); diff --git a/test/core/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/TilingSprite.js b/test/core/TilingSprite.js index 2e449a7..7694792 100644 --- a/test/core/TilingSprite.js +++ b/test/core/TilingSprite.js @@ -24,4 +24,25 @@ expect(bounds.height).to.equal(600); }); }); + + it('checks if tilingSprite contains a point', function () + { + const texture = new PIXI.Texture(new PIXI.BaseTexture()); + const tilingSprite = new PIXI.extras.TilingSprite(texture, 200, 300); + + expect(tilingSprite.containsPoint(new PIXI.Point(1, 1))).to.equal(true); + expect(tilingSprite.containsPoint(new PIXI.Point(300, 400))).to.equal(false); + }); + + it('gets and sets height and width correctly', function () + { + const texture = new PIXI.Texture(new PIXI.BaseTexture()); + const tilingSprite = new PIXI.extras.TilingSprite(texture, 200, 300); + + tilingSprite.width = 400; + tilingSprite.height = 600; + + expect(tilingSprite.width).to.equal(400); + expect(tilingSprite.height).to.equal(600); + }); }); diff --git a/test/core/WebGLRenderer.js b/test/core/WebGLRenderer.js new file mode 100644 index 0000000..3d6cf98 --- /dev/null +++ b/test/core/WebGLRenderer.js @@ -0,0 +1,21 @@ +'use strict'; + +const withGL = require('../withGL'); + +describe('PIXI.WebGLRenderer', function () +{ + it('setting option legacy should disable VAOs and SPRITE_MAX_TEXTURES', withGL(function () + { + const renderer = new PIXI.WebGLRenderer(1, 1, { legacy: true }); + + 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/getLocalBounds.js b/test/core/getLocalBounds.js index d160a56..309da0e 100644 --- a/test/core/getLocalBounds.js +++ b/test/core/getLocalBounds.js @@ -196,4 +196,13 @@ expect(bounds.width).to.equal(100); expect(bounds.height).to.equal(100); }); + + it('should register correct local-bounds with a Text', function () + { + const text = new PIXI.Text('hello'); + const bounds = text.getLocalBounds(); + + expect(bounds.width).to.not.equal(0); + expect(bounds.height).to.not.equal(0); + }); }); diff --git a/test/core/index.js b/test/core/index.js index 2a056a2..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'); @@ -23,4 +24,7 @@ require('./Circle'); require('./Graphics'); 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..bf22490 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); @@ -58,9 +97,15 @@ */ mousedown(x, y) { + const mouseEvent = new MouseEvent('mousedown', { + clientX: x, + clientY: y, + preventDefault: sinon.stub(), + }); + this.setPosition(x, y); this.render(); - this.interaction.onMouseDown({ clientX: 0, clientY: 0, preventDefault: sinon.stub() }); + this.interaction.onPointerDown(mouseEvent); } /** @@ -69,9 +114,15 @@ */ mouseup(x, y) { + const mouseEvent = new MouseEvent('mouseup', { + clientX: x, + clientY: y, + preventDefault: sinon.stub(), + }); + this.setPosition(x, y); this.render(); - this.interaction.onMouseUp({ clientX: 0, clientY: 0, preventDefault: sinon.stub() }); + this.interaction.onPointerUp(mouseEvent); } /** @@ -90,12 +141,16 @@ */ touchstart(x, y) { + const touchEvent = new TouchEvent('touchstart', { + preventDefault: sinon.stub(), + changedTouches: [ + new Touch({ identifier: 0, target: this.renderer.view }), + ], + }); + this.setPosition(x, y); this.render(); - this.interaction.onTouchStart({ - preventDefault: sinon.stub(), - changedTouches: [new Touch({ identifier: 0, target: this.renderer.view })], - }); + this.interaction.onPointerDown(touchEvent); } /** @@ -104,12 +159,16 @@ */ touchend(x, y) { + const touchEvent = new TouchEvent('touchend', { + preventDefault: sinon.stub(), + changedTouches: [ + new Touch({ identifier: 0, target: this.renderer.view }), + ], + }); + this.setPosition(x, y); this.render(); - this.interaction.onTouchEnd({ - preventDefault: sinon.stub(), - changedTouches: [new Touch({ identifier: 0, target: this.renderer.view })], - }); + this.interaction.onPointerUp(touchEvent); } } diff --git a/package.json b/package.json index 1271fef..71ade15 100644 --- a/package.json +++ b/package.json @@ -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/scripts/release.js b/scripts/release.js index c4c8e36..156620c 100644 --- a/scripts/release.js +++ b/scripts/release.js @@ -12,7 +12,8 @@ 'dist/**/*', 'lib/**/*', 'src/**/*', - 'scripts/**/*', + 'scripts/*', + 'scripts/renders/*', 'test/**/*', '*.json', '*.md', diff --git a/scripts/renders/client.js b/scripts/renders/client.js index 16c9565..6faee34 100755 --- a/scripts/renders/client.js +++ b/scripts/renders/client.js @@ -1,6 +1,6 @@ 'use strict'; -require('../../bin/pixi'); +require('../../dist/pixi'); PIXI.utils.skipHello(); const fs = require('fs'); 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 bc730db..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. @@ -33,8 +33,11 @@ * need to call toDataUrl on the webgl context * @param {number} [options.resolution=1] - The resolution / device pixel ratio of the renderer, retina would be 2 * @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 @@ -43,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. */ @@ -73,7 +99,7 @@ */ stop() { - this.ticker.stop(); + this._ticker.stop(); } /** @@ -81,7 +107,7 @@ */ start() { - this.ticker.start(); + this._ticker.start(); } /** @@ -95,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 index 81e076b..78b17e6 100644 --- a/src/core/Shader.js +++ b/src/core/Shader.js @@ -1,9 +1,7 @@ import { GLShader } from 'pixi-gl-core'; import settings from './settings'; -const { PRECISION } = settings; - -function checkPrecision(src) +function checkPrecision(src, def) { if (src instanceof Array) { @@ -11,14 +9,14 @@ { const copy = src.slice(0); - copy.unshift(`precision ${PRECISION} float;`); + copy.unshift(`precision ${def} float;`); return copy; } } else if (src.substring(0, 9) !== 'precision') { - return `precision ${PRECISION} float;\n${src}`; + return `precision ${def} float;\n${src}`; } return src; @@ -42,6 +40,7 @@ */ constructor(gl, vertexSrc, fragmentSrc) { - super(gl, checkPrecision(vertexSrc), checkPrecision(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 3425b41..f9235e4 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -181,7 +181,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 b3792ef..17fde8d 100644 --- a/src/core/display/Container.js +++ b/src/core/display/Container.js @@ -73,13 +73,14 @@ } child.parent = this; - - // ensure a transform will be recalculated.. - this.transform._parentID = -1; - this._boundsID++; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.push(child); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(this.children.length - 1); child.emit('added', this); @@ -108,9 +109,14 @@ } child.parent = this; + // ensure child transform will be recalculated + child.transform._parentID = -1; this.children.splice(index, 0, child); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(index); child.emit('added', this); @@ -174,6 +180,7 @@ removeItems(this.children, currentIndex, 1); // remove from old position this.children.splice(index, 0, child); // add at new position + this.onChildrenChange(index); } @@ -220,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! @@ -244,9 +252,14 @@ { const child = this.getChildAt(index); + // ensure child transform will be recalculated.. child.parent = null; + child.transform._parentID = -1; removeItems(this.children, index, 1); + // ensure bounds will be recalculated + this._boundsID++; + // TODO - lets either do all callbacks or all events.. not both! this.onChildrenChange(index); child.emit('removed', this); @@ -275,8 +288,14 @@ for (let i = 0; i < removed.length; ++i) { removed[i].parent = null; + if (removed[i].transform) + { + removed[i].transform._parentID = -1; + } } + this._boundsID++; + this.onChildrenChange(beginIndex); for (let i = 0; i < removed.length; ++i) diff --git a/src/core/display/DisplayObject.js b/src/core/display/DisplayObject.js index 3affe36..4e6c77d 100644 --- a/src/core/display/DisplayObject.js +++ b/src/core/display/DisplayObject.js @@ -109,7 +109,7 @@ /** * The original, cached mask of the object * - * @member {PIXI.Rectangle} + * @member {PIXI.Graphics|PIXI.Sprite} * @private */ this._mask = null; diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index 95f2af4..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 ); @@ -1065,10 +1061,15 @@ canvasRenderer = new CanvasRenderer(); } - tempMatrix.tx = -bounds.x; - tempMatrix.ty = -bounds.y; + this.transform.updateLocalTransform(); + this.transform.localTransform.copy(tempMatrix); - canvasRenderer.render(this, canvasBuffer, false, tempMatrix); + tempMatrix.invert(); + + tempMatrix.tx -= bounds.x; + tempMatrix.ty -= bounds.y; + + canvasRenderer.render(this, canvasBuffer, true, tempMatrix); const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode); 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 e67680f..867dde2 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/buildCircle.js b/src/core/graphics/webgl/utils/buildCircle.js index 0400d13..abf5d02 100644 --- a/src/core/graphics/webgl/utils/buildCircle.js +++ b/src/core/graphics/webgl/utils/buildCircle.js @@ -33,6 +33,11 @@ height = circleData.height; } + if (width === 0 || height === 0) + { + return; + } + const totalSegs = Math.floor(30 * Math.sqrt(circleData.radius)) || Math.floor(15 * Math.sqrt(circleData.width + circleData.height)); diff --git a/src/core/graphics/webgl/utils/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 90155f9..f03fe15 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -30,6 +30,7 @@ export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; export { default as CanvasGraphicsRenderer } from './graphics/canvas/CanvasGraphicsRenderer'; +export { default as Spritesheet } from './textures/Spritesheet'; export { default as Texture } from './textures/Texture'; export { default as BaseTexture } from './textures/BaseTexture'; export { default as RenderTexture } from './textures/RenderTexture'; diff --git a/src/core/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 ccce680..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); } /** @@ -162,6 +162,9 @@ if (transform) { transform.copy(tempWt); + + // lets not forget to flag the parent transform as dirty... + this._tempDisplayObjectParent.transform._worldID = -1; } else { @@ -169,6 +172,7 @@ } displayObject.parent = this._tempDisplayObjectParent; + displayObject.updateTransform(); displayObject.parent = cacheParent; // displayObject.hitArea = //TODO add a temp hit area @@ -279,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/canvas/utils/CanvasRenderTarget.js b/src/core/renderers/canvas/utils/CanvasRenderTarget.js index 3de0aef..b409891 100644 --- a/src/core/renderers/canvas/utils/CanvasRenderTarget.js +++ b/src/core/renderers/canvas/utils/CanvasRenderTarget.js @@ -1,5 +1,4 @@ import settings from '../../../settings'; -const { RESOLUTION } = settings; /** * Creates a Canvas element of the given size. @@ -30,7 +29,7 @@ */ this.context = this.canvas.getContext('2d'); - this.resolution = resolution || RESOLUTION; + this.resolution = resolution || settings.RESOLUTION; this.resize(width, height); } 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 ce0e1c4..788ed6c 100644 --- a/src/core/renderers/webgl/WebGLRenderer.js +++ b/src/core/renderers/webgl/WebGLRenderer.js @@ -30,8 +30,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 @@ -49,10 +49,19 @@ * enable this if you need to call toDataUrl on the webgl context. * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when * rendering, stopping pixel interpolation. + * @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; + + if (this.legacy) + { + glCore.VertexArrayObject.FORCE_NATIVE = true; + } /** * The type of this renderer as a standardised const @@ -229,7 +238,7 @@ this.emit('context', gl); // setup the width/height properties and gl viewport - this.resize(this.width, this.height); + this.resize(this.screen.width, this.screen.height); } /** @@ -323,16 +332,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) { @@ -376,6 +385,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 @@ -440,9 +469,10 @@ * Changes the current shader to the one given in parameter * * @param {PIXI.Shader} shader - the new shader + * @param {boolean} [autoProject=true] - Whether automatically set the projection matrix * @return {PIXI.WebGLRenderer} Returns itself. */ - bindShader(shader) + bindShader(shader, autoProject) { // TODO cache if (this._activeShader !== shader) @@ -450,8 +480,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 bbab3b3..944f41d 100644 --- a/src/core/renderers/webgl/filters/Filter.js +++ b/src/core/renderers/webgl/filters/Filter.js @@ -1,6 +1,7 @@ import extractUniformsFromSrc from './extractUniformsFromSrc'; import { uid } from '../../../utils'; import { BLEND_MODES } from '../../../const'; +import settings from '../../../settings'; const SOURCE_KEY_MAP = {}; @@ -10,7 +11,7 @@ * @memberof PIXI * @extends PIXI.Shader */ -class Filter +export default class Filter { /** * @param {string} [vertexSrc] - The source of the vertex shader. @@ -35,8 +36,6 @@ this.blendMode = BLEND_MODES.NORMAL; - // pull out the vertex and shader uniforms if they are not specified.. - // currently this does not extract structs only default types this.uniformData = uniforms || extractUniformsFromSrc(this.vertexSrc, this.fragmentSrc, 'projectionMatrix|uSampler'); /** @@ -80,7 +79,7 @@ * * @member {number} */ - this.resolution = 1; + this.resolution = settings.RESOLUTION; /** * If enabled is true the filter is applied, if false it will not. @@ -97,8 +96,11 @@ * @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 { // --- // // this.uniforms.filterMatrix = filterManager.calculateSpriteMatrix(tempMatrix, window.panda ); @@ -170,5 +172,3 @@ ].join('\n'); } } - -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 dc7072a..6c7422d 100644 --- a/src/core/renderers/webgl/managers/FilterManager.js +++ b/src/core/renderers/webgl/managers/FilterManager.js @@ -154,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 @@ -176,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; @@ -184,7 +184,7 @@ flop = t; } - filters[i].apply(this, flip, lastState.renderTarget, true); + filters[i].apply(this, flip, lastState.renderTarget, false, currentState); this.freePotRenderTarget(flip); this.freePotRenderTarget(flop); @@ -291,9 +291,13 @@ let textureCount = 1; let currentState; - if (shader.uniforms.data.filterArea) + // 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; @@ -306,9 +310,9 @@ // use this to clamp displaced texture coords so they belong to filterArea // see displacementFilter fragment shader for an example - if (shader.uniforms.data.filterClamp) + if (shader.uniforms.filterClamp) { - currentState = this.filterData.stack[this.filterData.index]; + currentState = currentState || this.filterData.stack[this.filterData.index]; const filterClamp = shader.uniforms.filterClamp; @@ -485,7 +489,7 @@ */ destroy() { - this.shaderCache = []; + this.shaderCache = {}; this.emptyPool(); } diff --git a/src/core/renderers/webgl/utils/RenderTarget.js b/src/core/renderers/webgl/utils/RenderTarget.js index afe51ab..6d2467f 100644 --- a/src/core/renderers/webgl/utils/RenderTarget.js +++ b/src/core/renderers/webgl/utils/RenderTarget.js @@ -124,7 +124,7 @@ * @default PIXI.settings.SCALE_MODE * @see PIXI.SCALE_MODES */ - this.scaleMode = scaleMode || settings.SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; /** * Whether this object is the root element or not 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 36ea5bb..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; @@ -346,8 +346,8 @@ /** * Gets the local bounds of the sprite object. * - * @param {Rectangle} rect - The output rectangle. - * @return {Rectangle} The bounds. + * @param {PIXI.Rectangle} rect - The output rectangle. + * @return {PIXI.Rectangle} The bounds. */ getLocalBounds(rect) { @@ -440,7 +440,7 @@ * * @static * @param {number|string|PIXI.BaseTexture|HTMLCanvasElement|HTMLVideoElement} source Source to create texture from - * @return {PIXI.Texture} 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 e77b39a..90b08c0 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++) @@ -103,11 +102,18 @@ { const gl = this.renderer.gl; - // step 1: first check max textures the GPU can handle. - this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); + if (this.renderer.legacy) + { + this.MAX_TEXTURES = 1; + } + else + { + // step 1: first check max textures the GPU can handle. + this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); - // step 2: check the maximum number of if statements the shader can have too.. - this.MAX_TEXTURES = checkMaxIfStatmentsInShader(this.MAX_TEXTURES, gl); + // step 2: check the maximum number of if statements the shader can have too.. + this.MAX_TEXTURES = checkMaxIfStatmentsInShader(this.MAX_TEXTURES, gl); + } const shader = this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); @@ -130,8 +136,12 @@ .addIndex(this.indexBuffer) .addAttribute(this.vertexBuffers[i], shader.attributes.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) .addAttribute(this.vertexBuffers[i], shader.attributes.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) - .addAttribute(this.vertexBuffers[i], shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4) - .addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + .addAttribute(this.vertexBuffers[i], shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); + + if (shader.attributes.aTextureId) + { + this.vaos[i].addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + } /* eslint-enable max-len */ } @@ -378,8 +388,12 @@ .addIndex(this.indexBuffer) .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) - .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4) - .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); + + if (this.shader.attributes.aTextureId) + { + this.vaos[this.vertexCount].addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + } /* eslint-enable max-len */ } diff --git a/src/core/text/Text.js b/src/core/text/Text.js index 013625a..cec8c32 100644 --- a/src/core/text/Text.js +++ b/src/core/text/Text.js @@ -6,6 +6,7 @@ import { TEXT_GRADIENT } from '../const'; import settings from '../settings'; import TextStyle from './TextStyle'; +import trimCanvas from '../utils/trimCanvas'; const defaultDestroyOptions = { texture: true, @@ -57,7 +58,7 @@ /** * The canvas 2d context that everything is drawn with - * @member {HTMLCanvasElement} + * @member {CanvasRenderingContext2D} */ this.context = this.canvas.getContext('2d'); @@ -189,10 +190,12 @@ if (style.dropShadow) { + this.context.shadowBlur = style.dropShadowBlur; + this.context.globalAlpha = style.dropShadowAlpha; + if (style.dropShadowBlur > 0) { this.context.shadowColor = style.dropShadowColor; - this.context.shadowBlur = style.dropShadowBlur; } else { @@ -237,6 +240,10 @@ } } + // reset the shadow blur and alpha that was set by the drop shadow, for the regular text + this.context.shadowBlur = 0; + this.context.globalAlpha = 1; + // set canvas text styles this.context.fillStyle = this._generateFillStyle(style, lines); @@ -326,6 +333,15 @@ */ updateTexture() { + if (this._style.trim) + { + const trimmed = trimCanvas(this.canvas); + + this.canvas.width = trimmed.width; + this.canvas.height = trimmed.height; + this.context.putImageData(trimmed.data, 0, 0); + } + const texture = this._texture; const style = this._style; @@ -475,6 +491,19 @@ } /** + * Gets the local bounds of the text object. + * + * @param {Rectangle} rect - The output rectangle. + * @return {Rectangle} The bounds. + */ + getLocalBounds(rect) + { + this.updateText(true); + + return super.getLocalBounds.call(this, rect); + } + + /** * calculates the bounds of the Text as a rectangle. The bounds calculation takes the worldTransform into account. */ _calculateBounds() @@ -525,6 +554,29 @@ const width = this.canvas.width / this.resolution; const height = this.canvas.height / this.resolution; + // make a copy of the style settings, so we can manipulate them later + const fill = style.fill.slice(); + const fillGradientStops = style.fillGradientStops.slice(); + + // wanting to evenly distribute the fills. So an array of 4 colours should give fills of 0.25, 0.5 and 0.75 + if (!fillGradientStops.length) + { + const lengthPlus1 = fill.length + 1; + + for (let i = 1; i < lengthPlus1; ++i) + { + fillGradientStops.push(i / lengthPlus1); + } + } + + // stop the bleeding of the last gradient on the line above to the top gradient of the this line + // by hard defining the first gradient colour at point 0, and last gradient colour at point 1 + fill.unshift(style.fill[0]); + fillGradientStops.unshift(0); + + fill.push(style.fill[style.fill.length - 1]); + fillGradientStops.push(1); + if (style.fillGradientType === TEXT_GRADIENT.LINEAR_VERTICAL) { // start the gradient at the top center of the canvas, and end at the bottom middle of the canvas @@ -532,15 +584,22 @@ // we need to repeat the gradient so that each individual line of text has the same vertical gradient effect // ['#FF0000', '#00FF00', '#0000FF'] over 2 lines would create stops at 0.125, 0.25, 0.375, 0.625, 0.75, 0.875 - totalIterations = (style.fill.length + 1) * lines.length; + totalIterations = (fill.length + 1) * lines.length; currentIteration = 0; for (let i = 0; i < lines.length; i++) { currentIteration += 1; - for (let j = 0; j < style.fill.length; j++) + for (let j = 0; j < fill.length; j++) { - stop = (currentIteration / totalIterations); - gradient.addColorStop(stop, style.fill[j]); + if (fillGradientStops[j]) + { + stop = (fillGradientStops[j] / lines.length) + (i / lines.length); + } + else + { + stop = currentIteration / totalIterations; + } + gradient.addColorStop(stop, fill[j]); currentIteration++; } } @@ -552,13 +611,20 @@ // can just evenly space out the gradients in this case, as multiple lines makes no difference // to an even left to right gradient - totalIterations = style.fill.length + 1; + totalIterations = fill.length + 1; currentIteration = 1; - for (let i = 0; i < style.fill.length; i++) + for (let i = 0; i < fill.length; i++) { - stop = currentIteration / totalIterations; - gradient.addColorStop(stop, style.fill[i]); + if (fillGradientStops[i]) + { + stop = fillGradientStops[i]; + } + else + { + stop = currentIteration / totalIterations; + } + gradient.addColorStop(stop, fill[i]); currentIteration++; } } @@ -569,7 +635,7 @@ /** * Destroys this text object. * Note* Unlike a Sprite, a Text object will automatically destroy its baseTexture and texture as - * the majorety of the time the texture will not be shared with any other Sprites. + * the majority of the time the texture will not be shared with any other Sprites. * * @param {object|boolean} [options] - Options parameter. A boolean will act as if all options * have been set to that value @@ -680,7 +746,7 @@ set text(text) // eslint-disable-line require-jsdoc { - text = String(text || ' '); + text = String(text === '' || text === null || text === undefined ? ' ' : text); if (this._text === text) { diff --git a/src/core/text/TextStyle.js b/src/core/text/TextStyle.js index 3b25528..890950b 100644 --- a/src/core/text/TextStyle.js +++ b/src/core/text/TextStyle.js @@ -8,12 +8,14 @@ align: 'left', breakWords: false, dropShadow: false, + dropShadowAlpha: 1, dropShadowAngle: Math.PI / 6, dropShadowBlur: 0, dropShadowColor: '#000000', dropShadowDistance: 5, fill: 'black', fillGradientType: TEXT_GRADIENT.LINEAR_VERTICAL, + fillGradientStops: [], fontFamily: 'Arial', fontSize: 26, fontStyle: 'normal', @@ -27,6 +29,7 @@ stroke: 'black', strokeThickness: 0, textBaseline: 'alphabetic', + trim: false, wordWrap: false, wordWrapWidth: 100, }; @@ -47,6 +50,7 @@ * @param {boolean} [style.breakWords=false] - Indicates if lines can be wrapped within words, it * needs wordWrap to be set to true * @param {boolean} [style.dropShadow=false] - Set a drop shadow for the text + * @param {number} [style.dropShadowAlpha=1] - Set alpha for the drop shadow * @param {number} [style.dropShadowAngle=Math.PI/6] - Set a angle of the drop shadow * @param {number} [style.dropShadowBlur=0] - Set a shadow blur radius * @param {string} [style.dropShadowColor='#000000'] - A fill style to be used on the dropshadow e.g 'red', '#00FF00' @@ -55,8 +59,10 @@ * fillstyle that will be used on the text e.g 'red', '#00FF00'. Can be an array to create a gradient * eg ['#000000','#FFFFFF'] * {@link https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/fillStyle|MDN} - * @param {number} [style.fillGradientType=PIXI.TEXT_GRADIENT.LINEAR_VERTICAL] - If fills styles are - * supplied, this can change the type/direction of the gradient. See {@link PIXI.TEXT_GRADIENT} for possible values + * @param {number} [style.fillGradientType=PIXI.TEXT_GRADIENT.LINEAR_VERTICAL] - If fill is an array of colours + * to create a gradient, this can change the type/direction of the gradient. See {@link PIXI.TEXT_GRADIENT} + * @param {number[]} [style.fillGradientStops] - If fill is an array of colours to create a gradient, this array can set + * the stop points (numbers between 0 and 1) for the color, overriding the default behaviour of evenly spacing them. * @param {string|string[]} [style.fontFamily='Arial'] - The font family * @param {number|string} [style.fontSize=26] - The font size (as a number it converts to px, but as a string, * equivalents are '26px','20pt','160%' or '1.6em') @@ -76,6 +82,7 @@ * e.g 'blue', '#FCFF00' * @param {number} [style.strokeThickness=0] - A number that represents the thickness of the stroke. * Default is 0 (no stroke) + * @param {boolean} [style.trim=false] - Trim transparent borders * @param {string} [style.textBaseline='alphabetic'] - The baseline of the text that is rendered. * @param {boolean} [style.wordWrap=false] - Indicates if word wrap should be used * @param {number} [style.wordWrapWidth=100] - The width at which text will wrap, it needs wordWrap to be set to true @@ -152,6 +159,19 @@ } } + get dropShadowAlpha() + { + return this._dropShadowAlpha; + } + set dropShadowAlpha(dropShadowAlpha) + { + if (this._dropShadowAlpha !== dropShadowAlpha) + { + this._dropShadowAlpha = dropShadowAlpha; + this.styleID++; + } + } + get dropShadowAngle() { return this._dropShadowAngle; @@ -232,6 +252,19 @@ } } + get fillGradientStops() + { + return this._fillGradientStops; + } + set fillGradientStops(fillGradientStops) + { + if (!areArraysEqual(this._fillGradientStops,fillGradientStops)) + { + this._fillGradientStops = fillGradientStops; + this.styleID++; + } + } + get fontFamily() { return this._fontFamily; @@ -402,6 +435,19 @@ } } + get trim() + { + return this._trim; + } + set trim(trim) + { + if (this._trim !== trim) + { + this._trim = trim; + this.styleID++; + } + } + get wordWrap() { return this._wordWrap; @@ -475,3 +521,34 @@ return color; } } + +/** + * Utility function to convert hexadecimal colors to strings, and simply return the color if it's a string. + * This version can also convert array of colors + * + * @param {Array} array1 First array to compare + * @param {Array} array2 Second array to compare + * @return {boolean} Do the arrays contain the same values in the same order + */ +function areArraysEqual(array1, array2) +{ + if (!Array.isArray(array1) || !Array.isArray(array2)) + { + return false; + } + + if (array1.length !== array2.length) + { + return false; + } + + for (let i = 0; i < array1.length; ++i) + { + if (array1[i] !== array2[i]) + { + return false; + } + } + + return true; +} diff --git a/src/core/textures/BaseRenderTexture.js b/src/core/textures/BaseRenderTexture.js index 5d50146..aa727f0 100644 --- a/src/core/textures/BaseRenderTexture.js +++ b/src/core/textures/BaseRenderTexture.js @@ -1,8 +1,6 @@ import BaseTexture from './BaseTexture'; import settings from '../settings'; -const { RESOLUTION, SCALE_MODE } = settings; - /** * A BaseRenderTexture is a special texture that allows any Pixi display object to be rendered to it. * @@ -54,7 +52,7 @@ { super(null, scaleMode); - this.resolution = resolution || RESOLUTION; + this.resolution = resolution || settings.RESOLUTION; this.width = width; this.height = height; @@ -62,7 +60,7 @@ this.realWidth = this.width * this.resolution; this.realHeight = this.height * this.resolution; - this.scaleMode = scaleMode || SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; this.hasLoaded = true; /** diff --git a/src/core/textures/BaseTexture.js b/src/core/textures/BaseTexture.js index 676dca3..aeccf39 100644 --- a/src/core/textures/BaseTexture.js +++ b/src/core/textures/BaseTexture.js @@ -77,7 +77,7 @@ * @default PIXI.settings.SCALE_MODE * @see PIXI.SCALE_MODES */ - this.scaleMode = scaleMode || settings.SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; /** * Set to true once the base texture has successfully loaded. @@ -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/RenderTexture.js b/src/core/textures/RenderTexture.js index 4c5034f..b3c6297 100644 --- a/src/core/textures/RenderTexture.js +++ b/src/core/textures/RenderTexture.js @@ -54,8 +54,8 @@ /* eslint-disable prefer-rest-params, no-console */ const width = arguments[1]; const height = arguments[2]; - const scaleMode = arguments[3] || 0; - const resolution = arguments[4] || 1; + const scaleMode = arguments[3]; + const resolution = arguments[4]; // we have an old render texture.. console.warn(`Please use RenderTexture.create(${width}, ${height}) instead of the ctor directly.`); 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 ca31456..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 @@ -459,7 +497,9 @@ if (frame.x + frame.width > this.baseTexture.width || frame.y + frame.height > this.baseTexture.height) { - throw new Error(`Texture Error: frame does not fit inside the base Texture dimensions ${this}`); + throw new Error('Texture Error: frame does not fit inside the base Texture dimensions: ' + + `X: ${frame.x} + ${frame.width} > ${this.baseTexture.width} ` + + `Y: ${frame.y} + ${frame.height} > ${this.baseTexture.height}`); } // this.valid = frame && frame.width && frame.height && this.baseTexture.source && this.baseTexture.hasLoaded; @@ -520,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. @@ -528,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 2a7497b..99e585a 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)); } /** @@ -360,3 +362,43 @@ * @private */ export const BaseTextureCache = {}; + +/** + * 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/core/utils/trimCanvas.js b/src/core/utils/trimCanvas.js new file mode 100644 index 0000000..5af6de0 --- /dev/null +++ b/src/core/utils/trimCanvas.js @@ -0,0 +1,83 @@ +/** + * Trim transparent borders from a canvas + * + * @memberof PIXI + * @function trimCanvas + * @private + * @param {HTMLCanvasElement} canvas - the canvas to trim + * @returns {object} Trim data + */ +export default function trimCanvas(canvas) +{ + // https://gist.github.com/remy/784508 + + let width = canvas.width; + let height = canvas.height; + + const context = canvas.getContext('2d'); + const imageData = context.getImageData(0, 0, width, height); + const pixels = imageData.data; + const len = pixels.length; + + const bound = { + top: null, + left: null, + right: null, + bottom: null, + }; + let i; + let x; + let y; + + for (i = 0; i < len; i += 4) + { + if (pixels[i + 3] !== 0) + { + x = (i / 4) % width; + y = ~~((i / 4) / width); + + if (bound.top === null) + { + bound.top = y; + } + + if (bound.left === null) + { + bound.left = x; + } + else if (x < bound.left) + { + bound.left = x; + } + + if (bound.right === null) + { + bound.right = x + 1; + } + else if (bound.right < x) + { + bound.right = x + 1; + } + + if (bound.bottom === null) + { + bound.bottom = y; + } + else if (bound.bottom < y) + { + bound.bottom = y; + } + } + } + + width = bound.right - bound.left; + height = bound.bottom - bound.top + 1; + + const data = context.getImageData(bound.left, bound.top, width, height); + + return { + height, + width, + data, + }; +} diff --git a/src/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/extras/TilingSprite.js b/src/extras/TilingSprite.js index f90d248..fe82451 100644 --- a/src/extras/TilingSprite.js +++ b/src/extras/TilingSprite.js @@ -220,20 +220,33 @@ transform.tx * resolution, transform.ty * resolution); - // TODO - this should be rolled into the setTransform above.. - context.scale(this.tileScale.x / baseTextureResolution, this.tileScale.y / baseTextureResolution); - - context.translate(modX + (this.anchor.x * -this._width), - modY + (this.anchor.y * -this._height)); - renderer.setBlendMode(this.blendMode); // fill the pattern! context.fillStyle = this._canvasPattern; - context.fillRect(-modX, - -modY, - this._width / this.tileScale.x * baseTextureResolution, - this._height / this.tileScale.y * baseTextureResolution); + + // TODO - this should be rolled into the setTransform above.. + context.scale(this.tileScale.x / baseTextureResolution, this.tileScale.y / baseTextureResolution); + + const anchorX = this.anchor.x * -this._width; + const anchorY = this.anchor.y * -this._height; + + if (this.uvRespectAnchor) + { + context.translate(modX, modY); + + context.fillRect(-modX + anchorX, -modY + anchorY, + this._width / this.tileScale.x * baseTextureResolution, + this._height / this.tileScale.y * baseTextureResolution); + } + else + { + context.translate(modX + anchorX, modY + anchorY); + + context.fillRect(-modX, -modY, + this._width / this.tileScale.x * baseTextureResolution, + this._height / this.tileScale.y * baseTextureResolution); + } } /** diff --git a/src/extras/cacheAsBitmap.js b/src/extras/cacheAsBitmap.js index 18043a7..5dd5640 100644 --- a/src/extras/cacheAsBitmap.js +++ b/src/extras/cacheAsBitmap.js @@ -41,6 +41,9 @@ * provide a performance benefit for complex static displayObjects. * To remove simply set this property to 'false' * + * IMPORTANT GOTCHA - make sure that all your textures are preloaded BEFORE setting this property to true + * as it will take a snapshot of what is currently there. If the textures have not loaded then they will not appear. + * * @member {boolean} * @memberof PIXI.DisplayObject# */ diff --git a/src/filters/blur/BlurFilter.js b/src/filters/blur/BlurFilter.js index 340fc58..1575db9 100644 --- a/src/filters/blur/BlurFilter.js +++ b/src/filters/blur/BlurFilter.js @@ -24,10 +24,9 @@ this.blurXFilter = new BlurXFilter(strength, quality, resolution, kernelSize); this.blurYFilter = new BlurYFilter(strength, quality, resolution, kernelSize); - this.resolution = 1; this.padding = 0; - this.resolution = resolution || 1; + this.resolution = resolution || core.settings.RESOLUTION; this.quality = quality || 4; this.blur = strength || 8; } diff --git a/src/filters/blur/BlurXFilter.js b/src/filters/blur/BlurXFilter.js index def99c3..0f48889 100644 --- a/src/filters/blur/BlurXFilter.js +++ b/src/filters/blur/BlurXFilter.js @@ -31,7 +31,7 @@ fragSrc ); - this.resolution = resolution || 1; + this.resolution = resolution || core.settings.RESOLUTION; this._quality = 0; diff --git a/src/filters/blur/BlurYFilter.js b/src/filters/blur/BlurYFilter.js index 638e0f0..6d489c7 100644 --- a/src/filters/blur/BlurYFilter.js +++ b/src/filters/blur/BlurYFilter.js @@ -31,7 +31,7 @@ fragSrc ); - this.resolution = resolution || 1; + this.resolution = resolution || core.settings.RESOLUTION; this._quality = 0; diff --git a/src/filters/colormatrix/ColorMatrixFilter.js b/src/filters/colormatrix/ColorMatrixFilter.js index 8509505..679f8ec 100644 --- a/src/filters/colormatrix/ColorMatrixFilter.js +++ b/src/filters/colormatrix/ColorMatrixFilter.js @@ -75,28 +75,28 @@ out[1] = (a[0] * b[1]) + (a[1] * b[6]) + (a[2] * b[11]) + (a[3] * b[16]); out[2] = (a[0] * b[2]) + (a[1] * b[7]) + (a[2] * b[12]) + (a[3] * b[17]); out[3] = (a[0] * b[3]) + (a[1] * b[8]) + (a[2] * b[13]) + (a[3] * b[18]); - out[4] = (a[0] * b[4]) + (a[1] * b[9]) + (a[2] * b[14]) + (a[3] * b[19]); + out[4] = (a[0] * b[4]) + (a[1] * b[9]) + (a[2] * b[14]) + (a[3] * b[19]) + a[4]; // Green Channel out[5] = (a[5] * b[0]) + (a[6] * b[5]) + (a[7] * b[10]) + (a[8] * b[15]); out[6] = (a[5] * b[1]) + (a[6] * b[6]) + (a[7] * b[11]) + (a[8] * b[16]); out[7] = (a[5] * b[2]) + (a[6] * b[7]) + (a[7] * b[12]) + (a[8] * b[17]); out[8] = (a[5] * b[3]) + (a[6] * b[8]) + (a[7] * b[13]) + (a[8] * b[18]); - out[9] = (a[5] * b[4]) + (a[6] * b[9]) + (a[7] * b[14]) + (a[8] * b[19]); + out[9] = (a[5] * b[4]) + (a[6] * b[9]) + (a[7] * b[14]) + (a[8] * b[19]) + a[9]; // Blue Channel out[10] = (a[10] * b[0]) + (a[11] * b[5]) + (a[12] * b[10]) + (a[13] * b[15]); out[11] = (a[10] * b[1]) + (a[11] * b[6]) + (a[12] * b[11]) + (a[13] * b[16]); out[12] = (a[10] * b[2]) + (a[11] * b[7]) + (a[12] * b[12]) + (a[13] * b[17]); out[13] = (a[10] * b[3]) + (a[11] * b[8]) + (a[12] * b[13]) + (a[13] * b[18]); - out[14] = (a[10] * b[4]) + (a[11] * b[9]) + (a[12] * b[14]) + (a[13] * b[19]); + out[14] = (a[10] * b[4]) + (a[11] * b[9]) + (a[12] * b[14]) + (a[13] * b[19]) + a[14]; // Alpha Channel out[15] = (a[15] * b[0]) + (a[16] * b[5]) + (a[17] * b[10]) + (a[18] * b[15]); out[16] = (a[15] * b[1]) + (a[16] * b[6]) + (a[17] * b[11]) + (a[18] * b[16]); out[17] = (a[15] * b[2]) + (a[16] * b[7]) + (a[17] * b[12]) + (a[18] * b[17]); out[18] = (a[15] * b[3]) + (a[16] * b[8]) + (a[17] * b[13]) + (a[18] * b[18]); - out[19] = (a[15] * b[4]) + (a[16] * b[9]) + (a[17] * b[14]) + (a[18] * b[19]); + out[19] = (a[15] * b[4]) + (a[16] * b[9]) + (a[17] * b[14]) + (a[18] * b[19]) + a[19]; return out; } diff --git a/src/filters/colormatrix/colorMatrix.frag b/src/filters/colormatrix/colorMatrix.frag index 3aaf2b3..c73c0e9 100644 --- a/src/filters/colormatrix/colorMatrix.frag +++ b/src/filters/colormatrix/colorMatrix.frag @@ -4,32 +4,38 @@ void main(void) { - vec4 c = texture2D(uSampler, vTextureCoord); + // Un-premultiply alpha before applying the color matrix. See issue #3539. + if (c.a > 0.0) { + c.rgb /= c.a; + } + vec4 result; + result.r = (m[0] * c.r); + result.r += (m[1] * c.g); + result.r += (m[2] * c.b); + result.r += (m[3] * c.a); + result.r += m[4]; - gl_FragColor.r = (m[0] * c.r); - gl_FragColor.r += (m[1] * c.g); - gl_FragColor.r += (m[2] * c.b); - gl_FragColor.r += (m[3] * c.a); - gl_FragColor.r += m[4] * c.a; + result.g = (m[5] * c.r); + result.g += (m[6] * c.g); + result.g += (m[7] * c.b); + result.g += (m[8] * c.a); + result.g += m[9]; - gl_FragColor.g = (m[5] * c.r); - gl_FragColor.g += (m[6] * c.g); - gl_FragColor.g += (m[7] * c.b); - gl_FragColor.g += (m[8] * c.a); - gl_FragColor.g += m[9] * c.a; + result.b = (m[10] * c.r); + result.b += (m[11] * c.g); + result.b += (m[12] * c.b); + result.b += (m[13] * c.a); + result.b += m[14]; - gl_FragColor.b = (m[10] * c.r); - gl_FragColor.b += (m[11] * c.g); - gl_FragColor.b += (m[12] * c.b); - gl_FragColor.b += (m[13] * c.a); - gl_FragColor.b += m[14] * c.a; + result.a = (m[15] * c.r); + result.a += (m[16] * c.g); + result.a += (m[17] * c.b); + result.a += (m[18] * c.a); + result.a += m[19]; - gl_FragColor.a = (m[15] * c.r); - gl_FragColor.a += (m[16] * c.g); - gl_FragColor.a += (m[17] * c.b); - gl_FragColor.a += (m[18] * c.a); - gl_FragColor.a += m[19] * c.a; + // Premultiply alpha again. + result.rgb *= result.a; -// gl_FragColor = vec4(m[0]); + gl_FragColor = result; } diff --git a/src/filters/displacement/DisplacementFilter.js b/src/filters/displacement/DisplacementFilter.js index 112de0b..d5355c3 100644 --- a/src/filters/displacement/DisplacementFilter.js +++ b/src/filters/displacement/DisplacementFilter.js @@ -36,7 +36,7 @@ this.maskMatrix = maskMatrix; this.uniforms.mapSampler = sprite.texture; - this.uniforms.filterMatrix = maskMatrix.toArray(true); + this.uniforms.filterMatrix = maskMatrix; this.uniforms.scale = { x: 1, y: 1 }; if (scale === null || scale === undefined) diff --git a/src/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/InteractionData.js b/src/interaction/InteractionData.js index 02d53a3..818da35 100644 --- a/src/interaction/InteractionData.js +++ b/src/interaction/InteractionData.js @@ -21,9 +21,9 @@ this.global = new core.Point(); /** - * The target Sprite that was interacted with + * The target DisplayObject that was interacted with * - * @member {PIXI.Sprite} + * @member {PIXI.DisplayObject} */ this.target = null; @@ -33,6 +33,13 @@ * @member {Event} */ this.originalEvent = null; + + /** + * Unique identifier for this interaction + * + * @member {number} + */ + this.identifier = null; } /** diff --git a/src/interaction/InteractionEvent.js b/src/interaction/InteractionEvent.js index 75b2966..dc554f9 100644 --- a/src/interaction/InteractionEvent.js +++ b/src/interaction/InteractionEvent.js @@ -58,7 +58,7 @@ } /** - * Prevents event from reaching any objects other than the current object. + * Resets the event. * * @private */ diff --git a/src/interaction/InteractionManager.js b/src/interaction/InteractionManager.js index 7d51134..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/src/prepare/BasePrepare.js b/src/prepare/BasePrepare.js index a2005f5..09eb5e5 100644 --- a/src/prepare/BasePrepare.js +++ b/src/prepare/BasePrepare.js @@ -20,7 +20,7 @@ * * @abstract * @class - * @memberof PIXI + * @memberof PIXI.prepare */ export default class BasePrepare { @@ -107,9 +107,9 @@ /** * Upload all the textures and graphics to the GPU. * - * @param {Function|PIXI.DisplayObject|PIXI.Container} item - Either - * the container or display object to search for items to upload or - * the callback function, if items have been added using `prepare.add`. + * @param {Function|PIXI.DisplayObject|PIXI.Container|PIXI.BaseTexture|PIXI.Texture|PIXI.Graphics|PIXI.Text} item - + * Either the container or display object to search for items to upload, the items to upload themselves, + * or the callback function, if items have been added using `prepare.add`. * @param {Function} [done] - Optional callback when all queued uploads have completed */ upload(item, done) @@ -236,7 +236,8 @@ /** * Manually add an item to the uploading queue. * - * @param {PIXI.DisplayObject|PIXI.Container|*} item - Object to add to the queue + * @param {PIXI.DisplayObject|PIXI.Container|PIXI.BaseTexture|PIXI.Texture|PIXI.Graphics|PIXI.Text|*} item - Object to + * add to the queue * @return {PIXI.CanvasPrepare} Instance of plugin for chaining. */ add(item) diff --git a/src/prepare/canvas/CanvasPrepare.js b/src/prepare/canvas/CanvasPrepare.js index 4b99d22..0224bb5 100644 --- a/src/prepare/canvas/CanvasPrepare.js +++ b/src/prepare/canvas/CanvasPrepare.js @@ -12,7 +12,8 @@ * An instance of this class is automatically created by default, and can be found at renderer.plugins.prepare * * @class - * @memberof PIXI + * @extends PIXI.prepare.BasePrepare + * @memberof PIXI.prepare */ export default class CanvasPrepare extends BasePrepare { diff --git a/src/prepare/webgl/WebGLPrepare.js b/src/prepare/webgl/WebGLPrepare.js index 1ef7467..eb7023f 100644 --- a/src/prepare/webgl/WebGLPrepare.js +++ b/src/prepare/webgl/WebGLPrepare.js @@ -7,7 +7,8 @@ * An instance of this class is automatically created by default, and can be found at renderer.plugins.prepare * * @class - * @memberof PIXI + * @extends PIXI.prepare.BasePrepare + * @memberof PIXI.prepare */ export default class WebGLPrepare extends BasePrepare { diff --git a/test/core/Circle.js b/test/core/Circle.js index 183f98f..f621992 100644 --- a/test/core/Circle.js +++ b/test/core/Circle.js @@ -53,6 +53,10 @@ expect(circ1.contains(10, 16)).to.be.false; expect(circ1.contains(11, 15)).to.be.false; expect(circ1.contains(0, 0)).to.be.false; + + const circ2 = new PIXI.Circle(10, 10, 0); + + expect(circ2.contains(10, 10)).to.be.false; }); it('should return framing rectangle', function () diff --git a/test/core/Container.js b/test/core/Container.js index 8a10b40..cefde54 100644 --- a/test/core/Container.js +++ b/test/core/Container.js @@ -1,5 +1,39 @@ 'use strict'; +function testAddChild(fn) +{ + return function () + { + fn(function (container, obj) + { + container.addChild(obj); + }); + fn(function (container, obj) + { + container.addChildAt(obj); + }); + }; +} + +function testRemoveChild(fn) +{ + return function () + { + 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 +104,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 +279,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/Ellipse.js b/test/core/Ellipse.js index f2e8234..3f47442 100644 --- a/test/core/Ellipse.js +++ b/test/core/Ellipse.js @@ -56,6 +56,10 @@ expect(ellipse1.contains(10, 16)).to.be.false; expect(ellipse1.contains(11, 15)).to.be.false; expect(ellipse1.contains(0, 0)).to.be.false; + + const ellipse2 = new PIXI.Ellipse(10, 10, 0, 0); + + expect(ellipse2.contains(10, 10)).to.be.false; }); it('should return framing rectangle', function () diff --git a/test/core/Graphics.js b/test/core/Graphics.js index 2a64946..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 () @@ -127,6 +130,16 @@ expect(graphics.containsPoint(point)).to.be.false; }); + + it('should return false when no fill', function () + { + const point = new PIXI.Point(1, 1); + const graphics = new PIXI.Graphics(); + + graphics.drawRect(0, 0, 10, 10); + + expect(graphics.containsPoint(point)).to.be.false; + }); }); describe('arc', function () @@ -208,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/Matrix.js b/test/core/Matrix.js index 9d2694b..76bd57c 100644 --- a/test/core/Matrix.js +++ b/test/core/Matrix.js @@ -107,10 +107,60 @@ expect(m1.ty).to.equal(m2.ty); }); + it('should prepend matrix', function () + { + const m1 = new PIXI.Matrix(); + const m2 = new PIXI.Matrix(); + + m2.set(2, 3, 4, 5, 100, 200); + m1.prepend(m2); + + expect(m1.a).to.equal(m2.a); + expect(m1.b).to.equal(m2.b); + expect(m1.c).to.equal(m2.c); + expect(m1.d).to.equal(m2.d); + expect(m1.tx).to.equal(m2.tx); + expect(m1.ty).to.equal(m2.ty); + + const m3 = new PIXI.Matrix(); + const m4 = new PIXI.Matrix(); + + m3.prepend(m4); + + expect(m3.a).to.equal(m4.a); + expect(m3.b).to.equal(m4.b); + expect(m3.c).to.equal(m4.c); + expect(m3.d).to.equal(m4.d); + expect(m3.tx).to.equal(m4.tx); + expect(m3.ty).to.equal(m4.ty); + }); + it('should get IDENTITY and TEMP_MATRIX', function () { expect(PIXI.Matrix.IDENTITY instanceof PIXI.Matrix).to.be.true; expect(PIXI.Matrix.TEMP_MATRIX instanceof PIXI.Matrix).to.be.true; }); -}); + it('should reset matrix to default when identity() is called', function () + { + const matrix = new PIXI.Matrix(); + + matrix.set(2, 3, 4, 5, 100, 200); + + expect(matrix.a).to.equal(2); + expect(matrix.b).to.equal(3); + expect(matrix.c).to.equal(4); + expect(matrix.d).to.equal(5); + expect(matrix.tx).to.equal(100); + expect(matrix.ty).to.equal(200); + + matrix.identity(); + + expect(matrix.a).to.equal(1); + expect(matrix.b).to.equal(0); + expect(matrix.c).to.equal(0); + expect(matrix.d).to.equal(1); + expect(matrix.tx).to.equal(0); + expect(matrix.ty).to.equal(0); + }); +}); 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/Text.js b/test/core/Text.js index b8ce561..5799ad4 100644 --- a/test/core/Text.js +++ b/test/core/Text.js @@ -112,4 +112,42 @@ expect(text.width).to.equal(300); }); }); + + describe('text', function () + { + it('should convert numbers into strings', function () + { + const text = new PIXI.Text(2); + + expect(text.text).to.equal('2'); + }); + + it('should not change 0 to \'\'', function () + { + const text = new PIXI.Text(0); + + expect(text.text).to.equal('0'); + }); + + it('should prevent setting null', function () + { + const text = new PIXI.Text(null); + + expect(text.text).to.equal(' '); + }); + + it('should prevent setting undefined', function () + { + const text = new PIXI.Text(); + + expect(text.text).to.equal(' '); + }); + + it('should prevent setting \'\'', function () + { + const text = new PIXI.Text(''); + + expect(text.text).to.equal(' '); + }); + }); }); diff --git a/test/core/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/TilingSprite.js b/test/core/TilingSprite.js index 2e449a7..7694792 100644 --- a/test/core/TilingSprite.js +++ b/test/core/TilingSprite.js @@ -24,4 +24,25 @@ expect(bounds.height).to.equal(600); }); }); + + it('checks if tilingSprite contains a point', function () + { + const texture = new PIXI.Texture(new PIXI.BaseTexture()); + const tilingSprite = new PIXI.extras.TilingSprite(texture, 200, 300); + + expect(tilingSprite.containsPoint(new PIXI.Point(1, 1))).to.equal(true); + expect(tilingSprite.containsPoint(new PIXI.Point(300, 400))).to.equal(false); + }); + + it('gets and sets height and width correctly', function () + { + const texture = new PIXI.Texture(new PIXI.BaseTexture()); + const tilingSprite = new PIXI.extras.TilingSprite(texture, 200, 300); + + tilingSprite.width = 400; + tilingSprite.height = 600; + + expect(tilingSprite.width).to.equal(400); + expect(tilingSprite.height).to.equal(600); + }); }); diff --git a/test/core/WebGLRenderer.js b/test/core/WebGLRenderer.js new file mode 100644 index 0000000..3d6cf98 --- /dev/null +++ b/test/core/WebGLRenderer.js @@ -0,0 +1,21 @@ +'use strict'; + +const withGL = require('../withGL'); + +describe('PIXI.WebGLRenderer', function () +{ + it('setting option legacy should disable VAOs and SPRITE_MAX_TEXTURES', withGL(function () + { + const renderer = new PIXI.WebGLRenderer(1, 1, { legacy: true }); + + 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/getLocalBounds.js b/test/core/getLocalBounds.js index d160a56..309da0e 100644 --- a/test/core/getLocalBounds.js +++ b/test/core/getLocalBounds.js @@ -196,4 +196,13 @@ expect(bounds.width).to.equal(100); expect(bounds.height).to.equal(100); }); + + it('should register correct local-bounds with a Text', function () + { + const text = new PIXI.Text('hello'); + const bounds = text.getLocalBounds(); + + expect(bounds.width).to.not.equal(0); + expect(bounds.height).to.not.equal(0); + }); }); diff --git a/test/core/index.js b/test/core/index.js index 2a056a2..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'); @@ -23,4 +24,7 @@ require('./Circle'); require('./Graphics'); 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..bf22490 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); @@ -58,9 +97,15 @@ */ mousedown(x, y) { + const mouseEvent = new MouseEvent('mousedown', { + clientX: x, + clientY: y, + preventDefault: sinon.stub(), + }); + this.setPosition(x, y); this.render(); - this.interaction.onMouseDown({ clientX: 0, clientY: 0, preventDefault: sinon.stub() }); + this.interaction.onPointerDown(mouseEvent); } /** @@ -69,9 +114,15 @@ */ mouseup(x, y) { + const mouseEvent = new MouseEvent('mouseup', { + clientX: x, + clientY: y, + preventDefault: sinon.stub(), + }); + this.setPosition(x, y); this.render(); - this.interaction.onMouseUp({ clientX: 0, clientY: 0, preventDefault: sinon.stub() }); + this.interaction.onPointerUp(mouseEvent); } /** @@ -90,12 +141,16 @@ */ touchstart(x, y) { + const touchEvent = new TouchEvent('touchstart', { + preventDefault: sinon.stub(), + changedTouches: [ + new Touch({ identifier: 0, target: this.renderer.view }), + ], + }); + this.setPosition(x, y); this.render(); - this.interaction.onTouchStart({ - preventDefault: sinon.stub(), - changedTouches: [new Touch({ identifier: 0, target: this.renderer.view })], - }); + this.interaction.onPointerDown(touchEvent); } /** @@ -104,12 +159,16 @@ */ touchend(x, y) { + const touchEvent = new TouchEvent('touchend', { + preventDefault: sinon.stub(), + changedTouches: [ + new Touch({ identifier: 0, target: this.renderer.view }), + ], + }); + this.setPosition(x, y); this.render(); - this.interaction.onTouchEnd({ - preventDefault: sinon.stub(), - changedTouches: [new Touch({ identifier: 0, target: this.renderer.view })], - }); + this.interaction.onPointerUp(touchEvent); } } 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;